From 394a5be4026afcf41202d876fb29dcf4beb8ab7c Mon Sep 17 00:00:00 2001 From: Lubomir Tetak Date: Fri, 17 Dec 2021 14:52:22 +0100 Subject: [PATCH 01/38] OSX handle CMD+key up combinations in Avalonia --- native/Avalonia.Native/src/OSX/app.mm | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/native/Avalonia.Native/src/OSX/app.mm b/native/Avalonia.Native/src/OSX/app.mm index 79175d9ff1..05b129baca 100644 --- a/native/Avalonia.Native/src/OSX/app.mm +++ b/native/Avalonia.Native/src/OSX/app.mm @@ -73,6 +73,11 @@ ComPtr _events; _isHandlingSendEvent = true; @try { [super sendEvent: event]; + if ([event type] == NSEventTypeKeyUp && ([event modifierFlags] & NSEventModifierFlagCommand)) + { + [[self keyWindow] sendEvent:event]; + } + } @finally { _isHandlingSendEvent = oldHandling; } From b2b4014b1c6e5805e7c2330582124aa2f73f34ca Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 3 Feb 2022 14:58:28 +0300 Subject: [PATCH 02/38] Make DispatcherPriority to be a struct with static readonly field values In future versions this will allow us to extend priority lists without breaking binary compatibility --- src/Avalonia.Base/ApiCompatBaseline.txt | 3 +- .../Threading/AvaloniaScheduler.cs | 2 +- src/Avalonia.Base/Threading/Dispatcher.cs | 12 +-- .../Threading/DispatcherPriority.cs | 97 ++++++++++++++----- .../Threading/DispatcherTimer.cs | 4 +- src/Avalonia.Base/Threading/IDispatcher.cs | 12 +-- .../Diagnostics/ViewModels/MainViewModel.cs | 3 +- .../Avalonia.UnitTests/ImmediateDispatcher.cs | 12 +-- 8 files changed, 96 insertions(+), 49 deletions(-) diff --git a/src/Avalonia.Base/ApiCompatBaseline.txt b/src/Avalonia.Base/ApiCompatBaseline.txt index 4701a83175..7f378d2f65 100644 --- a/src/Avalonia.Base/ApiCompatBaseline.txt +++ b/src/Avalonia.Base/ApiCompatBaseline.txt @@ -1,3 +1,4 @@ Compat issues with assembly Avalonia.Base: +MembersMustExist : Member 'public System.Int32 System.Int32 Avalonia.Threading.DispatcherPriority.value__' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Threading.IDispatcher.Post(System.Action, T, Avalonia.Threading.DispatcherPriority)' is present in the implementation but not in the contract. -Total Issues: 1 +Total Issues: 2 diff --git a/src/Avalonia.Base/Threading/AvaloniaScheduler.cs b/src/Avalonia.Base/Threading/AvaloniaScheduler.cs index 397826df53..6423d86e7c 100644 --- a/src/Avalonia.Base/Threading/AvaloniaScheduler.cs +++ b/src/Avalonia.Base/Threading/AvaloniaScheduler.cs @@ -46,7 +46,7 @@ namespace Avalonia.Threading { composite.Add(action(this, state)); } - }, DispatcherPriority.DataBind); + }, DispatcherPriority.Background); composite.Add(cancellation); diff --git a/src/Avalonia.Base/Threading/Dispatcher.cs b/src/Avalonia.Base/Threading/Dispatcher.cs index 49cee441d0..2eb2e7c01f 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.cs @@ -83,42 +83,42 @@ namespace Avalonia.Threading _jobRunner.HasJobsWithPriority(minimumPriority); /// - public Task InvokeAsync(Action action, DispatcherPriority priority = DispatcherPriority.Normal) + public Task InvokeAsync(Action action, DispatcherPriority priority = default) { _ = action ?? throw new ArgumentNullException(nameof(action)); return _jobRunner.InvokeAsync(action, priority); } /// - public Task InvokeAsync(Func function, DispatcherPriority priority = DispatcherPriority.Normal) + public Task InvokeAsync(Func function, DispatcherPriority priority = default) { _ = function ?? throw new ArgumentNullException(nameof(function)); return _jobRunner.InvokeAsync(function, priority); } /// - public Task InvokeAsync(Func function, DispatcherPriority priority = DispatcherPriority.Normal) + public Task InvokeAsync(Func function, DispatcherPriority priority = default) { _ = function ?? throw new ArgumentNullException(nameof(function)); return _jobRunner.InvokeAsync(function, priority).Unwrap(); } /// - public Task InvokeAsync(Func> function, DispatcherPriority priority = DispatcherPriority.Normal) + public Task InvokeAsync(Func> function, DispatcherPriority priority = default) { _ = function ?? throw new ArgumentNullException(nameof(function)); return _jobRunner.InvokeAsync(function, priority).Unwrap(); } /// - public void Post(Action action, DispatcherPriority priority = DispatcherPriority.Normal) + public void Post(Action action, DispatcherPriority priority = default) { _ = action ?? throw new ArgumentNullException(nameof(action)); _jobRunner.Post(action, priority); } /// - public void Post(Action action, T arg, DispatcherPriority priority = DispatcherPriority.Normal) + public void Post(Action action, T arg, DispatcherPriority priority = default) { _ = action ?? throw new ArgumentNullException(nameof(action)); _jobRunner.Post(action, arg, priority); diff --git a/src/Avalonia.Base/Threading/DispatcherPriority.cs b/src/Avalonia.Base/Threading/DispatcherPriority.cs index a2b4b86bac..a93e4f406d 100644 --- a/src/Avalonia.Base/Threading/DispatcherPriority.cs +++ b/src/Avalonia.Base/Threading/DispatcherPriority.cs @@ -1,74 +1,121 @@ +using System; + namespace Avalonia.Threading { /// /// Defines the priorities with which jobs can be invoked on a . /// - // TODO: These are copied from WPF - many won't apply to Avalonia. - public enum DispatcherPriority + public readonly struct DispatcherPriority : IEquatable, IComparable { + /// + /// The integer value of the priority + /// + public int Value { get; } + + private DispatcherPriority(int value) + { + Value = value; + } + /// /// Minimum possible priority /// - MinValue = 1, - + public static readonly DispatcherPriority MinValue = new(0); + /// /// The job will be processed when the system is idle. /// - SystemIdle = 1, + [Obsolete("WPF compatibility")] public static readonly DispatcherPriority SystemIdle = MinValue; /// /// The job will be processed when the application is idle. /// - ApplicationIdle = 2, + [Obsolete("WPF compatibility")] public static readonly DispatcherPriority ApplicationIdle = MinValue; /// /// The job will be processed after background operations have completed. /// - ContextIdle = 3, + [Obsolete("WPF compatibility")] public static readonly DispatcherPriority ContextIdle = MinValue; /// - /// The job will be processed after other non-idle operations have completed. + /// The job will be processed with normal priority. /// - Background = 4, + public static readonly DispatcherPriority Normal = MinValue; /// - /// The job will be processed with the same priority as input. + /// The job will be processed after other non-idle operations have completed. /// - Input = 5, + public static readonly DispatcherPriority Background = new(1); /// - /// The job will be processed after layout and render but before input. + /// The job will be processed with the same priority as input. /// - Loaded = 6, + public static readonly DispatcherPriority Input = new(2); /// - /// The job will be processed with the same priority as render. + /// The job will be processed after layout and render but before input. /// - Render = 7, + public static readonly DispatcherPriority Loaded = new(3); /// /// The job will be processed with the same priority as render. /// - Layout = 8, - + public static readonly DispatcherPriority Render = new(5); + /// - /// The job will be processed with the same priority as data binding. + /// The job will be processed with the same priority as render. /// - DataBind = 9, + public static readonly DispatcherPriority Layout = new(6); /// - /// The job will be processed with normal priority. + /// The job will be processed with the same priority as data binding. /// - Normal = 10, + [Obsolete("WPF compatibility")] public static readonly DispatcherPriority DataBind = MinValue; /// /// The job will be processed before other asynchronous operations. /// - Send = 11, - + public static readonly DispatcherPriority Send = new(7); + /// /// Maximum possible priority /// - MaxValue = 11 + public static readonly DispatcherPriority MaxValue = Send; + + // Note: unlike ctor this one is validating + public static DispatcherPriority FromValue(int value) + { + if (value < MinValue.Value || value > MaxValue.Value) + throw new ArgumentOutOfRangeException(nameof(value)); + return new DispatcherPriority(value); + } + + public static implicit operator int(DispatcherPriority priority) => priority.Value; + + public static implicit operator DispatcherPriority(int value) => FromValue(value); + + /// + public bool Equals(DispatcherPriority other) => Value == other.Value; + + /// + public override bool Equals(object? obj) => obj is DispatcherPriority other && Equals(other); + + /// + public override int GetHashCode() => Value.GetHashCode(); + + public static bool operator ==(DispatcherPriority left, DispatcherPriority right) => left.Value == right.Value; + + public static bool operator !=(DispatcherPriority left, DispatcherPriority right) => left.Value != right.Value; + + public static bool operator <(DispatcherPriority left, DispatcherPriority right) => left.Value < right.Value; + + public static bool operator >(DispatcherPriority left, DispatcherPriority right) => left.Value > right.Value; + + public static bool operator <=(DispatcherPriority left, DispatcherPriority right) => left.Value <= right.Value; + + public static bool operator >=(DispatcherPriority left, DispatcherPriority right) => left.Value >= right.Value; + + /// + public int CompareTo(DispatcherPriority other) => Value.CompareTo(other.Value); } -} +} \ No newline at end of file diff --git a/src/Avalonia.Base/Threading/DispatcherTimer.cs b/src/Avalonia.Base/Threading/DispatcherTimer.cs index 93023b90a5..0c25d89722 100644 --- a/src/Avalonia.Base/Threading/DispatcherTimer.cs +++ b/src/Avalonia.Base/Threading/DispatcherTimer.cs @@ -123,7 +123,7 @@ namespace Avalonia.Threading /// The interval at which to tick. /// The priority to use. /// An used to cancel the timer. - public static IDisposable Run(Func action, TimeSpan interval, DispatcherPriority priority = DispatcherPriority.Normal) + public static IDisposable Run(Func action, TimeSpan interval, DispatcherPriority priority = default) { var timer = new DispatcherTimer(priority) { Interval = interval }; @@ -152,7 +152,7 @@ namespace Avalonia.Threading public static IDisposable RunOnce( Action action, TimeSpan interval, - DispatcherPriority priority = DispatcherPriority.Normal) + DispatcherPriority priority = default) { interval = (interval != TimeSpan.Zero) ? interval : TimeSpan.FromTicks(1); diff --git a/src/Avalonia.Base/Threading/IDispatcher.cs b/src/Avalonia.Base/Threading/IDispatcher.cs index cd5add70d4..eccd42bd4e 100644 --- a/src/Avalonia.Base/Threading/IDispatcher.cs +++ b/src/Avalonia.Base/Threading/IDispatcher.cs @@ -24,7 +24,7 @@ namespace Avalonia.Threading /// /// The method. /// The priority with which to invoke the method. - void Post(Action action, DispatcherPriority priority = DispatcherPriority.Normal); + void Post(Action action, DispatcherPriority priority = default); /// /// Posts an action that will be invoked on the dispatcher thread. @@ -33,7 +33,7 @@ namespace Avalonia.Threading /// 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 = DispatcherPriority.Normal); + void Post(Action action, T arg, DispatcherPriority priority = default); /// /// Invokes a action on the dispatcher thread. @@ -41,7 +41,7 @@ namespace Avalonia.Threading /// The method. /// The priority with which to invoke the method. /// A task that can be used to track the method's execution. - Task InvokeAsync(Action action, DispatcherPriority priority = DispatcherPriority.Normal); + Task InvokeAsync(Action action, DispatcherPriority priority = default); /// /// Invokes a method on the dispatcher thread. @@ -49,7 +49,7 @@ namespace Avalonia.Threading /// 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 = DispatcherPriority.Normal); + Task InvokeAsync(Func function, DispatcherPriority priority = default); /// /// Queues the specified work to run on the dispatcher thread and returns a proxy for the @@ -58,7 +58,7 @@ namespace Avalonia.Threading /// 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 = DispatcherPriority.Normal); + Task InvokeAsync(Func function, DispatcherPriority priority = default); /// /// Queues the specified work to run on the dispatcher thread and returns a proxy for the @@ -67,6 +67,6 @@ namespace Avalonia.Threading /// 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 = DispatcherPriority.Normal); + Task InvokeAsync(Func> function, DispatcherPriority priority = default); } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs index e08c5bc8dd..140515eb40 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs @@ -163,8 +163,7 @@ namespace Avalonia.Diagnostics.ViewModels } catch { } }, - TimeSpan.FromMilliseconds(0), - DispatcherPriority.ApplicationIdle); + TimeSpan.FromMilliseconds(0)); } RaiseAndSetIfChanged(ref _content, value); diff --git a/tests/Avalonia.UnitTests/ImmediateDispatcher.cs b/tests/Avalonia.UnitTests/ImmediateDispatcher.cs index 5f0d41590f..03c89732f3 100644 --- a/tests/Avalonia.UnitTests/ImmediateDispatcher.cs +++ b/tests/Avalonia.UnitTests/ImmediateDispatcher.cs @@ -16,39 +16,39 @@ namespace Avalonia.UnitTests } /// - public void Post(Action action, DispatcherPriority priority = DispatcherPriority.Normal) + public void Post(Action action, DispatcherPriority priority) { action(); } /// - public void Post(Action action, T arg, DispatcherPriority priority = DispatcherPriority.Normal) + public void Post(Action action, T arg, DispatcherPriority priority) { action(arg); } /// - public Task InvokeAsync(Action action, DispatcherPriority priority = DispatcherPriority.Normal) + public Task InvokeAsync(Action action, DispatcherPriority priority) { action(); return Task.CompletedTask; } /// - public Task InvokeAsync(Func function, DispatcherPriority priority = DispatcherPriority.Normal) + public Task InvokeAsync(Func function, DispatcherPriority priority) { var result = function(); return Task.FromResult(result); } /// - public Task InvokeAsync(Func function, DispatcherPriority priority = DispatcherPriority.Normal) + public Task InvokeAsync(Func function, DispatcherPriority priority) { return function(); } /// - public Task InvokeAsync(Func> function, DispatcherPriority priority = DispatcherPriority.Normal) + public Task InvokeAsync(Func> function, DispatcherPriority priority) { return function(); } From 5669a640d019ab5425775a80339aa73a32662dcd Mon Sep 17 00:00:00 2001 From: Kibnet Philosoff Date: Wed, 9 Feb 2022 13:13:27 +0300 Subject: [PATCH 03/38] Fix #7567 - TreeView crashes the app when trying to display the same viewmodel twice --- .../Generators/TreeContainerIndex.cs | 54 +++++++++++++++++-- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/Generators/TreeContainerIndex.cs b/src/Avalonia.Controls/Generators/TreeContainerIndex.cs index da13416700..eb60fca367 100644 --- a/src/Avalonia.Controls/Generators/TreeContainerIndex.cs +++ b/src/Avalonia.Controls/Generators/TreeContainerIndex.cs @@ -15,6 +15,7 @@ namespace Avalonia.Controls.Generators /// public class TreeContainerIndex { + private readonly Dictionary> _itemToContainerSet = new Dictionary>(); private readonly Dictionary _itemToContainer = new Dictionary(); private readonly Dictionary _containerToItem = new Dictionary(); @@ -45,14 +46,45 @@ namespace Avalonia.Controls.Generators /// The item container. public void Add(object item, IControl container) { - _itemToContainer.Add(item, container); + _itemToContainer[item] = container; + if (_itemToContainerSet.TryGetValue(item, out var set)) + { + set.Add(container); + } + else + { + _itemToContainerSet.Add(item, new HashSet { container }); + } + _containerToItem.Add(container, item); Materialized?.Invoke( - this, + this, new ItemContainerEventArgs(new ItemContainerInfo(container, item, 0))); } + /// + /// Removes a container from private collections. + /// + /// The item container. + /// The DataContext object + private void RemoveContainer(IControl container, object item) + { + if (_itemToContainerSet.TryGetValue(item, out var set)) + { + set.Remove(container); + if (set.Count == 0) + { + _itemToContainerSet.Remove(item); + _itemToContainer.Remove(item); + } + else + { + _itemToContainer[item] = set.First(); + } + } + } + /// /// Removes a container from the index. /// @@ -61,10 +93,10 @@ namespace Avalonia.Controls.Generators { var item = _containerToItem[container]; _containerToItem.Remove(container); - _itemToContainer.Remove(item); + RemoveContainer(container, item); Dematerialized?.Invoke( - this, + this, new ItemContainerEventArgs(new ItemContainerInfo(container, item, 0))); } @@ -79,7 +111,7 @@ namespace Avalonia.Controls.Generators { var item = _containerToItem[container.ContainerControl]; _containerToItem.Remove(container.ContainerControl); - _itemToContainer.Remove(item); + RemoveContainer(container.ContainerControl, item); } Dematerialized?.Invoke( @@ -97,6 +129,14 @@ namespace Avalonia.Controls.Generators if (item != null) { _itemToContainer.TryGetValue(item, out var result); + if (result == null) + { + _itemToContainerSet.TryGetValue(item, out var set); + if (set?.Count > 0) + { + return set.FirstOrDefault(); + } + } return result; } @@ -113,6 +153,10 @@ namespace Avalonia.Controls.Generators if (container != null) { _containerToItem.TryGetValue(container, out var result); + if (result != null) + { + _itemToContainer[result] = container; + } return result; } From bcc2be8d645275a41cb83c0f8d15955b56894af4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Tue, 29 Mar 2022 01:09:47 +0100 Subject: [PATCH 04/38] Fixed ScrollViewer Padding. --- src/Avalonia.Themes.Default/Controls/ScrollViewer.xaml | 2 +- src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Themes.Default/Controls/ScrollViewer.xaml b/src/Avalonia.Themes.Default/Controls/ScrollViewer.xaml index b357446bfa..aab1b76259 100644 --- a/src/Avalonia.Themes.Default/Controls/ScrollViewer.xaml +++ b/src/Avalonia.Themes.Default/Controls/ScrollViewer.xaml @@ -13,7 +13,7 @@ CanVerticallyScroll="{TemplateBinding CanVerticallyScroll}" Content="{TemplateBinding Content}" Extent="{TemplateBinding Extent, Mode=TwoWay}" - Margin="{TemplateBinding Padding}" + Padding="{TemplateBinding Padding}" Offset="{TemplateBinding Offset, Mode=TwoWay}" Viewport="{TemplateBinding Viewport, Mode=TwoWay}" IsScrollChainingEnabled="{TemplateBinding IsScrollChainingEnabled}"> diff --git a/src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml b/src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml index 53a1b721d1..b7addcb61e 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ScrollViewer.xaml @@ -32,7 +32,7 @@ CanVerticallyScroll="{TemplateBinding CanVerticallyScroll}" Content="{TemplateBinding Content}" Extent="{TemplateBinding Extent, Mode=TwoWay}" - Margin="{TemplateBinding Padding}" + Padding="{TemplateBinding Padding}" Offset="{TemplateBinding Offset, Mode=TwoWay}" Viewport="{TemplateBinding Viewport, Mode=TwoWay}" IsScrollChainingEnabled="{TemplateBinding IsScrollChainingEnabled}"> From f100caf0114c129f82641787c5320ae55cf7ae43 Mon Sep 17 00:00:00 2001 From: Tako Date: Mon, 11 Apr 2022 18:07:32 +0300 Subject: [PATCH 05/38] Disabled items in ComboBox souldnt be selectable with arrow keys. --- src/Avalonia.Controls/ComboBox.cs | 39 +++++++++++++------------------ 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index a6dee5cfaa..582f90e472 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -453,42 +453,35 @@ namespace Avalonia.Controls private void SelectNext() { - int next = SelectedIndex + 1; - - if (next >= ItemCount) + if (ItemCount >= 1) { - if (WrapSelection == true) + if (SelectedIndex == -1) { - next = 0; + if (MoveSelection(NavigationDirection.First, WrapSelection) == false) + { + // MoveSelection works only with indexes starting from 0 + // so to make it search further than the first item we need to set SelectedIndex to 0. + SelectedIndex = 0; + var isSelectionMoved = MoveSelection(NavigationDirection.Next, WrapSelection); + if (isSelectionMoved == false) + { + SelectedIndex = -1; + } + } } else { - return; + MoveSelection(NavigationDirection.Next, WrapSelection); } } - - - - SelectedIndex = next; } private void SelectPrev() { - int prev = SelectedIndex - 1; - - if (prev < 0) + if (ItemCount >= 1) { - if (WrapSelection == true) - { - prev = ItemCount - 1; - } - else - { - return; - } + MoveSelection(NavigationDirection.Previous, WrapSelection); } - - SelectedIndex = prev; } } } From 9bef2acafe18551bd0d717c316864677e6400814 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 14 Apr 2022 11:47:25 -0400 Subject: [PATCH 06/38] Move pointerover logic out of input device layer --- src/Avalonia.Controls/TopLevel.cs | 25 +- src/Avalonia.Input/IInputRoot.cs | 3 - src/Avalonia.Input/IKeyboardDevice.cs | 6 - src/Avalonia.Input/IMouseDevice.cs | 2 + src/Avalonia.Input/IPointerDevice.cs | 16 +- src/Avalonia.Input/KeyboardDevice.cs | 2 +- src/Avalonia.Input/MouseDevice.cs | 328 +++--------------- src/Avalonia.Input/PointerEventArgs.cs | 2 + src/Avalonia.Input/PointerOverPreProcessor.cs | 210 +++++++++++ src/Avalonia.Input/Raw/RawDragEvent.cs | 2 +- src/Avalonia.Input/Raw/RawInputHelpers.cs | 27 ++ src/Avalonia.Input/Raw/RawPointerEventArgs.cs | 2 + src/Avalonia.Input/TouchDevice.cs | 47 ++- 13 files changed, 358 insertions(+), 314 deletions(-) create mode 100644 src/Avalonia.Input/PointerOverPreProcessor.cs create mode 100644 src/Avalonia.Input/Raw/RawInputHelpers.cs diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 55202dd20d..75a34659a2 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -15,7 +15,6 @@ using Avalonia.Rendering; using Avalonia.Styling; using Avalonia.Utilities; using Avalonia.VisualTree; -using JetBrains.Annotations; namespace Avalonia.Controls { @@ -87,6 +86,8 @@ namespace Avalonia.Controls private readonly IKeyboardNavigationHandler? _keyboardNavigationHandler; private readonly IPlatformRenderInterface? _renderInterface; private readonly IGlobalStyles? _globalStyles; + private readonly PointerOverPreProcessor? _pointerOverPreProcessor; + private readonly IDisposable? _pointerOverPreProcessorSubscription; private Size _clientSize; private Size? _frameSize; private WindowTransparencyLevel _actualTransparencyLevel; @@ -195,6 +196,9 @@ namespace Avalonia.Controls } impl.LostFocus += PlatformImpl_LostFocus; + + _pointerOverPreProcessor = new PointerOverPreProcessor(this); + _pointerOverPreProcessorSubscription = _inputManager?.PreProcess.Subscribe(_pointerOverPreProcessor); } /// @@ -283,9 +287,7 @@ namespace Avalonia.Controls /// IKeyboardNavigationHandler IInputRoot.KeyboardNavigationHandler => _keyboardNavigationHandler!; - /// - /// Gets or sets the input element that the pointer is currently over. - /// + /// IInputElement? IInputRoot.PointerOverElement { get { return GetValue(PointerOverElementProperty); } @@ -378,10 +380,12 @@ namespace Avalonia.Controls Renderer?.Dispose(); Renderer = null!; - - (this as IInputRoot).MouseDevice?.TopLevelClosed(this); + + _pointerOverPreProcessor?.OnCompleted(); + _pointerOverPreProcessorSubscription?.Dispose(); + PlatformImpl = null; - + var logicalArgs = new LogicalTreeAttachmentEventArgs(this, this, null); ((ILogical)this).NotifyDetachedFromLogicalTree(logicalArgs); @@ -515,12 +519,17 @@ namespace Avalonia.Controls /// The event args. private void HandleInput(RawInputEventArgs e) { + if (e is RawPointerEventArgs pointerArgs) + { + pointerArgs.InputHitTestResult = this.InputHitTest(pointerArgs.Position); + } + _inputManager?.ProcessInput(e); } private void SceneInvalidated(object? sender, SceneInvalidatedEventArgs e) { - (this as IInputRoot).MouseDevice?.SceneInvalidated(this, e.DirtyRect); + _pointerOverPreProcessor?.SceneInvalidated(e.DirtyRect); } void PlatformImpl_LostFocus() diff --git a/src/Avalonia.Input/IInputRoot.cs b/src/Avalonia.Input/IInputRoot.cs index 3e2b8cc477..98e8699573 100644 --- a/src/Avalonia.Input/IInputRoot.cs +++ b/src/Avalonia.Input/IInputRoot.cs @@ -1,5 +1,3 @@ -using JetBrains.Annotations; - namespace Avalonia.Input { /// @@ -30,7 +28,6 @@ namespace Avalonia.Input /// /// Gets associated mouse device /// - [CanBeNull] IMouseDevice? MouseDevice { get; } } } diff --git a/src/Avalonia.Input/IKeyboardDevice.cs b/src/Avalonia.Input/IKeyboardDevice.cs index 9506dc36fb..d0e84e5ad0 100644 --- a/src/Avalonia.Input/IKeyboardDevice.cs +++ b/src/Avalonia.Input/IKeyboardDevice.cs @@ -50,12 +50,6 @@ namespace Avalonia.Input KeyboardMask = Alt | Control | Shift | Meta } - internal static class KeyModifiersUtils - { - public static KeyModifiers ConvertToKey(RawInputModifiers modifiers) => - (KeyModifiers)(modifiers & RawInputModifiers.KeyboardMask); - } - public interface IKeyboardDevice : IInputDevice, INotifyPropertyChanged { IInputElement? FocusedElement { get; } diff --git a/src/Avalonia.Input/IMouseDevice.cs b/src/Avalonia.Input/IMouseDevice.cs index 272d1eb8d7..6b7f0e76e5 100644 --- a/src/Avalonia.Input/IMouseDevice.cs +++ b/src/Avalonia.Input/IMouseDevice.cs @@ -13,8 +13,10 @@ namespace Avalonia.Input [Obsolete("Use PointerEventArgs.GetPosition")] PixelPoint Position { get; } + [Obsolete] void TopLevelClosed(IInputRoot root); + [Obsolete] void SceneInvalidated(IInputRoot root, Rect rect); } } diff --git a/src/Avalonia.Input/IPointerDevice.cs b/src/Avalonia.Input/IPointerDevice.cs index 1f82cb1ed7..0096bb77bf 100644 --- a/src/Avalonia.Input/IPointerDevice.cs +++ b/src/Avalonia.Input/IPointerDevice.cs @@ -1,17 +1,31 @@ using System; using Avalonia.VisualTree; +using Avalonia.Input.Raw; namespace Avalonia.Input { public interface IPointerDevice : IInputDevice { + /// [Obsolete("Use IPointer")] IInputElement? Captured { get; } - + + /// [Obsolete("Use IPointer")] void Capture(IInputElement? control); + /// [Obsolete("Use PointerEventArgs.GetPosition")] Point GetPosition(IVisual relativeTo); + + /// + /// Gets a pointer for specific event args. + /// + /// + /// If pointer doesn't exist or wasn't yet created this method will return null. + /// + /// Raw pointer event args associated with the pointer. + /// The pointer. + IPointer? TryGetPointer(RawPointerEventArgs ev); } } diff --git a/src/Avalonia.Input/KeyboardDevice.cs b/src/Avalonia.Input/KeyboardDevice.cs index 3df717b8c4..0600b54618 100644 --- a/src/Avalonia.Input/KeyboardDevice.cs +++ b/src/Avalonia.Input/KeyboardDevice.cs @@ -188,7 +188,7 @@ namespace Avalonia.Input RoutedEvent = routedEvent, Device = this, Key = keyInput.Key, - KeyModifiers = KeyModifiersUtils.ConvertToKey(keyInput.Modifiers), + KeyModifiers = keyInput.Modifiers.ToKeyModifiers(), Source = element, }; diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs index a5d54bb047..5f8ab24b79 100644 --- a/src/Avalonia.Input/MouseDevice.cs +++ b/src/Avalonia.Input/MouseDevice.cs @@ -21,27 +21,17 @@ namespace Avalonia.Input private readonly Pointer _pointer; private bool _disposed; - private PixelPoint? _position; + private PixelPoint? _position; + private MouseButton _lastMouseDownButton; public MouseDevice(Pointer? pointer = null) { _pointer = pointer ?? new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); } - - /// - /// Gets the control that is currently capturing by the mouse, if any. - /// - /// - /// When an element captures the mouse, it receives mouse input whether the cursor is - /// within the control's bounds or not. To set the mouse capture, call the - /// method. - /// + [Obsolete("Use IPointer instead")] public IInputElement? Captured => _pointer.Captured; - /// - /// Gets the mouse position, in screen coordinates. - /// [Obsolete("Use events instead")] public PixelPoint Position { @@ -49,15 +39,7 @@ namespace Avalonia.Input protected set => _position = value; } - /// - /// Captures mouse input to the specified control. - /// - /// The control. - /// - /// When an element captures the mouse, it receives mouse input whether the cursor is - /// within the control's bounds or not. The current mouse capture control is exposed - /// by the property. - /// + [Obsolete("Use IPointer instead")] public void Capture(IInputElement? control) { _pointer.Capture(control); @@ -90,39 +72,6 @@ namespace Avalonia.Input ProcessRawEvent(margs); } - public void TopLevelClosed(IInputRoot root) - { - ClearPointerOver(this, 0, root, PointerPointProperties.None, KeyModifiers.None); - } - - public void SceneInvalidated(IInputRoot root, Rect rect) - { - // Pointer is outside of the target area - if (_position == null ) - { - if (root.PointerOverElement != null) - ClearPointerOver(this, 0, root, PointerPointProperties.None, KeyModifiers.None); - return; - } - - - var clientPoint = root.PointToClient(_position.Value); - - if (rect.Contains(clientPoint)) - { - if (_pointer.Captured == null) - { - SetPointerOver(this, 0 /* TODO: proper timestamp */, root, clientPoint, - PointerPointProperties.None, KeyModifiers.None); - } - else - { - SetPointerOver(this, 0 /* TODO: proper timestamp */, root, _pointer.Captured, - PointerPointProperties.None, KeyModifiers.None); - } - } - } - int ButtonCount(PointerPointProperties props) { var rv = 0; @@ -138,7 +87,7 @@ namespace Avalonia.Input rv++; return rv; } - + private void ProcessRawEvent(RawPointerEventArgs e) { e = e ?? throw new ArgumentNullException(nameof(e)); @@ -147,15 +96,14 @@ namespace Avalonia.Input if(mouse._disposed) return; - if (e.Type == RawPointerEventType.NonClientLeftButtonDown) return; - _position = e.Root.PointToScreen(e.Position); var props = CreateProperties(e); - var keyModifiers = KeyModifiersUtils.ConvertToKey(e.InputModifiers); + var keyModifiers = e.InputModifiers.ToKeyModifiers(); switch (e.Type) { case RawPointerEventType.LeaveWindow: - LeaveWindow(mouse, e.Timestamp, e.Root, props, keyModifiers); + case RawPointerEventType.NonClientLeftButtonDown: + LeaveWindow(); break; case RawPointerEventType.LeftButtonDown: case RawPointerEventType.RightButtonDown: @@ -163,10 +111,9 @@ namespace Avalonia.Input case RawPointerEventType.XButton1Down: case RawPointerEventType.XButton2Down: if (ButtonCount(props) > 1) - e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints); + e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints, e.InputHitTestResult); else - e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position, - props, keyModifiers); + e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.InputHitTestResult); break; case RawPointerEventType.LeftButtonUp: case RawPointerEventType.RightButtonUp: @@ -174,82 +121,50 @@ namespace Avalonia.Input case RawPointerEventType.XButton1Up: case RawPointerEventType.XButton2Up: if (ButtonCount(props) != 0) - e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints); + e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints, e.InputHitTestResult); else - e.Handled = MouseUp(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers); + e.Handled = MouseUp(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.InputHitTestResult); break; case RawPointerEventType.Move: - e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints); + e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints, e.InputHitTestResult); break; case RawPointerEventType.Wheel: - e.Handled = MouseWheel(mouse, e.Timestamp, e.Root, e.Position, props, ((RawMouseWheelEventArgs)e).Delta, keyModifiers); + e.Handled = MouseWheel(mouse, e.Timestamp, e.Root, e.Position, props, ((RawMouseWheelEventArgs)e).Delta, keyModifiers, e.InputHitTestResult); break; case RawPointerEventType.Magnify: - e.Handled = GestureMagnify(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers); + e.Handled = GestureMagnify(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers, e.InputHitTestResult); break; case RawPointerEventType.Rotate: - e.Handled = GestureRotate(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers); + e.Handled = GestureRotate(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers, e.InputHitTestResult); break; case RawPointerEventType.Swipe: - e.Handled = GestureSwipe(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers); + e.Handled = GestureSwipe(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers, e.InputHitTestResult); break; } } - private void LeaveWindow(IMouseDevice device, ulong timestamp, IInputRoot root, PointerPointProperties properties, - KeyModifiers inputModifiers) + private void LeaveWindow() { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - _position = null; - ClearPointerOver(this, timestamp, root, properties, inputModifiers); } - PointerPointProperties CreateProperties(RawPointerEventArgs args) { - - var kind = PointerUpdateKind.Other; - - if (args.Type == RawPointerEventType.LeftButtonDown) - kind = PointerUpdateKind.LeftButtonPressed; - if (args.Type == RawPointerEventType.MiddleButtonDown) - kind = PointerUpdateKind.MiddleButtonPressed; - if (args.Type == RawPointerEventType.RightButtonDown) - kind = PointerUpdateKind.RightButtonPressed; - if (args.Type == RawPointerEventType.XButton1Down) - kind = PointerUpdateKind.XButton1Pressed; - if (args.Type == RawPointerEventType.XButton2Down) - kind = PointerUpdateKind.XButton2Pressed; - if (args.Type == RawPointerEventType.LeftButtonUp) - kind = PointerUpdateKind.LeftButtonReleased; - if (args.Type == RawPointerEventType.MiddleButtonUp) - kind = PointerUpdateKind.MiddleButtonReleased; - if (args.Type == RawPointerEventType.RightButtonUp) - kind = PointerUpdateKind.RightButtonReleased; - if (args.Type == RawPointerEventType.XButton1Up) - kind = PointerUpdateKind.XButton1Released; - if (args.Type == RawPointerEventType.XButton2Up) - kind = PointerUpdateKind.XButton2Released; - - return new PointerPointProperties(args.InputModifiers, kind); + return new PointerPointProperties(args.InputModifiers, args.Type.ToUpdateKind()); } - private MouseButton _lastMouseDownButton; private bool MouseDown(IMouseDevice device, ulong timestamp, IInputElement root, Point p, PointerPointProperties properties, - KeyModifiers inputModifiers) + KeyModifiers inputModifiers, IInputElement? hitTest) { device = device ?? throw new ArgumentNullException(nameof(device)); root = root ?? throw new ArgumentNullException(nameof(root)); - var hit = HitTest(root, p); + var source = _pointer.Captured ?? root.InputHitTest(p); - if (hit != null) + if (source != null) { - _pointer.Capture(hit); - var source = GetSource(hit); + _pointer.Capture(source); if (source != null) { var settings = AvaloniaLocator.Current.GetService(); @@ -275,23 +190,14 @@ namespace Avalonia.Input return false; } - private bool MouseMove(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties properties, - KeyModifiers inputModifiers, Lazy?>? intermediatePoints) + private bool MouseMove(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, + PointerPointProperties properties, KeyModifiers inputModifiers, Lazy?>? intermediatePoints, + IInputElement? hitTest) { device = device ?? throw new ArgumentNullException(nameof(device)); root = root ?? throw new ArgumentNullException(nameof(root)); - IInputElement? source; - - if (_pointer.Captured == null) - { - source = SetPointerOver(this, timestamp, root, p, properties, inputModifiers); - } - else - { - SetPointerOver(this, timestamp, root, _pointer.Captured, properties, inputModifiers); - source = _pointer.Captured; - } + var source = _pointer.Captured ?? hitTest; if (source is object) { @@ -306,13 +212,12 @@ namespace Avalonia.Input } private bool MouseUp(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties props, - KeyModifiers inputModifiers) + KeyModifiers inputModifiers, IInputElement? hitTest) { device = device ?? throw new ArgumentNullException(nameof(device)); root = root ?? throw new ArgumentNullException(nameof(root)); - var hit = HitTest(root, p); - var source = GetSource(hit); + var source = _pointer.Captured ?? hitTest; if (source is not null) { @@ -329,13 +234,12 @@ namespace Avalonia.Input private bool MouseWheel(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties props, - Vector delta, KeyModifiers inputModifiers) + Vector delta, KeyModifiers inputModifiers, IInputElement? hitTest) { device = device ?? throw new ArgumentNullException(nameof(device)); root = root ?? throw new ArgumentNullException(nameof(root)); - var hit = HitTest(root, p); - var source = GetSource(hit); + var source = _pointer.Captured ?? hitTest; // KeyModifiers.Shift should scroll in horizontal direction. This does not work on every platform. // If Shift-Key is pressed and X is close to 0 we swap the Vector. @@ -356,16 +260,15 @@ namespace Avalonia.Input } private bool GestureMagnify(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, - PointerPointProperties props, Vector delta, KeyModifiers inputModifiers) + PointerPointProperties props, Vector delta, KeyModifiers inputModifiers, IInputElement? hitTest) { device = device ?? throw new ArgumentNullException(nameof(device)); root = root ?? throw new ArgumentNullException(nameof(root)); - var hit = HitTest(root, p); + var source = _pointer.Captured ?? hitTest; - if (hit != null) + if (source != null) { - var source = GetSource(hit); var e = new PointerDeltaEventArgs(Gestures.PointerTouchPadGestureMagnifyEvent, source, _pointer, root, p, timestamp, props, inputModifiers, delta); @@ -377,16 +280,15 @@ namespace Avalonia.Input } private bool GestureRotate(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, - PointerPointProperties props, Vector delta, KeyModifiers inputModifiers) + PointerPointProperties props, Vector delta, KeyModifiers inputModifiers, IInputElement? hitTest) { device = device ?? throw new ArgumentNullException(nameof(device)); root = root ?? throw new ArgumentNullException(nameof(root)); - var hit = HitTest(root, p); + var source = _pointer.Captured ?? hitTest; - if (hit != null) + if (source != null) { - var source = GetSource(hit); var e = new PointerDeltaEventArgs(Gestures.PointerTouchPadGestureRotateEvent, source, _pointer, root, p, timestamp, props, inputModifiers, delta); @@ -398,16 +300,15 @@ namespace Avalonia.Input } private bool GestureSwipe(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, - PointerPointProperties props, Vector delta, KeyModifiers inputModifiers) + PointerPointProperties props, Vector delta, KeyModifiers inputModifiers, IInputElement? hitTest) { device = device ?? throw new ArgumentNullException(nameof(device)); root = root ?? throw new ArgumentNullException(nameof(root)); - var hit = HitTest(root, p); + var source = _pointer.Captured ?? hitTest; - if (hit != null) + if (source != null) { - var source = GetSource(hit); var e = new PointerDeltaEventArgs(Gestures.PointerTouchPadGestureSwipeEvent, source, _pointer, root, p, timestamp, props, inputModifiers, delta); @@ -418,154 +319,27 @@ namespace Avalonia.Input return false; } - private IInteractive? GetSource(IVisual? hit) - { - if (hit is null) - return null; - - return _pointer.Captured ?? - (hit as IInteractive) ?? - hit.GetSelfAndVisualAncestors().OfType().FirstOrDefault(); - } - - private IInputElement? HitTest(IInputElement root, Point p) - { - root = root ?? throw new ArgumentNullException(nameof(root)); - - return _pointer.Captured ?? root.InputHitTest(p); - } - - PointerEventArgs CreateSimpleEvent(RoutedEvent ev, ulong timestamp, IInteractive? source, - PointerPointProperties properties, - KeyModifiers inputModifiers) - { - return new PointerEventArgs(ev, source, _pointer, null, default, - timestamp, properties, inputModifiers); - } - - private void ClearPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, - PointerPointProperties properties, - KeyModifiers inputModifiers) - { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - var element = root.PointerOverElement; - var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, element, properties, inputModifiers); - - if (element!=null && !element.IsAttachedToVisualTree) - { - // element has been removed from visual tree so do top down cleanup - if (root.IsPointerOver) - ClearChildrenPointerOver(e, root,true); - } - while (element != null) - { - e.Source = element; - e.Handled = false; - element.RaiseEvent(e); - element = (IInputElement?)element.VisualParent; - } - - root.PointerOverElement = null; - } - - private void ClearChildrenPointerOver(PointerEventArgs e, IInputElement element,bool clearRoot) + public void Dispose() { - foreach (IInputElement el in element.VisualChildren) - { - if (el.IsPointerOver) - { - ClearChildrenPointerOver(e, el, true); - break; - } - } - if(clearRoot) - { - e.Source = element; - e.Handled = false; - element.RaiseEvent(e); - } + _disposed = true; + _pointer?.Dispose(); } - private IInputElement? SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, Point p, - PointerPointProperties properties, - KeyModifiers inputModifiers) + [Obsolete] + public void TopLevelClosed(IInputRoot root) { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - - var element = root.InputHitTest(p); - - if (element != root.PointerOverElement) - { - if (element != null) - { - SetPointerOver(device, timestamp, root, element, properties, inputModifiers); - } - else - { - ClearPointerOver(device, timestamp, root, properties, inputModifiers); - } - } - - return element; + // no-op } - private void SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, IInputElement element, - PointerPointProperties properties, - KeyModifiers inputModifiers) + [Obsolete] + public void SceneInvalidated(IInputRoot root, Rect rect) { - device = device ?? throw new ArgumentNullException(nameof(device)); - root = root ?? throw new ArgumentNullException(nameof(root)); - element = element ?? throw new ArgumentNullException(nameof(element)); - - IInputElement? branch = null; - - IInputElement? el = element; - - while (el != null) - { - if (el.IsPointerOver) - { - branch = el; - break; - } - el = (IInputElement?)el.VisualParent; - } - - el = root.PointerOverElement; - - var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, el, properties, inputModifiers); - if (el!=null && branch!=null && !el.IsAttachedToVisualTree) - { - ClearChildrenPointerOver(e,branch,false); - } - - while (el != null && el != branch) - { - e.Source = el; - e.Handled = false; - el.RaiseEvent(e); - el = (IInputElement?)el.VisualParent; - } - - el = root.PointerOverElement = element; - e.RoutedEvent = InputElement.PointerEnterEvent; - - while (el != null && el != branch) - { - e.Source = el; - e.Handled = false; - el.RaiseEvent(e); - el = (IInputElement?)el.VisualParent; - } + // no-op } - public void Dispose() + public IPointer? TryGetPointer(RawPointerEventArgs ev) { - _disposed = true; - _pointer?.Dispose(); + return _pointer; } } } diff --git a/src/Avalonia.Input/PointerEventArgs.cs b/src/Avalonia.Input/PointerEventArgs.cs index 0604d09dc4..5495802920 100644 --- a/src/Avalonia.Input/PointerEventArgs.cs +++ b/src/Avalonia.Input/PointerEventArgs.cs @@ -63,6 +63,8 @@ namespace Avalonia.Input } public Point GetPosition(IVisual relativeTo) => _ev.GetPosition(relativeTo); + + public IPointer? TryGetPointer(RawPointerEventArgs ev) => _ev.Pointer; } public IPointer Pointer { get; } diff --git a/src/Avalonia.Input/PointerOverPreProcessor.cs b/src/Avalonia.Input/PointerOverPreProcessor.cs new file mode 100644 index 0000000000..a38364197d --- /dev/null +++ b/src/Avalonia.Input/PointerOverPreProcessor.cs @@ -0,0 +1,210 @@ +using System; +using Avalonia.Input.Raw; + +namespace Avalonia.Input +{ + internal class PointerOverPreProcessor : IObserver + { + private IPointerDevice? _lastActivePointerDevice; + private (IPointer pointer, PixelPoint position)? _lastPointer; + + private readonly IInputRoot _inputRoot; + + public PointerOverPreProcessor(IInputRoot inputRoot) + { + _inputRoot = inputRoot ?? throw new ArgumentNullException(nameof(inputRoot)); + } + + public void OnCompleted() + { + ClearPointerOver(); + } + + public void OnError(Exception error) + { + } + + public void OnNext(RawInputEventArgs value) + { + if (value is RawPointerEventArgs args + && args.Root == _inputRoot + && value.Device is IPointerDevice pointerDevice) + { + if (pointerDevice != _lastActivePointerDevice) + { + ClearPointerOver(); + + // Set last active device before processing input, because ClearPointerOver might be called and clear last device. + _lastActivePointerDevice = pointerDevice; + } + + if (args.Type is RawPointerEventType.LeaveWindow or RawPointerEventType.NonClientLeftButtonDown + && _lastPointer is (var lastPointer, var lastPosition)) + { + _lastPointer = null; + ClearPointerOver(lastPointer, args.Root, 0, args.Root.PointToClient(lastPosition), + new PointerPointProperties(args.InputModifiers, args.Type.ToUpdateKind()), + args.InputModifiers.ToKeyModifiers()); + } + else if (pointerDevice.TryGetPointer(args) is IPointer pointer + && pointer.Type != PointerType.Touch) + { + var element = pointer.Captured ?? args.InputHitTestResult; + + SetPointerOver(pointer, args.Root, element, args.Timestamp, args.Position, + new PointerPointProperties(args.InputModifiers, args.Type.ToUpdateKind()), + args.InputModifiers.ToKeyModifiers()); + } + } + } + + public void SceneInvalidated(Rect dirtyRect) + { + // Pointer is outside of the target area + if (_lastPointer is (var pointer, var position)) + { + var clientPoint = _inputRoot.PointToClient(position); + + if (dirtyRect.Contains(clientPoint)) + { + SetPointerOver(pointer, _inputRoot, _inputRoot.InputHitTest(clientPoint), 0, clientPoint, PointerPointProperties.None, KeyModifiers.None); + } + else if (!_inputRoot.Bounds.Contains(clientPoint)) + { + ClearPointerOver(pointer, _inputRoot, 0, new Point(-1, -1), PointerPointProperties.None, KeyModifiers.None); + } + } + } + + private void ClearPointerOver() + { + if (_lastPointer is (var pointer, var _)) + { + ClearPointerOver(pointer, _inputRoot, 0, new Point(-1, -1), PointerPointProperties.None, KeyModifiers.None); + } + _lastPointer = null; + _lastActivePointerDevice = null; + } + + private void ClearPointerOver(IPointer pointer, IInputRoot root, + ulong timestamp, Point position, PointerPointProperties properties, KeyModifiers inputModifiers) + { + var element = root.PointerOverElement; + if (element is null) + { + return; + } + + // Do not pass rootVisual, when we have unknown (negative) position, + // so GetPosition won't return invalid values. + var hasPosition = position.X >= 0 && position.Y >= 0; + var e = new PointerEventArgs(InputElement.PointerLeaveEvent, element, pointer, + hasPosition ? root : null, hasPosition ? position : default, + timestamp, properties, inputModifiers); + + if (element != null && !element.IsAttachedToVisualTree) + { + // element has been removed from visual tree so do top down cleanup + if (root.IsPointerOver) + { + ClearChildrenPointerOver(e, root, true); + } + } + while (element != null) + { + e.Source = element; + e.Handled = false; + element.RaiseEvent(e); + element = (IInputElement?)element.VisualParent; + } + + root.PointerOverElement = null; + _lastActivePointerDevice = null; + _lastPointer = null; + } + + private void ClearChildrenPointerOver(PointerEventArgs e, IInputElement element, bool clearRoot) + { + foreach (IInputElement el in element.VisualChildren) + { + if (el.IsPointerOver) + { + ClearChildrenPointerOver(e, el, true); + break; + } + } + if (clearRoot) + { + e.Source = element; + e.Handled = false; + element.RaiseEvent(e); + } + } + + private void SetPointerOver(IPointer pointer, IInputRoot root, IInputElement? element, + ulong timestamp, Point position, PointerPointProperties properties, KeyModifiers inputModifiers) + { + var pointerOverElement = root.PointerOverElement; + + if (element != pointerOverElement) + { + if (element != null) + { + SetPointerOverToElement(pointer, root, element, timestamp, position, properties, inputModifiers); + } + else + { + ClearPointerOver(pointer, root, timestamp, position, properties, inputModifiers); + } + } + } + + private void SetPointerOverToElement(IPointer pointer, IInputRoot root, IInputElement element, + ulong timestamp, Point position, PointerPointProperties properties, KeyModifiers inputModifiers) + { + IInputElement? branch = null; + + IInputElement? el = element; + + while (el != null) + { + if (el.IsPointerOver) + { + branch = el; + break; + } + el = (IInputElement?)el.VisualParent; + } + + el = root.PointerOverElement; + + var e = new PointerEventArgs(InputElement.PointerLeaveEvent, el, pointer, root, position, + timestamp, properties, inputModifiers); + if (el != null && branch != null && !el.IsAttachedToVisualTree) + { + ClearChildrenPointerOver(e, branch, false); + } + + while (el != null && el != branch) + { + e.Source = el; + e.Handled = false; + el.RaiseEvent(e); + el = (IInputElement?)el.VisualParent; + } + + el = root.PointerOverElement = element; + _lastPointer = (pointer, root.PointToScreen(position)); + + e.RoutedEvent = InputElement.PointerEnterEvent; + + while (el != null && el != branch) + { + e.Source = el; + e.Handled = false; + el.RaiseEvent(e); + el = (IInputElement?)el.VisualParent; + } + } + } +} diff --git a/src/Avalonia.Input/Raw/RawDragEvent.cs b/src/Avalonia.Input/Raw/RawDragEvent.cs index 6e9ce20ff1..652bad7115 100644 --- a/src/Avalonia.Input/Raw/RawDragEvent.cs +++ b/src/Avalonia.Input/Raw/RawDragEvent.cs @@ -20,7 +20,7 @@ namespace Avalonia.Input.Raw Location = location; Data = data; Effects = effects; - KeyModifiers = KeyModifiersUtils.ConvertToKey(modifiers); + KeyModifiers = modifiers.ToKeyModifiers(); #pragma warning disable CS0618 // Type or member is obsolete Modifiers = (InputModifiers)modifiers; #pragma warning restore CS0618 // Type or member is obsolete diff --git a/src/Avalonia.Input/Raw/RawInputHelpers.cs b/src/Avalonia.Input/Raw/RawInputHelpers.cs new file mode 100644 index 0000000000..9d329bae59 --- /dev/null +++ b/src/Avalonia.Input/Raw/RawInputHelpers.cs @@ -0,0 +1,27 @@ +using Avalonia.Input.Raw; + +namespace Avalonia.Input +{ + internal static class RawInputHelpers + { + public static KeyModifiers ToKeyModifiers(this RawInputModifiers modifiers) => + (KeyModifiers)(modifiers & RawInputModifiers.KeyboardMask); + + public static PointerUpdateKind ToUpdateKind(this RawPointerEventType type) => type switch + { + RawPointerEventType.LeftButtonDown => PointerUpdateKind.LeftButtonPressed, + RawPointerEventType.LeftButtonUp => PointerUpdateKind.LeftButtonReleased, + RawPointerEventType.RightButtonDown => PointerUpdateKind.RightButtonPressed, + RawPointerEventType.RightButtonUp => PointerUpdateKind.RightButtonReleased, + RawPointerEventType.MiddleButtonDown => PointerUpdateKind.MiddleButtonPressed, + RawPointerEventType.MiddleButtonUp => PointerUpdateKind.MiddleButtonReleased, + RawPointerEventType.XButton1Down => PointerUpdateKind.XButton1Pressed, + RawPointerEventType.XButton1Up => PointerUpdateKind.XButton1Released, + RawPointerEventType.XButton2Down => PointerUpdateKind.XButton2Pressed, + RawPointerEventType.XButton2Up => PointerUpdateKind.XButton2Released, + RawPointerEventType.TouchBegin => PointerUpdateKind.LeftButtonPressed, + RawPointerEventType.TouchEnd => PointerUpdateKind.LeftButtonReleased, + _ => PointerUpdateKind.Other + }; + } +} diff --git a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs index c157fa059c..8b9d7c161d 100644 --- a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs @@ -120,6 +120,8 @@ namespace Avalonia.Input.Raw /// only valid for Move and TouchUpdate /// public Lazy?>? IntermediatePoints { get; set; } + + internal IInputElement? InputHitTestResult { get; set; } } public struct RawPointerPoint diff --git a/src/Avalonia.Input/TouchDevice.cs b/src/Avalonia.Input/TouchDevice.cs index 20cafb9e8e..54dcc4051e 100644 --- a/src/Avalonia.Input/TouchDevice.cs +++ b/src/Avalonia.Input/TouchDevice.cs @@ -3,24 +3,26 @@ using System.Collections.Generic; using System.Linq; using Avalonia.Input.Raw; using Avalonia.Platform; +using Avalonia.VisualTree; namespace Avalonia.Input { /// /// Handles raw touch events + /// /// /// This class is supposed to be used on per-toplevel basis, don't use a shared one /// - /// - public class TouchDevice : IInputDevice, IDisposable + public class TouchDevice : IPointerDevice, IDisposable { private readonly Dictionary _pointers = new Dictionary(); private bool _disposed; private int _clickCount; private Rect _lastClickRect; private ulong _lastClickTime; - KeyModifiers GetKeyModifiers(RawInputModifiers modifiers) => - (KeyModifiers)(modifiers & RawInputModifiers.KeyboardMask); + private Pointer? _lastPointer; + + IInputElement? IPointerDevice.Captured => _lastPointer?.Captured; RawInputModifiers GetModifiers(RawInputModifiers modifiers, bool isLeftButtonDown) { @@ -30,6 +32,10 @@ namespace Avalonia.Input return rv; } + void IPointerDevice.Capture(IInputElement? control) => _lastPointer?.Capture(control); + + Point IPointerDevice.GetPosition(IVisual relativeTo) => default; + public void ProcessRawEvent(RawInputEventArgs ev) { if (ev.Handled || _disposed) @@ -39,15 +45,18 @@ namespace Avalonia.Input { if (args.Type == RawPointerEventType.TouchEnd) return; - var hit = args.Root.InputHitTest(args.Position); + var hit = args.InputHitTestResult; _pointers[args.TouchPointId] = pointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Touch, _pointers.Count == 0); pointer.Capture(hit); } - + _lastPointer = pointer; var target = pointer.Captured ?? args.Root; + var updateKind = args.Type.ToUpdateKind(); + var keyModifier = args.InputModifiers.ToKeyModifiers(); + if (args.Type == RawPointerEventType.TouchBegin) { if (_pointers.Count > 1) @@ -73,9 +82,8 @@ namespace Avalonia.Input target.RaiseEvent(new PointerPressedEventArgs(target, pointer, args.Root, args.Position, ev.Timestamp, - new PointerPointProperties(GetModifiers(args.InputModifiers, true), - PointerUpdateKind.LeftButtonPressed), - GetKeyModifiers(args.InputModifiers), _clickCount)); + new PointerPointProperties(GetModifiers(args.InputModifiers, true), updateKind), + keyModifier, _clickCount)); } if (args.Type == RawPointerEventType.TouchEnd) @@ -85,10 +93,10 @@ namespace Avalonia.Input { target.RaiseEvent(new PointerReleasedEventArgs(target, pointer, args.Root, args.Position, ev.Timestamp, - new PointerPointProperties(GetModifiers(args.InputModifiers, false), - PointerUpdateKind.LeftButtonReleased), - GetKeyModifiers(args.InputModifiers), MouseButton.Left)); + new PointerPointProperties(GetModifiers(args.InputModifiers, false), updateKind), + keyModifier, MouseButton.Left)); } + _lastPointer = null; } if (args.Type == RawPointerEventType.TouchCancel) @@ -96,18 +104,16 @@ namespace Avalonia.Input _pointers.Remove(args.TouchPointId); using (pointer) pointer.Capture(null); + _lastPointer = null; } if (args.Type == RawPointerEventType.TouchUpdate) { - var modifiers = GetModifiers(args.InputModifiers, pointer.IsPrimary); target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, target, pointer, args.Root, args.Position, ev.Timestamp, - new PointerPointProperties(GetModifiers(args.InputModifiers, true), PointerUpdateKind.Other), - GetKeyModifiers(args.InputModifiers), args.IntermediatePoints)); + new PointerPointProperties(GetModifiers(args.InputModifiers, true), updateKind), + keyModifier, args.IntermediatePoints)); } - - } public void Dispose() @@ -121,5 +127,12 @@ namespace Avalonia.Input p.Dispose(); } + public IPointer? TryGetPointer(RawPointerEventArgs ev) + { + return ev is RawTouchEventArgs args + && _pointers.TryGetValue(args.TouchPointId, out var pointer) + ? pointer + : null; + } } } From 8fba608156ecc275f90a44d6b7af1aaa2a7e15f5 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 14 Apr 2022 11:47:35 -0400 Subject: [PATCH 07/38] Add more tests for pointerover --- .../TopLevelTests.cs | 21 +- .../MouseDeviceTests.cs | 177 ------ .../PointerOverTests.cs | 533 ++++++++++++++++++ 3 files changed, 536 insertions(+), 195 deletions(-) create mode 100644 tests/Avalonia.Input.UnitTests/PointerOverTests.cs diff --git a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs index 9c2d760733..db6349cc5a 100644 --- a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs @@ -193,6 +193,9 @@ namespace Avalonia.Controls.UnitTests public void Impl_Input_Should_Pass_Input_To_InputManager() { var inputManagerMock = new Mock(); + inputManagerMock.DefaultValue = DefaultValue.Mock; + inputManagerMock.SetupAllProperties(); + var services = TestServices.StyledWindow.With(inputManager: inputManagerMock.Object); using (UnitTestApplication.Start(services)) @@ -249,24 +252,6 @@ namespace Avalonia.Controls.UnitTests } } - [Fact] - public void Close_Should_Notify_MouseDevice() - { - using (UnitTestApplication.Start(TestServices.StyledWindow)) - { - var impl = new Mock(); - var mouseDevice = new Mock(); - impl.SetupAllProperties(); - impl.Setup(x => x.MouseDevice).Returns(mouseDevice.Object); - - var target = new TestTopLevel(impl.Object); - - impl.Object.Closed(); - - mouseDevice.Verify(x => x.TopLevelClosed(target)); - } - } - [Fact] public void Close_Should_Dispose_LayoutManager() { diff --git a/tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs b/tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs index 223f458f25..e8f8244d83 100644 --- a/tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs +++ b/tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs @@ -1,13 +1,10 @@ using Avalonia.Controls; using Avalonia.Input.Raw; -using Avalonia.Interactivity; using Avalonia.Media; using Avalonia.Rendering; using Avalonia.UnitTests; -using Avalonia.VisualTree; using Moq; using System; -using System.Collections.Generic; using Xunit; namespace Avalonia.Input.UnitTests @@ -34,160 +31,6 @@ namespace Avalonia.Input.UnitTests } #pragma warning restore CS0618 // Type or member is obsolete - [Fact] - public void MouseMove_Should_Update_IsPointerOver() - { - var renderer = new Mock(); - - using (TestApplication(renderer.Object)) - { - var inputManager = InputManager.Instance; - - Canvas canvas; - Border border; - Decorator decorator; - - var root = new TestRoot - { - MouseDevice = new MouseDevice(), - Renderer = renderer.Object, - Child = new Panel - { - Children = - { - (canvas = new Canvas()), - (border = new Border - { - Child = decorator = new Decorator(), - }) - } - } - }; - - SetHit(renderer, decorator); - SendMouseMove(inputManager, root); - - Assert.True(decorator.IsPointerOver); - Assert.True(border.IsPointerOver); - Assert.False(canvas.IsPointerOver); - Assert.True(root.IsPointerOver); - - SetHit(renderer, canvas); - SendMouseMove(inputManager, root); - - Assert.False(decorator.IsPointerOver); - Assert.False(border.IsPointerOver); - Assert.True(canvas.IsPointerOver); - Assert.True(root.IsPointerOver); - } - } - - [Fact] - public void IsPointerOver_Should_Be_Updated_When_Child_Sets_Handled_True() - { - var renderer = new Mock(); - - using (TestApplication(renderer.Object)) - { - var inputManager = InputManager.Instance; - - Canvas canvas; - Border border; - Decorator decorator; - - var root = new TestRoot - { - MouseDevice = new MouseDevice(), - Renderer = renderer.Object, - Child = new Panel - { - Children = - { - (canvas = new Canvas()), - (border = new Border - { - Child = decorator = new Decorator(), - }) - } - } - }; - - SetHit(renderer, canvas); - SendMouseMove(inputManager, root); - - Assert.False(decorator.IsPointerOver); - Assert.False(border.IsPointerOver); - Assert.True(canvas.IsPointerOver); - Assert.True(root.IsPointerOver); - - // Ensure that e.Handled is reset between controls. - decorator.PointerEnter += (s, e) => e.Handled = true; - - SetHit(renderer, decorator); - SendMouseMove(inputManager, root); - - Assert.True(decorator.IsPointerOver); - Assert.True(border.IsPointerOver); - Assert.False(canvas.IsPointerOver); - Assert.True(root.IsPointerOver); - } - } - - [Fact] - public void PointerEnter_Leave_Should_Be_Raised_In_Correct_Order() - { - var renderer = new Mock(); - var result = new List<(object, string)>(); - - void HandleEvent(object sender, PointerEventArgs e) - { - result.Add((sender, e.RoutedEvent.Name)); - } - - using (TestApplication(renderer.Object)) - { - var inputManager = InputManager.Instance; - - Canvas canvas; - Border border; - Decorator decorator; - - var root = new TestRoot - { - MouseDevice = new MouseDevice(), - Renderer = renderer.Object, - Child = new Panel - { - Children = - { - (canvas = new Canvas()), - (border = new Border - { - Child = decorator = new Decorator(), - }) - } - } - }; - - SetHit(renderer, canvas); - SendMouseMove(inputManager, root); - - AddEnterLeaveHandlers(HandleEvent, root, canvas, border, decorator); - SetHit(renderer, decorator); - SendMouseMove(inputManager, root); - - Assert.Equal( - new[] - { - ((object)canvas, "PointerLeave"), - ((object)decorator, "PointerEnter"), - ((object)border, "PointerEnter"), - }, - result); - } - } - - [Fact] public void GetPosition_Should_Respect_Control_RenderTransform() { @@ -216,17 +59,6 @@ namespace Avalonia.Input.UnitTests } } - private void AddEnterLeaveHandlers( - EventHandler handler, - params IControl[] controls) - { - foreach (var c in controls) - { - c.PointerEnter += handler; - c.PointerLeave += handler; - } - } - private void SendMouseMove(IInputManager inputManager, TestRoot root, Point p = new Point()) { inputManager.ProcessInput(new RawPointerEventArgs( @@ -238,15 +70,6 @@ namespace Avalonia.Input.UnitTests RawInputModifiers.None)); } - private void SetHit(Mock renderer, IControl hit) - { - renderer.Setup(x => x.HitTest(It.IsAny(), It.IsAny(), It.IsAny>())) - .Returns(new[] { hit }); - - renderer.Setup(x => x.HitTestFirst(It.IsAny(), It.IsAny(), It.IsAny>())) - .Returns(hit); - } - private IDisposable TestApplication(IRenderer renderer) { return UnitTestApplication.Start( diff --git a/tests/Avalonia.Input.UnitTests/PointerOverTests.cs b/tests/Avalonia.Input.UnitTests/PointerOverTests.cs new file mode 100644 index 0000000000..14acf9f946 --- /dev/null +++ b/tests/Avalonia.Input.UnitTests/PointerOverTests.cs @@ -0,0 +1,533 @@ +#nullable enable +using System; +using System.Collections.Generic; + +using Avalonia.Controls; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Templates; +using Avalonia.Input.Raw; +using Avalonia.Platform; +using Avalonia.Rendering; +using Avalonia.UnitTests; +using Avalonia.VisualTree; + +using Moq; + +using Xunit; + +namespace Avalonia.Input.UnitTests +{ + public class PointerOverTests + { + // https://github.com/AvaloniaUI/Avalonia/issues/2821 + [Fact] + public void Close_Should_Remove_PointerOver() + { + using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); + + var renderer = new Mock(); + var device = CreatePointerDeviceMock().Object; + var impl = CreateTopLevelImplMock(renderer.Object); + + Canvas canvas; + var root = CreateInputRoot(impl.Object, new Panel + { + Children = + { + (canvas = new Canvas()) + } + }); + + SetHit(renderer, canvas); + impl.Object.Input!(CreateRawPointerMovedArgs(device, root)); + + Assert.True(canvas.IsPointerOver); + + impl.Object.Closed!(); + + Assert.False(canvas.IsPointerOver); + } + + [Fact] + public void MouseMove_Should_Update_IsPointerOver() + { + using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); + + var renderer = new Mock(); + var device = CreatePointerDeviceMock().Object; + var impl = CreateTopLevelImplMock(renderer.Object); + + Canvas canvas; + Border border; + Decorator decorator; + + var root = CreateInputRoot(impl.Object, new Panel + { + Children = + { + (canvas = new Canvas()), + (border = new Border + { + Child = decorator = new Decorator(), + }) + } + }); + + SetHit(renderer, decorator); + impl.Object.Input!(CreateRawPointerMovedArgs(device, root)); + + Assert.True(decorator.IsPointerOver); + Assert.True(border.IsPointerOver); + Assert.False(canvas.IsPointerOver); + Assert.True(root.IsPointerOver); + + SetHit(renderer, canvas); + impl.Object.Input!(CreateRawPointerMovedArgs(device, root)); + + Assert.False(decorator.IsPointerOver); + Assert.False(border.IsPointerOver); + Assert.True(canvas.IsPointerOver); + Assert.True(root.IsPointerOver); + } + + + [Fact] + public void TouchMove_Should_Not_Set_IsPointerOver() + { + using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); + + var renderer = new Mock(); + var device = CreatePointerDeviceMock(pointerType: PointerType.Touch).Object; + var impl = CreateTopLevelImplMock(renderer.Object); + + Canvas canvas; + + var root = CreateInputRoot(impl.Object, new Panel + { + Children = + { + (canvas = new Canvas()) + } + }); + + SetHit(renderer, canvas); + impl.Object.Input!(CreateRawPointerMovedArgs(device, root)); + + Assert.False(canvas.IsPointerOver); + Assert.False(root.IsPointerOver); + } + + [Fact] + public void HitTest_Should_Be_Ignored_If_Element_Captured() + { + using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); + + var renderer = new Mock(); + var pointer = new Mock(); + var device = CreatePointerDeviceMock(pointer.Object).Object; + var impl = CreateTopLevelImplMock(renderer.Object); + + Canvas canvas; + Border border; + Decorator decorator; + + var root = CreateInputRoot(impl.Object, new Panel + { + Children = + { + (canvas = new Canvas()), + (border = new Border + { + Child = decorator = new Decorator(), + }) + } + }); + + SetHit(renderer, canvas); + pointer.SetupGet(p => p.Captured).Returns(decorator); + impl.Object.Input!(CreateRawPointerMovedArgs(device, root)); + + Assert.True(decorator.IsPointerOver); + Assert.True(border.IsPointerOver); + Assert.False(canvas.IsPointerOver); + Assert.True(root.IsPointerOver); + } + + [Fact] + public void IsPointerOver_Should_Be_Updated_When_Child_Sets_Handled_True() + { + using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); + + var renderer = new Mock(); + var device = CreatePointerDeviceMock().Object; + var impl = CreateTopLevelImplMock(renderer.Object); + + Canvas canvas; + Border border; + Decorator decorator; + + var root = CreateInputRoot(impl.Object, new Panel + { + Children = + { + (canvas = new Canvas()), + (border = new Border + { + Child = decorator = new Decorator(), + }) + } + }); + + SetHit(renderer, canvas); + impl.Object.Input!(CreateRawPointerMovedArgs(device, root)); + + Assert.False(decorator.IsPointerOver); + Assert.False(border.IsPointerOver); + Assert.True(canvas.IsPointerOver); + Assert.True(root.IsPointerOver); + + // Ensure that e.Handled is reset between controls. + root.PointerMoved += (s, e) => e.Handled = true; + decorator.PointerEnter += (s, e) => e.Handled = true; + + SetHit(renderer, decorator); + impl.Object.Input!(CreateRawPointerMovedArgs(device, root)); + + Assert.True(decorator.IsPointerOver); + Assert.True(border.IsPointerOver); + Assert.False(canvas.IsPointerOver); + Assert.True(root.IsPointerOver); + } + + [Fact] + public void Pointer_Enter_Move_Leave_Should_Be_Followed() + { + using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); + + var renderer = new Mock(); + var deviceMock = CreatePointerDeviceMock(); + var impl = CreateTopLevelImplMock(renderer.Object); + var result = new List<(object?, string)>(); + + void HandleEvent(object? sender, PointerEventArgs e) + { + result.Add((sender, e.RoutedEvent!.Name)); + } + + Canvas canvas; + Border border; + Decorator decorator; + + var root = CreateInputRoot(impl.Object, new Panel + { + Children = + { + (canvas = new Canvas()), + (border = new Border + { + Child = decorator = new Decorator(), + }) + } + }); + + AddEnterLeaveHandlers(HandleEvent, canvas, decorator); + + // Enter decorator + SetHit(renderer, decorator); + SetMove(deviceMock, root, decorator); + impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root)); + + // Leave decorator + SetHit(renderer, canvas); + SetMove(deviceMock, root, canvas); + impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root)); + + Assert.Equal( + new[] + { + ((object?)decorator, "PointerEnter"), + (decorator, "PointerMove"), + (decorator, "PointerLeave"), + (canvas, "PointerEnter"), + (canvas, "PointerMove") + }, + result); + } + + [Fact] + public void PointerEnter_Leave_Should_Be_Raised_In_Correct_Order() + { + using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); + + var renderer = new Mock(); + var deviceMock = CreatePointerDeviceMock(); + var impl = CreateTopLevelImplMock(renderer.Object); + var result = new List<(object?, string)>(); + + void HandleEvent(object? sender, PointerEventArgs e) + { + result.Add((sender, e.RoutedEvent!.Name)); + } + + Canvas canvas; + Border border; + Decorator decorator; + + var root = CreateInputRoot(impl.Object, new Panel + { + Children = + { + (canvas = new Canvas()), + (border = new Border + { + Child = decorator = new Decorator(), + }) + } + }); + + SetHit(renderer, canvas); + impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root)); + + AddEnterLeaveHandlers(HandleEvent, root, canvas, border, decorator); + + SetHit(renderer, decorator); + impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root)); + + Assert.Equal( + new[] + { + ((object?)canvas, "PointerLeave"), + (decorator, "PointerEnter"), + (border, "PointerEnter"), + }, + result); + } + + // https://github.com/AvaloniaUI/Avalonia/issues/7896 + [Fact] + public void PointerEnter_Leave_Should_Set_Correct_Position() + { + using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); + + var expectedPosition = new Point(15, 15); + var renderer = new Mock(); + var deviceMock = CreatePointerDeviceMock(); + var impl = CreateTopLevelImplMock(renderer.Object); + var result = new List<(object?, string, Point)>(); + + void HandleEvent(object? sender, PointerEventArgs e) + { + result.Add((sender, e.RoutedEvent!.Name, e.GetPosition(null))); + } + + Canvas canvas; + + var root = CreateInputRoot(impl.Object, new Panel + { + Children = + { + (canvas = new Canvas()) + } + }); + + AddEnterLeaveHandlers(HandleEvent, root, canvas); + + SetHit(renderer, canvas); + impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root, expectedPosition)); + + SetHit(renderer, null); + impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root, expectedPosition)); + + Assert.Equal( + new[] + { + ((object?)canvas, "PointerEnter", expectedPosition), + (root, "PointerEnter", expectedPosition), + (canvas, "PointerLeave", expectedPosition), + (root, "PointerLeave", expectedPosition) + }, + result); + } + + [Fact] + public void Render_Invalidation_Should_Affect_PointerOver() + { + using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); + + var renderer = new Mock(); + var deviceMock = CreatePointerDeviceMock(); + var impl = CreateTopLevelImplMock(renderer.Object); + + var invalidateRect = new Rect(0, 0, 15, 15); + + Canvas canvas; + + var root = CreateInputRoot(impl.Object, new Panel + { + Children = + { + (canvas = new Canvas()) + } + }); + + // Let input know about latest device. + SetHit(renderer, canvas); + impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root)); + Assert.True(canvas.IsPointerOver); + + SetHit(renderer, canvas); + renderer.Raise(r => r.SceneInvalidated += null, new SceneInvalidatedEventArgs((IRenderRoot)root, invalidateRect)); + Assert.True(canvas.IsPointerOver); + + // Raise SceneInvalidated again, but now hide element from the hittest. + SetHit(renderer, null); + renderer.Raise(r => r.SceneInvalidated += null, new SceneInvalidatedEventArgs((IRenderRoot)root, invalidateRect)); + Assert.False(canvas.IsPointerOver); + } + + // https://github.com/AvaloniaUI/Avalonia/issues/7748 + [Fact] + public void LeaveWindow_Should_Reset_PointerOver() + { + using var app = UnitTestApplication.Start(new TestServices(inputManager: new InputManager())); + + var renderer = new Mock(); + var deviceMock = CreatePointerDeviceMock(); + var impl = CreateTopLevelImplMock(renderer.Object); + + var lastClientPosition = new Point(1, 5); + var invalidateRect = new Rect(0, 0, 15, 15); + var result = new List<(object?, string, Point)>(); + + void HandleEvent(object? sender, PointerEventArgs e) + { + result.Add((sender, e.RoutedEvent!.Name, e.GetPosition(null))); + } + + Canvas canvas; + + var root = CreateInputRoot(impl.Object, new Panel + { + Children = + { + (canvas = new Canvas()) + } + }); + + AddEnterLeaveHandlers(HandleEvent, root, canvas); + + // Init pointer over. + SetHit(renderer, canvas); + impl.Object.Input!(CreateRawPointerMovedArgs(deviceMock.Object, root, lastClientPosition)); + Assert.True(canvas.IsPointerOver); + + // Send LeaveWindow. + impl.Object.Input!(new RawPointerEventArgs(deviceMock.Object, 0, root, RawPointerEventType.LeaveWindow, new Point(), default)); + Assert.False(canvas.IsPointerOver); + + Assert.Equal( + new[] + { + ((object?)canvas, "PointerEnter", lastClientPosition), + (root, "PointerEnter", lastClientPosition), + (canvas, "PointerLeave", lastClientPosition), + (root, "PointerLeave", lastClientPosition), + }, + result); + } + + private static void AddEnterLeaveHandlers( + EventHandler handler, + params IInputElement[] controls) + { + foreach (var c in controls) + { + c.PointerEnter += handler; + c.PointerLeave += handler; + c.PointerMoved += handler; + } + } + + private static void SetHit(Mock renderer, IControl? hit) + { + renderer.Setup(x => x.HitTest(It.IsAny(), It.IsAny(), It.IsAny>())) + .Returns(hit is null ? Array.Empty() : new[] { hit }); + + renderer.Setup(x => x.HitTestFirst(It.IsAny(), It.IsAny(), It.IsAny>())) + .Returns(hit); + } + + private static void SetMove(Mock deviceMock, IInputRoot root, IInputElement element) + { + deviceMock.Setup(d => d.ProcessRawEvent(It.IsAny())) + .Callback(() => element.RaiseEvent(CreatePointerMovedArgs(root, element))); + } + + private static Mock CreateTopLevelImplMock(IRenderer renderer) + { + var impl = new Mock(); + impl.DefaultValue = DefaultValue.Mock; + impl.SetupAllProperties(); + impl.SetupGet(r => r.RenderScaling).Returns(1); + impl.Setup(r => r.CreateRenderer(It.IsAny())).Returns(renderer); + impl.Setup(r => r.PointToScreen(It.IsAny())).Returns(p => new PixelPoint((int)p.X, (int)p.Y)); + impl.Setup(r => r.PointToClient(It.IsAny())).Returns(p => new Point(p.X, p.Y)); + return impl; + } + + private static IInputRoot CreateInputRoot(IWindowImpl impl, IControl child) + { + var root = new Window(impl) + { + Width = 100, + Height = 100, + Content = child, + Template = new FuncControlTemplate((w, _) => new ContentPresenter + { + Content = w.Content + }) + }; + root.Show(); + return root; + } + + private static IInputRoot CreateInputRoot(IRenderer renderer, IControl child) + { + return CreateInputRoot(CreateTopLevelImplMock(renderer).Object, child); + } + + private static RawPointerEventArgs CreateRawPointerMovedArgs( + IPointerDevice pointerDevice, + IInputRoot root, + Point? positition = null) + { + return new RawPointerEventArgs(pointerDevice, 0, root, RawPointerEventType.Move, + positition ?? default, default); + } + + private static PointerEventArgs CreatePointerMovedArgs( + IInputRoot root, IInputElement? source, Point? positition = null) + { + return new PointerEventArgs(InputElement.PointerMovedEvent, source, new Mock().Object, root, + positition ?? default, default, PointerPointProperties.None, KeyModifiers.None); + } + + private static Mock CreatePointerDeviceMock( + IPointer? pointer = null, + PointerType pointerType = PointerType.Mouse) + { + if (pointer is null) + { + var pointerMock = new Mock(); + pointerMock.SetupGet(p => p.Type).Returns(pointerType); + pointer = pointerMock.Object; + } + + var pointerDevice = new Mock(); + pointerDevice.Setup(d => d.TryGetPointer(It.IsAny())) + .Returns(pointer); + + return pointerDevice; + } + } +} From 45916251515cbe354833366fb91c43decf1541c6 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 14 Apr 2022 11:47:38 -0400 Subject: [PATCH 08/38] Api compat --- src/Avalonia.Input/ApiCompatBaseline.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Input/ApiCompatBaseline.txt b/src/Avalonia.Input/ApiCompatBaseline.txt index 93c4cba5a6..6e68e7aed9 100644 --- a/src/Avalonia.Input/ApiCompatBaseline.txt +++ b/src/Avalonia.Input/ApiCompatBaseline.txt @@ -14,6 +14,7 @@ MembersMustExist : Member 'public void Avalonia.Input.InputElement.add_TextInput MembersMustExist : Member 'public void Avalonia.Input.InputElement.remove_DoubleTapped(System.EventHandler)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Input.InputElement.remove_Tapped(System.EventHandler)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public void Avalonia.Input.InputElement.remove_TextInputOptionsQuery(System.EventHandler)' does not exist in the implementation but it does exist in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Input.IPointer Avalonia.Input.IPointerDevice.TryGetPointer(Avalonia.Input.Raw.RawPointerEventArgs)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Input.TextInput.ITextInputMethodImpl.SetActive(System.Boolean)' is present in the contract but not in the implementation. MembersMustExist : Member 'public void Avalonia.Input.TextInput.ITextInputMethodImpl.SetActive(System.Boolean)' does not exist in the implementation but it does exist in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Input.TextInput.ITextInputMethodImpl.SetClient(Avalonia.Input.TextInput.ITextInputMethodClient)' is present in the implementation but not in the contract. From 3c8cc3610f5559f8b8f2610faf8ca8ab0fdb3cb6 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 14 Apr 2022 11:47:56 -0400 Subject: [PATCH 09/38] Use InputHitTest instead of GetInputElementsAt+Linq in dev tools --- .../Diagnostics/ViewModels/MainViewModel.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs index e08c5bc8dd..d92bbb742b 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs @@ -7,7 +7,6 @@ using Avalonia.Diagnostics.Models; using Avalonia.Input; using Avalonia.Metadata; using Avalonia.Threading; -using System.Reactive.Linq; using System.Linq; namespace Avalonia.Diagnostics.ViewModels @@ -59,8 +58,8 @@ namespace Avalonia.Diagnostics.ViewModels .Subscribe(e => { PointerOverRoot = e.Root; - PointerOverElement = e.Root.GetInputElementsAt(e.Position).FirstOrDefault(); - }); + PointerOverElement = e.Root.InputHitTest(e.Position); + }); #nullable restore } Console = new ConsoleViewModel(UpdateConsoleContext); From 0bb22cff12d804b5a7993ea2e9ef6f0876060b3b Mon Sep 17 00:00:00 2001 From: Steven He Date: Fri, 15 Apr 2022 01:07:11 +0800 Subject: [PATCH 10/38] SubtypesFactory generator --- Avalonia.sln | 29 ++- build/SourceGenerators.props | 6 + src/Avalonia.Base/Animation/Easings/Easing.cs | 35 ++-- src/Avalonia.Base/Avalonia.Base.csproj | 1 + src/Avalonia.Base/Input/KeyGesture.cs | 8 + .../Remote/HtmlTransport/HtmlTransport.cs | 8 + .../Avalonia.SourceGenerator.csproj | 17 ++ .../SubtypesFactoryGenerator.cs | 167 ++++++++++++++++++ src/Shared/SourceGeneratorAttributes.cs | 17 ++ 9 files changed, 265 insertions(+), 23 deletions(-) create mode 100644 build/SourceGenerators.props create mode 100644 src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj create mode 100644 src/Avalonia.SourceGenerator/SubtypesFactoryGenerator.cs create mode 100644 src/Shared/SourceGeneratorAttributes.cs diff --git a/Avalonia.sln b/Avalonia.sln index a216229a5b..39788160b2 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -39,6 +39,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{A689DE ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig src\Shared\ModuleInitializer.cs = src\Shared\ModuleInitializer.cs + src\Shared\RawEventGrouping.cs = src\Shared\RawEventGrouping.cs + src\Shared\SourceGeneratorAttributes.cs = src\Shared\SourceGeneratorAttributes.cs EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI", "src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj", "{6417B24E-49C2-4985-8DB2-3AB9D898EC91}" @@ -209,10 +211,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlSamples", "samples\S EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.PlatformSupport", "src\Avalonia.PlatformSupport\Avalonia.PlatformSupport.csproj", "{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.iOS", "samples\ControlCatalog.iOS\ControlCatalog.iOS.csproj", "{70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.iOS", "samples\ControlCatalog.iOS\ControlCatalog.iOS.csproj", "{70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.PlatformSupport.UnitTests", "tests\Avalonia.PlatformSupport.UnitTests\Avalonia.PlatformSupport.UnitTests.csproj", "{CE910927-CE5A-456F-BC92-E4C757354A5C}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.SourceGenerator", "src\Avalonia.SourceGenerator\Avalonia.SourceGenerator.csproj", "{CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevAnalyzers", "src\tools\DevAnalyzers\DevAnalyzers.csproj", "{2B390431-288C-435C-BB6B-A374033BD8D1}" EndProject Global @@ -1929,6 +1932,30 @@ Global {CE910927-CE5A-456F-BC92-E4C757354A5C}.Release|iPhone.Build.0 = Release|Any CPU {CE910927-CE5A-456F-BC92-E4C757354A5C}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {CE910927-CE5A-456F-BC92-E4C757354A5C}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.AppStore|iPhone.Build.0 = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Debug|iPhone.Build.0 = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Release|Any CPU.Build.0 = Release|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Release|iPhone.ActiveCfg = Release|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Release|iPhone.Build.0 = Release|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {2B390431-288C-435C-BB6B-A374033BD8D1}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU {2B390431-288C-435C-BB6B-A374033BD8D1}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU {2B390431-288C-435C-BB6B-A374033BD8D1}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU diff --git a/build/SourceGenerators.props b/build/SourceGenerators.props new file mode 100644 index 0000000000..f9eab24d08 --- /dev/null +++ b/build/SourceGenerators.props @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/Avalonia.Base/Animation/Easings/Easing.cs b/src/Avalonia.Base/Animation/Easings/Easing.cs index c721772f3e..6dfc4c86b3 100644 --- a/src/Avalonia.Base/Animation/Easings/Easing.cs +++ b/src/Avalonia.Base/Animation/Easings/Easing.cs @@ -1,8 +1,9 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.Linq; +using Avalonia.SourceGenerator; namespace Avalonia.Animation.Easings { @@ -10,14 +11,15 @@ namespace Avalonia.Animation.Easings /// Base class for all Easing classes. /// [TypeConverter(typeof(EasingTypeConverter))] - public abstract class Easing : IEasing + public abstract partial class Easing : IEasing { /// public abstract double Ease(double progress); - static Dictionary? _easingTypes; + private const string Namespace = "Avalonia.Animation.Easings"; - static readonly Type s_thisType = typeof(Easing); + [SubtypesFactory(typeof(Easing), Namespace)] + private static partial bool TryCreateEasingInstance(string type, [NotNullWhen(true)] out Easing? instance); /// /// Parses a Easing type string. @@ -26,33 +28,22 @@ namespace Avalonia.Animation.Easings /// Returns the instance of the parsed type. public static Easing Parse(string e) { +#if NETSTANDARD2_0 + if (e.Contains(",")) +#else if (e.Contains(',')) +#endif { return new SplineEasing(KeySpline.Parse(e, CultureInfo.InvariantCulture)); } - if (_easingTypes == null) + if (TryCreateEasingInstance(e, out var easing)) { - _easingTypes = new Dictionary(); - - // Fetch the built-in easings. - var derivedTypes = typeof(Easing).Assembly.GetTypes() - .Where(p => p.Namespace == s_thisType.Namespace) - .Where(p => p.IsSubclassOf(s_thisType)) - .Select(p => p); - - foreach (var easingType in derivedTypes) - _easingTypes.Add(easingType.Name, easingType); - } - - if (_easingTypes.ContainsKey(e)) - { - var type = _easingTypes[e]; - return (Easing)Activator.CreateInstance(type)!; + return easing; } else { - throw new FormatException($"Easing \"{e}\" was not found in {s_thisType.Namespace} namespace."); + throw new FormatException($"Easing \"{e}\" was not found in {Namespace} namespace."); } } } diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj index 5b79d3cd7b..8e4755b4b7 100644 --- a/src/Avalonia.Base/Avalonia.Base.csproj +++ b/src/Avalonia.Base/Avalonia.Base.csproj @@ -16,4 +16,5 @@ + diff --git a/src/Avalonia.Base/Input/KeyGesture.cs b/src/Avalonia.Base/Input/KeyGesture.cs index 0adbe73263..6f302a3ae7 100644 --- a/src/Avalonia.Base/Input/KeyGesture.cs +++ b/src/Avalonia.Base/Input/KeyGesture.cs @@ -155,7 +155,11 @@ namespace Avalonia.Input if (s_keySynonyms.TryGetValue(key.ToLower(), out Key rv)) return rv; +#if NETSTANDARD2_0 return (Key)Enum.Parse(typeof(Key), key, true); +#else + return Enum.Parse(key, true); +#endif } private static KeyModifiers ParseModifier(ReadOnlySpan modifier) @@ -172,7 +176,11 @@ namespace Avalonia.Input return KeyModifiers.Meta; } +#if NETSTANDARD2_0 return (KeyModifiers)Enum.Parse(typeof(KeyModifiers), modifier.ToString(), true); +#else + return Enum.Parse(modifier.ToString(), true); +#endif } private Key ResolveNumPadOperationKey(Key key) diff --git a/src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs b/src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs index 6b1934ed06..7965e5d0d8 100644 --- a/src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs +++ b/src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs @@ -320,15 +320,23 @@ namespace Avalonia.DesignerSupport.Remote.HtmlTransport ? null : modifiersText .Split(',') +#if NETSTANDARD2_0 .Select(x => (InputProtocol.InputModifiers)Enum.Parse( typeof(InputProtocol.InputModifiers), x, true)) +#else + .Select(x => Enum.Parse(x, true)) +#endif .ToArray(); private static InputProtocol.MouseButton ParseMouseButton(string buttonText) => string.IsNullOrWhiteSpace(buttonText) ? InputProtocol.MouseButton.None +#if NETSTANDARD2_0 : (InputProtocol.MouseButton)Enum.Parse( typeof(InputProtocol.MouseButton), buttonText, true); +#else + : Enum.Parse(buttonText, true); +#endif private static double ParseDouble(string text) => double.Parse(text, NumberStyles.Float, CultureInfo.InvariantCulture); diff --git a/src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj b/src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj new file mode 100644 index 0000000000..97e58f8a64 --- /dev/null +++ b/src/Avalonia.SourceGenerator/Avalonia.SourceGenerator.csproj @@ -0,0 +1,17 @@ + + + + netstandard2.0 + enable + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + diff --git a/src/Avalonia.SourceGenerator/SubtypesFactoryGenerator.cs b/src/Avalonia.SourceGenerator/SubtypesFactoryGenerator.cs new file mode 100644 index 0000000000..7bc240853e --- /dev/null +++ b/src/Avalonia.SourceGenerator/SubtypesFactoryGenerator.cs @@ -0,0 +1,167 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Avalonia.SourceGenerator +{ + internal class GenerateSubtypesSyntaxReceiver : ISyntaxReceiver + { + public List<(MethodDeclarationSyntax, AttributeSyntax)> CandidateMethods { get; } = new(); + public List Types { get; } = new(); + + public void OnVisitSyntaxNode(SyntaxNode syntaxNode) + { + if (syntaxNode is MethodDeclarationSyntax declarationSyntax) + { + foreach (var attribute in declarationSyntax.AttributeLists.SelectMany(i => i.Attributes)) + { + CandidateMethods.Add((declarationSyntax, attribute)); + } + } + + if (syntaxNode is ClassDeclarationSyntax or StructDeclarationSyntax) + { + Types.Add(syntaxNode); + } + } + } + + [Generator] + internal class SubtypesFactoryGenerator : ISourceGenerator + { + private readonly GenerateSubtypesSyntaxReceiver _receiver = new(); + private static readonly string s_attributeName = typeof(SubtypesFactoryAttribute).FullName; + + public void Execute(GeneratorExecutionContext context) + { + var methods = new List<(IMethodSymbol, ITypeSymbol, string)>(); + + foreach (var (method, attribute) in _receiver.CandidateMethods) + { + var semanticModel = context.Compilation.GetSemanticModel(method.SyntaxTree); + var attributeTypeInfo = semanticModel.GetTypeInfo(attribute); + if (attributeTypeInfo.Type is null || + attributeTypeInfo.Type.ToString() != s_attributeName || + attribute.ArgumentList is null) + { + continue; + } + + var arguments = attribute.ArgumentList.Arguments; + if (arguments.Count != 2) + { + continue; + } + + if (arguments[0].Expression is not TypeOfExpressionSyntax typeOfExpr || + arguments[1].Expression is not LiteralExpressionSyntax and not IdentifierNameSyntax) + { + continue; + } + + var type = semanticModel.GetTypeInfo(typeOfExpr.Type); + var ns = semanticModel.GetConstantValue(arguments[1].Expression); + var methodDeclInfo = semanticModel.GetDeclaredSymbol(method); + + if (type.Type is not ITypeSymbol baseType || + ns.HasValue is false || + ns.Value is not string nsValue || + methodDeclInfo is not IMethodSymbol methodSymbol || + methodSymbol.Parameters.Length != 2 || + methodSymbol.Parameters[1].RefKind != RefKind.Out) + { + continue; + } + + methods.Add((methodSymbol, baseType, nsValue)); + } + + var types = new List(); + foreach (var type in _receiver.Types) + { + var semanticModel = context.Compilation.GetSemanticModel(type.SyntaxTree); + var decl = semanticModel.GetDeclaredSymbol(type); + if (decl is ITypeSymbol typeSymbol) + { + types.Add(typeSymbol); + } + } + + GenerateSubTypes(context, methods, types); + } + + private bool IsSubtypeOf(ITypeSymbol type, ITypeSymbol baseType) + { + if (type.BaseType is null) + { + return false; + } + + if (SymbolEqualityComparer.Default.Equals(type.BaseType, baseType)) + { + return true; + } + + return IsSubtypeOf(type.BaseType, baseType); + } + + private void GenerateSubTypes( + GeneratorExecutionContext context, + List<(IMethodSymbol Method, ITypeSymbol BaseType, string Namespace)> methods, + List types) + { + foreach (var (method, baseType, @namespace) in methods) + { + var candidateTypes = types.Where(i => IsSubtypeOf(i, baseType)).Where(i => $"{i.ContainingNamespace}.".StartsWith($"{@namespace}.")).ToArray(); + var type = method.ContainingType; + var isGeneric = type.TypeParameters.Length > 0; + var isClass = type.TypeKind == TypeKind.Class; + + if (method.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax() is not MethodDeclarationSyntax methodDecl) + { + continue; + } + + var parameters = new SeparatedSyntaxList().AddRange(methodDecl.ParameterList.Parameters.Select(i => i.WithAttributeLists(new SyntaxList()))); + + var methodDeclText = methodDecl + .WithAttributeLists(new SyntaxList()) + .WithParameterList(methodDecl.ParameterList.WithParameters(parameters)) + .WithBody(null) + .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.None)) + .WithoutTrivia().ToString(); + + var typeDecl = $"partial {(isClass ? "class" : "struct")} {type.Name}{(isGeneric ? $"<{string.Join(", ", type.TypeParameters)}>" : "")}"; + var source = $@"using System; +using System.Collections.Generic; + +namespace {method.ContainingNamespace} +{{ + {typeDecl} + {{ + {methodDeclText} + {{ + var hasMatch = false; + (hasMatch, {method.Parameters[1].Name}) = {method.Parameters[0].Name} switch + {{ +{string.Join("\n", candidateTypes.Select(i => $" \"{i.Name}\" => (true, ({method.Parameters[1].Type})new {i}()),"))} + _ => (false, default({method.Parameters[1].Type})) + }}; + + return hasMatch; + }} + }} +}}"; + + context.AddSource($"{type}.{method.MetadataName}.gen.cs", source); + } + } + + public void Initialize(GeneratorInitializationContext context) + { + context.RegisterForSyntaxNotifications(() => _receiver); + } + } +} diff --git a/src/Shared/SourceGeneratorAttributes.cs b/src/Shared/SourceGeneratorAttributes.cs new file mode 100644 index 0000000000..fdb5977d23 --- /dev/null +++ b/src/Shared/SourceGeneratorAttributes.cs @@ -0,0 +1,17 @@ +using System; + +namespace Avalonia.SourceGenerator +{ + [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)] + internal sealed class SubtypesFactoryAttribute : Attribute + { + public SubtypesFactoryAttribute(Type baseType, string @namespace) + { + BaseType = baseType; + Namespace = @namespace; + } + + public string Namespace { get; } + public Type BaseType { get; } + } +} From e6044ea33069d994b65aa05aad7c775e7686d8d9 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 14 Apr 2022 23:49:52 +0200 Subject: [PATCH 11/38] Remove Avalonia.AndroidTestApplication. It's no longer needed. --- Avalonia.sln | 21 +--- .../Avalonia.AndroidTestApplication.csproj | 79 ------------- .../MainActivity.cs | 104 ------------------ .../Properties/AndroidManifest.xml | 4 - .../Resources/AboutResources.txt | 50 --------- .../Resources/drawable/Icon.png | Bin 4147 -> 0 bytes .../Resources/values/Strings.xml | 6 - 7 files changed, 1 insertion(+), 263 deletions(-) delete mode 100644 src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj delete mode 100644 src/Android/Avalonia.AndroidTestApplication/MainActivity.cs delete mode 100644 src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml delete mode 100644 src/Android/Avalonia.AndroidTestApplication/Resources/AboutResources.txt delete mode 100644 src/Android/Avalonia.AndroidTestApplication/Resources/drawable/Icon.png delete mode 100644 src/Android/Avalonia.AndroidTestApplication/Resources/values/Strings.xml diff --git a/Avalonia.sln b/Avalonia.sln index a216229a5b..64ed214e1f 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -55,8 +55,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Android", "Android", "{7CF9 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Android", "src\Android\Avalonia.Android\Avalonia.Android.csproj", "{7B92AF71-6287-4693-9DCB-BD5B6E927E23}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.AndroidTestApplication", "src\Android\Avalonia.AndroidTestApplication\Avalonia.AndroidTestApplication.csproj", "{FF69B927-C545-49AE-8E16-3D14D621AA12}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "iOS", "iOS", "{0CB0B92E-6CFF-4240-80A5-CCAFE75D91E1}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.iOS", "src\iOS\Avalonia.iOS\Avalonia.iOS.csproj", "{4488AD85-1495-4809-9AA4-DDFE0A48527E}" @@ -209,7 +207,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlSamples", "samples\S EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.PlatformSupport", "src\Avalonia.PlatformSupport\Avalonia.PlatformSupport.csproj", "{E8A597F0-2AB5-4BDA-A235-41162DAF53CF}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.iOS", "samples\ControlCatalog.iOS\ControlCatalog.iOS.csproj", "{70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.iOS", "samples\ControlCatalog.iOS\ControlCatalog.iOS.csproj", "{70B9F5CC-E2F9-4314-9514-EDE762ACCC4B}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.PlatformSupport.UnitTests", "tests\Avalonia.PlatformSupport.UnitTests\Avalonia.PlatformSupport.UnitTests.csproj", "{CE910927-CE5A-456F-BC92-E4C757354A5C}" EndProject @@ -629,22 +627,6 @@ Global {7B92AF71-6287-4693-9DCB-BD5B6E927E23}.Release|Any CPU.Build.0 = Release|Any CPU {7B92AF71-6287-4693-9DCB-BD5B6E927E23}.Release|iPhone.ActiveCfg = Release|Any CPU {7B92AF71-6287-4693-9DCB-BD5B6E927E23}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.AppStore|Any CPU.ActiveCfg = Release|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.AppStore|iPhone.ActiveCfg = Release|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Debug|Any CPU.Deploy.0 = Debug|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Release|Any CPU.Build.0 = Release|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Release|Any CPU.Deploy.0 = Release|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Release|iPhone.ActiveCfg = Release|Any CPU - {FF69B927-C545-49AE-8E16-3D14D621AA12}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {4488AD85-1495-4809-9AA4-DDFE0A48527E}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU {4488AD85-1495-4809-9AA4-DDFE0A48527E}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU {4488AD85-1495-4809-9AA4-DDFE0A48527E}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU @@ -1970,7 +1952,6 @@ Global {8EF392D5-1416-45AA-9956-7CBBC3229E8A} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {08B3E6B9-1CD5-443C-9F61-6D49D1C5F162} = {9B9E3891-2366-4253-A952-D08BCEB71098} {7B92AF71-6287-4693-9DCB-BD5B6E927E23} = {7CF9789C-F1D3-4D0E-90E5-F1DF67A2753F} - {FF69B927-C545-49AE-8E16-3D14D621AA12} = {7CF9789C-F1D3-4D0E-90E5-F1DF67A2753F} {4488AD85-1495-4809-9AA4-DDFE0A48527E} = {0CB0B92E-6CFF-4240-80A5-CCAFE75D91E1} {E1AA3DBF-9056-4530-9376-18119A7A3FFE} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {88060192-33D5-4932-B0F9-8BD2763E857D} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} diff --git a/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj b/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj deleted file mode 100644 index ee41b9f354..0000000000 --- a/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj +++ /dev/null @@ -1,79 +0,0 @@ - - - net6.0-android - 21 - Exe - enable - com.Avalonia.AndroidTestApplication - 1 - 1.0 - apk - true - portable - - - - - Resources\drawable\Icon.png - - - - - True - True - True - True - - - - - - - True - - - - True - - - - - - - - - {7b92af71-6287-4693-9dcb-bd5b6e927e23} - Avalonia.Android - - - {3e53a01a-b331-47f3-b828-4a5717e77a24} - Avalonia.Markup.Xaml - - - {b09b78d8-9b26-48b0-9149-d64a2f120f3f} - Avalonia.Base - - - {d2221c82-4a25-4583-9b43-d791e3f6820c} - Avalonia.Controls - - - {7062ae20-5dcc-4442-9645-8195bdece63e} - Avalonia.Diagnostics - - - {3e10a5fa-e8da-48b1-ad44-6a5b6cb7750f} - Avalonia.Themes.Default - - - {7d2d3083-71dd-4cc9-8907-39a0d86fb322} - Avalonia.Skia - - - - - - - - - diff --git a/src/Android/Avalonia.AndroidTestApplication/MainActivity.cs b/src/Android/Avalonia.AndroidTestApplication/MainActivity.cs deleted file mode 100644 index 8f4beb2737..0000000000 --- a/src/Android/Avalonia.AndroidTestApplication/MainActivity.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System; -using Android.App; -using Android.Content.PM; -using Avalonia.Android; -using Avalonia.Controls; -using Avalonia.Controls.ApplicationLifetimes; -using Avalonia.Input.TextInput; -using Avalonia.Markup.Xaml; -using Avalonia.Media; -using Avalonia.Styling; -using Avalonia.Themes.Default; - -namespace Avalonia.AndroidTestApplication -{ - [Activity(Label = "Main", - MainLauncher = true, - Icon = "@drawable/icon", - Theme = "@style/Theme.AppCompat.NoActionBar", - ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, - LaunchMode = LaunchMode.SingleInstance/*, - ScreenOrientation = ScreenOrientation.Landscape*/)] - public class MainActivity : AvaloniaActivity - { - protected override AppBuilder CustomizeAppBuilder(AppBuilder builder) - { - return base.CustomizeAppBuilder(builder); - } - } - - public class App : Application - { - public override void Initialize() - { - Styles.Add(new SimpleTheme(new Uri("avares://Avalonia.AndroidTestApplication"))); - } - - public override void OnFrameworkInitializationCompleted() - { - if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime) - { - singleViewLifetime.MainView = CreateSimpleWindow(); - } - - base.OnFrameworkInitializationCompleted(); - } - - // This provides a simple UI tree for testing input handling, drawing, etc - public static ContentControl CreateSimpleWindow() - { - ContentControl window = new ContentControl() - { - Background = Brushes.Red, - Content = new StackPanel - { - Margin = new Thickness(30), - Background = Brushes.Yellow, - Children = - { - new TextBlock - { - Text = "TEXT BLOCK", - Width = 300, - Height = 40, - Background = Brushes.White, - Foreground = Brushes.Black - }, - - new Button - { - Content = "BUTTON", - Width = 150, - Height = 40, - Background = Brushes.LightGreen, - Foreground = Brushes.Black - }, - - CreateTextBox(TextInputContentType.Normal), - CreateTextBox(TextInputContentType.Password), - CreateTextBox(TextInputContentType.Email), - CreateTextBox(TextInputContentType.Url), - CreateTextBox(TextInputContentType.Digits), - CreateTextBox(TextInputContentType.Number), - } - } - }; - - return window; - } - - private static TextBox CreateTextBox(TextInputContentType contentType) - { - var textBox = new TextBox() - { - Margin = new Thickness(20, 10), - Watermark = contentType.ToString(), - BorderThickness = new Thickness(3), - FontSize = 20, - [TextInputOptions.ContentTypeProperty] = contentType - }; - - return textBox; - } - } -} diff --git a/src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml b/src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml deleted file mode 100644 index ad8134f628..0000000000 --- a/src/Android/Avalonia.AndroidTestApplication/Properties/AndroidManifest.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/Android/Avalonia.AndroidTestApplication/Resources/AboutResources.txt b/src/Android/Avalonia.AndroidTestApplication/Resources/AboutResources.txt deleted file mode 100644 index 194ae28a59..0000000000 --- a/src/Android/Avalonia.AndroidTestApplication/Resources/AboutResources.txt +++ /dev/null @@ -1,50 +0,0 @@ -Images, layout descriptions, binary blobs and string dictionaries can be included -in your application as resource files. Various Android APIs are designed to -operate on the resource IDs instead of dealing with images, strings or binary blobs -directly. - -For example, a sample Android app that contains a user interface layout (main.xml), -an internationalization string table (strings.xml) and some icons (drawable-XXX/icon.png) -would keep its resources in the "Resources" directory of the application: - -Resources/ - drawable-hdpi/ - icon.png - - drawable-ldpi/ - icon.png - - drawable-mdpi/ - icon.png - - layout/ - main.xml - - values/ - strings.xml - -In order to get the build system to recognize Android resources, set the build action to -"AndroidResource". The native Android APIs do not operate directly with filenames, but -instead operate on resource IDs. When you compile an Android application that uses resources, -the build system will package the resources for distribution and generate a class called -"Resource" that contains the tokens for each one of the resources included. For example, -for the above Resources layout, this is what the Resource class would expose: - -public class Resource { - public class drawable { - public const int icon = 0x123; - } - - public class layout { - public const int main = 0x456; - } - - public class strings { - public const int first_string = 0xabc; - public const int second_string = 0xbcd; - } -} - -You would then use R.drawable.icon to reference the drawable/icon.png file, or Resource.layout.main -to reference the layout/main.xml file, or Resource.strings.first_string to reference the first -string in the dictionary file values/strings.xml. \ No newline at end of file diff --git a/src/Android/Avalonia.AndroidTestApplication/Resources/drawable/Icon.png b/src/Android/Avalonia.AndroidTestApplication/Resources/drawable/Icon.png deleted file mode 100644 index 8074c4c571b8cd19e27f4ee5545df367420686d7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4147 zcmV-35X|q1P)OwvMs$Q8_8nISM!^>PxsujeDCl4&hPxrxkp%Qc^^|l zp6LqAcf3zf1H4aA1Gv-O6ha)ktct9Y+VA@N^9i;p0H%6v>ZJZYQ`zEa396z-gi{r_ zDz)D=vgRv62GCVeRjK{15j7V@v6|2nafFX6W7z2j1_T0a zLyT3pGTubf1lB5)32>bl0*BflrA!$|_(WD2)iJIfV}37=ZKAC zSe3boYtQ=;o0i>)RtBvsI#iT{0!oF1VFeW`jDjF2Q4aE?{pGCAd>o8Kg#neIh*AMY zLl{;F!vLiem7s*x0<9FKAd6LoPz3~G32P+F+cuGOJ5gcC@pU_?C2fmix7g2)SUaQO$NS07~H)#fn!Q<}KQWtX}wW`g2>cMld+`7Rxgq zChaey66SG560JhO66zA!;sK1cWa2AG$9k~VQY??6bOmJsw9@3uL*z;WWa7(Nm{^TA zilc?y#N9O3LcTo2c)6d}SQl-v-pE4^#wb=s(RxaE28f3FQW(yp$ulG9{KcQ7r>7mQ zE!HYxUYex~*7IinL+l*>HR*UaD;HkQhkL(5I@UwN%Wz504M^d!ylo>ANvKPF_TvA< zkugG5;F6x}$s~J8cnev->_(Ic7%lGQgUi3n#XVo36lUpcS9s z)ympRr7}@|6WF)Ae;D{owN1;aZSR50al9h~?-WhbtKK%bDd zhML131oi1Bu1&Qb$Cp199LJ#;j5d|FhW8_i4KO1OI>}J^p2DfreMSVGY9aFlr&90t zyI2FvxQiKMFviSQeP$Ixh#70qj5O%I+O_I2t2XHWqmh2!1~tHpN3kA4n=1iHj?`@c<~3q^X6_Q$AqTDjBU`|!y<&lkqL|m5tG(b z8a!z&j^m(|;?SW(l*?tZ*{m2H9d&3jqBtXh>O-5e4Qp-W*a5=2NL&Oi62BUM)>zE3 zbSHb>aU3d@3cGggA`C-PsT9^)oy}%dHCaO~nwOrm5E54=aDg(&HR4S23Oa#-a^=}w%g?ZP-1iq8PSjE8jYaGZu z$I)?YN8he?F9>)2d$G6a*zm0XB*Rf&gZAjq(8l@CUDSY1tB#!i> zW$VfG%#SYSiZ};)>pHA`qlfDTEYQEwN6>NNEp+uxuqx({Fgr zjI@!4xRc?vk^9+~eU|mzH__dCDI=xb{Cd}4bELS9xRaS!*FXMwtMR-RR%SLMh0Cjl zencr8#Su<4(%}$yGVBU-HX{18v=yPH*+%^Vtknc>2A;%-~DrYFx^3XfuVgvZ{#1tA== zm3>IzAM2{3Iv_d1XG{P6^tN3|PkJMnjs&CWN7%7_CmjoVakUhsa&dMv==2~^ri?&x zVdv*rnfVyM+I1^Kg*S=23mR@+0T9BWFZUu~@toA8d)fw6be=`Yb6DSX6D?jB%2YT~ z*aHjtIOozfMhA!Jd*?u5_n!SnX>vX`=Ti-1HA4RiE>eI3vTn zz+>Ccf0HX6Ans-ebOB>RJST-Cyr#4XAk+mAlJgdQnoE{^iIN)OcYFSpgJUmXtl@tT z-^ZuUeSj5hSFrQwqX>~EtZ*{>Gi8Bu9_|o06oNtaXP?E936!a@DsvS*tsB@fa6kEA z5GkjwmH?EgpiG&itsB_Tb1NxtFnvxh_s@9KYX1Sttf?AlI~)z zT=6Y7ulx=}<8Scr_UqU-_z)5gPo%050PsbM*ZLno;_-ow&k?FZJtYmb2hPA$LkP)8 z=^d0Q6PImh6Y|QT?{grxj)S=uBKvY2EQUbm@ns9^yKiP~$DcD)c$5Em`zDSScH%iH zVov&m=cMo`1tYwA=!a}vb_ef_{)Q2?FUqn>BR$6phXQRv^1%=YfyE-F$AR4Q?9D!f zCzB^^#td~4u&l~l#rp2QLfe3+_ub9@+|x+m;=2(sQ`s%gO|j$XBb>A7Q(UydipiMw%igcweV#Cr~SP);q>w`bxts_4} znKHg?X==JDkQl3Y>Ckt%`s{n?Nq-1Fw5~%Mq$CAsi-`yu_bKm zxs#QdE7&vgJD%M84f4SNzSDv)S|V?|$!d5a#lhT5>>YWE4NGqa9-fbmV$=)@k&32kdEYetna>=j@0>V8+wRsL;po!3ivVwh<9tn z2S<1u9DAAQ>x1Sn=fk`)At|quvleV($B|#Kap_lB-F^*yV=wZ{9baUu(uXfokr95^ zA*!*W=5a>$2Ps`-F^+qRQT^{*cN>vipT*4!r#p%{(#I7s z0NN94*q?ib$KJjfDI_sjHNdmEVp5wB&j54O#VoFqBwy)gfA$%)4d_X4q${L9Xom2R3xy&ZBSNgt4a1d7K^CDWa9r zVb-_52m}Vp)`9;ZSKd#|U4ZYj5}Gp49{4utST|=c`~(#>KHF6}CCov1iHYw zt{bWo)A@yF2$~c(nR$rSAaFQ$(Wh{vkG1AlutDMw=mM`C`T=X&|Ad9fb5Od}ROt1z zOpczHqrb4Jo^rSCiW#&o(m7jFamnrsTpQb;*h4o8r#$aZ}2RaT-x2u^^ z%u@YyIv$U^u~@9(XGbSwU@fk6SikH>j+D1jQrYTKGJpW%vUT{!d}7THI5&Sa?~MKy zS0-mvMl+BOcroEJ@hN!2H_?coTEJ5Q<;Nd?yx;eIj4{$$E2?YUO|NtNPJ-PdDf;s} zab;}Mz0kbOI}5*w@3gROcnl#5)wQnEhDBfn!Xhy`u>C}*E~vWpO^HS)FC>8^umI=+ z&H;LW6w#;EF`}vQd_9Muru`KnQVPI9U?(sD)&Dg-0j3#(!fNKVZ_GoYH{la~d*1Yh$TI-TL>mI4vpNb@sU2=IZ8vL%AXUx0 zz{K0|nK(yizLHaeW#ZhRfQXoK^}1$=$#1{Yn002ovPDHLkV1n#w+^+xt diff --git a/src/Android/Avalonia.AndroidTestApplication/Resources/values/Strings.xml b/src/Android/Avalonia.AndroidTestApplication/Resources/values/Strings.xml deleted file mode 100644 index c8dca13c35..0000000000 --- a/src/Android/Avalonia.AndroidTestApplication/Resources/values/Strings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - Hello World, Click Me! - Avalonia.AndroidTestApplication - \ No newline at end of file From a71ddcc8700cdd9a589b2385c0e801d55df0928c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 14 Apr 2022 23:50:16 +0200 Subject: [PATCH 12/38] Fix project files broken by #5831. --- .../ControlCatalog.Android.csproj | 44 +-------------- .../ControlCatalog.iOS.csproj | 53 +------------------ 2 files changed, 4 insertions(+), 93 deletions(-) diff --git a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj index 8d2ead054c..04c67e84e8 100644 --- a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj +++ b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj @@ -39,49 +39,9 @@ - + - - {7b92af71-6287-4693-9dcb-bd5b6e927e23} - Avalonia.Android - - - {b09b78d8-9b26-48b0-9149-d64a2f120f3f} - Avalonia.Base - - - {d2221c82-4a25-4583-9b43-d791e3f6820c} - Avalonia.Controls - - - {7062ae20-5dcc-4442-9645-8195bdece63e} - Avalonia.Diagnostics - - - {3e10a5fa-e8da-48b1-ad44-6a5b6cb7750f} - Avalonia.Themes.Default - - - {3e53a01a-b331-47f3-b828-4a5717e77a24} - Avalonia.Markup.Xaml - - - {6417e941-21bc-467b-a771-0de389353ce6} - Avalonia.Markup - - - {7d2d3083-71dd-4cc9-8907-39a0d86fb322} - Avalonia.Skia - - - {d0a739b9-3c68-4ba6-a328-41606954b6bd} - ControlCatalog - - - - - - + \ No newline at end of file diff --git a/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj b/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj index d4f10b3580..513ac44f83 100644 --- a/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj +++ b/samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj @@ -7,59 +7,10 @@ True iossimulator-x64 - + - - - - - - - - - {4488ad85-1495-4809-9aa4-ddfe0a48527e} - Avalonia.iOS - - - {3E53A01A-B331-47F3-B828-4A5717E77A24} - Avalonia.Markup.Xaml - - - {6417E941-21BC-467B-A771-0DE389353CE6} - Avalonia.Markup - - - {B09B78D8-9B26-48B0-9149-D64A2F120F3F} - Avalonia.Base - - - {D2221C82-4A25-4583-9B43-D791E3F6820C} - Avalonia.Controls - - - {7062AE20-5DCC-4442-9645-8195BDECE63E} - Avalonia.Diagnostics - - - {3E10A5FA-E8DA-48B1-AD44-6A5B6CB7750F} - Avalonia.Themes.Default - - - {7d2d3083-71dd-4cc9-8907-39a0d86fb322} - Avalonia.Skia - - - {d0a739b9-3c68-4ba6-a328-41606954b6bd} - ControlCatalog - - - - - - - - + \ No newline at end of file From c1644eb21455bb2658ed67c52c5425139c0cb672 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 Apr 2022 22:11:48 +0000 Subject: [PATCH 13/38] Bump async in /src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp Bumps [async](https://github.com/caolan/async) from 2.6.3 to 2.6.4. - [Release notes](https://github.com/caolan/async/releases) - [Changelog](https://github.com/caolan/async/blob/v2.6.4/CHANGELOG.md) - [Commits](https://github.com/caolan/async/compare/v2.6.3...v2.6.4) --- updated-dependencies: - dependency-name: async dependency-type: indirect ... Signed-off-by: dependabot[bot] --- .../Remote/HtmlTransport/webapp/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package-lock.json b/src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package-lock.json index 08ede477c3..403bb5a59a 100644 --- a/src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package-lock.json +++ b/src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package-lock.json @@ -377,9 +377,9 @@ "dev": true }, "async": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", - "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", "dev": true, "requires": { "lodash": "^4.17.14" From 3b24669d084c1801c3f94eb294d7e95ba2194bfc Mon Sep 17 00:00:00 2001 From: Steven He Date: Fri, 15 Apr 2022 17:27:39 +0800 Subject: [PATCH 14/38] Add missing props reference to sln --- Avalonia.sln | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Avalonia.sln b/Avalonia.sln index e5baee7911..8e94eb4e63 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -113,6 +113,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1 build\SharedVersion.props = build\SharedVersion.props build\SharpDX.props = build\SharpDX.props build\SkiaSharp.props = build\SkiaSharp.props + build\SourceGenerators.props = build\SourceGenerators.props build\SourceLink.props = build\SourceLink.props build\System.Drawing.Common.props = build\System.Drawing.Common.props build\System.Memory.props = build\System.Memory.props @@ -214,6 +215,7 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.PlatformSupport.UnitTests", "tests\Avalonia.PlatformSupport.UnitTests\Avalonia.PlatformSupport.UnitTests.csproj", "{CE910927-CE5A-456F-BC92-E4C757354A5C}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.SourceGenerator", "src\Avalonia.SourceGenerator\Avalonia.SourceGenerator.csproj", "{CA932DF3-2616-4BF6-8F28-1AD0EC40F1FF}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevAnalyzers", "src\tools\DevAnalyzers\DevAnalyzers.csproj", "{2B390431-288C-435C-BB6B-A374033BD8D1}" EndProject Global From 7418fd7dc39aa91ea70dc4dcc2a4c5246ec71993 Mon Sep 17 00:00:00 2001 From: Steven He Date: Fri, 15 Apr 2022 18:19:07 +0800 Subject: [PATCH 15/38] Apply code review feedbacks --- Avalonia.sln | 2 +- src/Avalonia.Base/Avalonia.Base.csproj | 1 + src/Avalonia.Base/Input/KeyGesture.cs | 12 ++---------- .../Avalonia.DesignerSupport.csproj | 1 + .../Remote/HtmlTransport/HtmlTransport.cs | 14 ++------------ src/Shared/EnumParserHelper.cs | 19 +++++++++++++++++++ 6 files changed, 26 insertions(+), 23 deletions(-) create mode 100644 src/Shared/EnumParserHelper.cs diff --git a/Avalonia.sln b/Avalonia.sln index 8e94eb4e63..1f59a94e2e 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -38,8 +38,8 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{A689DEF5-D50F-4975-8B72-124C9EB54066}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig + src\Shared\EnumParserHelper.cs = src\Shared\EnumParserHelper.cs src\Shared\ModuleInitializer.cs = src\Shared\ModuleInitializer.cs - src\Shared\RawEventGrouping.cs = src\Shared\RawEventGrouping.cs src\Shared\SourceGeneratorAttributes.cs = src\Shared\SourceGeneratorAttributes.cs EndProjectSection EndProject diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj index 8e4755b4b7..f0d54d831a 100644 --- a/src/Avalonia.Base/Avalonia.Base.csproj +++ b/src/Avalonia.Base/Avalonia.Base.csproj @@ -7,6 +7,7 @@ + diff --git a/src/Avalonia.Base/Input/KeyGesture.cs b/src/Avalonia.Base/Input/KeyGesture.cs index 6f302a3ae7..d7fe89afda 100644 --- a/src/Avalonia.Base/Input/KeyGesture.cs +++ b/src/Avalonia.Base/Input/KeyGesture.cs @@ -155,11 +155,7 @@ namespace Avalonia.Input if (s_keySynonyms.TryGetValue(key.ToLower(), out Key rv)) return rv; -#if NETSTANDARD2_0 - return (Key)Enum.Parse(typeof(Key), key, true); -#else - return Enum.Parse(key, true); -#endif + return EnumParserHelper.ParseEnum(key, true); } private static KeyModifiers ParseModifier(ReadOnlySpan modifier) @@ -176,11 +172,7 @@ namespace Avalonia.Input return KeyModifiers.Meta; } -#if NETSTANDARD2_0 - return (KeyModifiers)Enum.Parse(typeof(KeyModifiers), modifier.ToString(), true); -#else - return Enum.Parse(modifier.ToString(), true); -#endif + return EnumParserHelper.ParseEnum(modifier.ToString(), true); } private Key ResolveNumPadOperationKey(Key key) diff --git a/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj b/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj index 0270000d8c..66a7a6389e 100644 --- a/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj +++ b/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj @@ -19,6 +19,7 @@ + diff --git a/src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs b/src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs index 7965e5d0d8..11a1efb9c7 100644 --- a/src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs +++ b/src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs @@ -320,23 +320,13 @@ namespace Avalonia.DesignerSupport.Remote.HtmlTransport ? null : modifiersText .Split(',') -#if NETSTANDARD2_0 - .Select(x => (InputProtocol.InputModifiers)Enum.Parse( - typeof(InputProtocol.InputModifiers), x, true)) -#else - .Select(x => Enum.Parse(x, true)) -#endif + .Select(x => EnumParserHelper.ParseEnum(x, true)) .ToArray(); private static InputProtocol.MouseButton ParseMouseButton(string buttonText) => string.IsNullOrWhiteSpace(buttonText) ? InputProtocol.MouseButton.None -#if NETSTANDARD2_0 - : (InputProtocol.MouseButton)Enum.Parse( - typeof(InputProtocol.MouseButton), buttonText, true); -#else - : Enum.Parse(buttonText, true); -#endif + : EnumParserHelper.ParseEnum(buttonText, true); private static double ParseDouble(string text) => double.Parse(text, NumberStyles.Float, CultureInfo.InvariantCulture); diff --git a/src/Shared/EnumParserHelper.cs b/src/Shared/EnumParserHelper.cs new file mode 100644 index 0000000000..09d6a0cb7d --- /dev/null +++ b/src/Shared/EnumParserHelper.cs @@ -0,0 +1,19 @@ +using System; + +namespace Avalonia +{ + internal class EnumParserHelper + { +#if NET6_0 + public static T ParseEnum(ReadOnlySpan key, bool ignoreCase) where T : struct + { + return Enum.Parse(key, ignoreCase); + } +#else + public static T ParseEnum(string key, bool ignoreCase) where T : struct + { + return (T)Enum.Parse(typeof(T), key, ignoreCase); + } +#endif + } +} From 67b18b8e3fdb7ed507dbae6f6834faff50a7d4ba Mon Sep 17 00:00:00 2001 From: Steven He Date: Fri, 15 Apr 2022 18:22:10 +0800 Subject: [PATCH 16/38] Rename EnumParserHelper --- Avalonia.sln | 2 +- src/Avalonia.Base/Avalonia.Base.csproj | 2 +- src/Avalonia.Base/Input/KeyGesture.cs | 4 ++-- .../Avalonia.DesignerSupport.csproj | 2 +- .../Remote/HtmlTransport/HtmlTransport.cs | 4 ++-- src/Shared/{EnumParserHelper.cs => EnumHelper.cs} | 6 +++--- 6 files changed, 10 insertions(+), 10 deletions(-) rename src/Shared/{EnumParserHelper.cs => EnumHelper.cs} (51%) diff --git a/Avalonia.sln b/Avalonia.sln index 1f59a94e2e..8a48566653 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -38,7 +38,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{A689DEF5-D50F-4975-8B72-124C9EB54066}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig - src\Shared\EnumParserHelper.cs = src\Shared\EnumParserHelper.cs + src\Shared\EnumHelper.cs = src\Shared\EnumHelper.cs src\Shared\ModuleInitializer.cs = src\Shared\ModuleInitializer.cs src\Shared\SourceGeneratorAttributes.cs = src\Shared\SourceGeneratorAttributes.cs EndProjectSection diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj index f0d54d831a..a9a606de8d 100644 --- a/src/Avalonia.Base/Avalonia.Base.csproj +++ b/src/Avalonia.Base/Avalonia.Base.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/Avalonia.Base/Input/KeyGesture.cs b/src/Avalonia.Base/Input/KeyGesture.cs index d7fe89afda..cb6d12ac0d 100644 --- a/src/Avalonia.Base/Input/KeyGesture.cs +++ b/src/Avalonia.Base/Input/KeyGesture.cs @@ -155,7 +155,7 @@ namespace Avalonia.Input if (s_keySynonyms.TryGetValue(key.ToLower(), out Key rv)) return rv; - return EnumParserHelper.ParseEnum(key, true); + return EnumHelper.Parse(key, true); } private static KeyModifiers ParseModifier(ReadOnlySpan modifier) @@ -172,7 +172,7 @@ namespace Avalonia.Input return KeyModifiers.Meta; } - return EnumParserHelper.ParseEnum(modifier.ToString(), true); + return EnumHelper.Parse(modifier.ToString(), true); } private Key ResolveNumPadOperationKey(Key key) diff --git a/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj b/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj index 66a7a6389e..586d13f179 100644 --- a/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj +++ b/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj @@ -19,7 +19,7 @@ - + diff --git a/src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs b/src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs index 11a1efb9c7..794bc94256 100644 --- a/src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs +++ b/src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs @@ -320,13 +320,13 @@ namespace Avalonia.DesignerSupport.Remote.HtmlTransport ? null : modifiersText .Split(',') - .Select(x => EnumParserHelper.ParseEnum(x, true)) + .Select(x => EnumHelper.Parse(x, true)) .ToArray(); private static InputProtocol.MouseButton ParseMouseButton(string buttonText) => string.IsNullOrWhiteSpace(buttonText) ? InputProtocol.MouseButton.None - : EnumParserHelper.ParseEnum(buttonText, true); + : EnumHelper.Parse(buttonText, true); private static double ParseDouble(string text) => double.Parse(text, NumberStyles.Float, CultureInfo.InvariantCulture); diff --git a/src/Shared/EnumParserHelper.cs b/src/Shared/EnumHelper.cs similarity index 51% rename from src/Shared/EnumParserHelper.cs rename to src/Shared/EnumHelper.cs index 09d6a0cb7d..8511e712fa 100644 --- a/src/Shared/EnumParserHelper.cs +++ b/src/Shared/EnumHelper.cs @@ -2,15 +2,15 @@ namespace Avalonia { - internal class EnumParserHelper + internal class EnumHelper { #if NET6_0 - public static T ParseEnum(ReadOnlySpan key, bool ignoreCase) where T : struct + public static T Parse(ReadOnlySpan key, bool ignoreCase) where T : struct { return Enum.Parse(key, ignoreCase); } #else - public static T ParseEnum(string key, bool ignoreCase) where T : struct + public static T Parse(string key, bool ignoreCase) where T : struct { return (T)Enum.Parse(typeof(T), key, ignoreCase); } From 4f6da322fa4ac370d320f50a84aa9bcb5e5dd405 Mon Sep 17 00:00:00 2001 From: Steven He Date: Fri, 15 Apr 2022 18:23:18 +0800 Subject: [PATCH 17/38] PrivateAssets --- build/SourceGenerators.props | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/build/SourceGenerators.props b/build/SourceGenerators.props index f9eab24d08..d000af1bf6 100644 --- a/build/SourceGenerators.props +++ b/build/SourceGenerators.props @@ -1,6 +1,10 @@ - + From d416b4de45413987330ef856035a02d07e177ad1 Mon Sep 17 00:00:00 2001 From: Steven He Date: Fri, 15 Apr 2022 18:30:08 +0800 Subject: [PATCH 18/38] NET6_0 -> NET6_0_OR_GREATER --- src/Shared/EnumHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Shared/EnumHelper.cs b/src/Shared/EnumHelper.cs index 8511e712fa..fddcf7ebc5 100644 --- a/src/Shared/EnumHelper.cs +++ b/src/Shared/EnumHelper.cs @@ -4,7 +4,7 @@ namespace Avalonia { internal class EnumHelper { -#if NET6_0 +#if NET6_0_OR_GREATER public static T Parse(ReadOnlySpan key, bool ignoreCase) where T : struct { return Enum.Parse(key, ignoreCase); From 578f5fc1cbd2abb42f85fb3bf798395721de6c57 Mon Sep 17 00:00:00 2001 From: Rustam Sayfutdinov Date: Fri, 15 Apr 2022 13:31:37 +0300 Subject: [PATCH 19/38] Fix PlatformSupport.UnitTests --- nukebuild/Build.cs | 1 + src/Avalonia.PlatformSupport/AssetLoader.cs | 8 ++++---- .../Avalonia.PlatformSupport.csproj | 2 +- .../Internal/AssemblyDescriptor.cs | 10 +++++++++- .../Internal/AssemblyDescriptorResolver.cs | 11 ++++++++--- .../AssetLoaderTests.cs | 6 +++--- 6 files changed, 26 insertions(+), 12 deletions(-) diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index 9fcb9d6b7f..f0f677b844 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -221,6 +221,7 @@ partial class Build : NukeBuild RunCoreTest("Avalonia.Markup.Xaml.UnitTests"); RunCoreTest("Avalonia.Skia.UnitTests"); RunCoreTest("Avalonia.ReactiveUI.UnitTests"); + RunCoreTest("Avalonia.PlatformSupport.UnitTests"); }); Target RunRenderTests => _ => _ diff --git a/src/Avalonia.PlatformSupport/AssetLoader.cs b/src/Avalonia.PlatformSupport/AssetLoader.cs index fb03ec2f6e..0e33c3d4c7 100644 --- a/src/Avalonia.PlatformSupport/AssetLoader.cs +++ b/src/Avalonia.PlatformSupport/AssetLoader.cs @@ -14,14 +14,14 @@ namespace Avalonia.PlatformSupport /// public class AssetLoader : IAssetLoader { - private static AssemblyDescriptorResolver s_assemblyDescriptorResolver = new(); + private static IAssemblyDescriptorResolver s_assemblyDescriptorResolver = new AssemblyDescriptorResolver(); private AssemblyDescriptor? _defaultResmAssembly; /// /// Introduced for tests. /// - internal static void SetAssemblyDescriptorResolver(AssemblyDescriptorResolver resolver) => + internal static void SetAssemblyDescriptorResolver(IAssemblyDescriptorResolver resolver) => s_assemblyDescriptorResolver = resolver; /// @@ -182,13 +182,13 @@ namespace Avalonia.PlatformSupport throw new ArgumentException($"Unsupported url type: " + uri.Scheme, nameof(uri)); } - private (AssemblyDescriptor asm, string path) GetResAsmAndPath(Uri uri) + private (IAssemblyDescriptor asm, string path) GetResAsmAndPath(Uri uri) { var asm = s_assemblyDescriptorResolver.GetAssembly(uri.Authority); return (asm, uri.GetUnescapeAbsolutePath()); } - private AssemblyDescriptor? GetAssembly(Uri? uri) + private IAssemblyDescriptor? GetAssembly(Uri? uri) { if (uri != null) { diff --git a/src/Avalonia.PlatformSupport/Avalonia.PlatformSupport.csproj b/src/Avalonia.PlatformSupport/Avalonia.PlatformSupport.csproj index 420ac0796c..5336f1e630 100644 --- a/src/Avalonia.PlatformSupport/Avalonia.PlatformSupport.csproj +++ b/src/Avalonia.PlatformSupport/Avalonia.PlatformSupport.csproj @@ -19,6 +19,6 @@ - + diff --git a/src/Avalonia.PlatformSupport/Internal/AssemblyDescriptor.cs b/src/Avalonia.PlatformSupport/Internal/AssemblyDescriptor.cs index a3de7f2b8a..64ffec8482 100644 --- a/src/Avalonia.PlatformSupport/Internal/AssemblyDescriptor.cs +++ b/src/Avalonia.PlatformSupport/Internal/AssemblyDescriptor.cs @@ -6,7 +6,15 @@ using Avalonia.Utilities; namespace Avalonia.PlatformSupport.Internal; -internal class AssemblyDescriptor +internal interface IAssemblyDescriptor +{ + Assembly Assembly { get; } + Dictionary? Resources { get; } + Dictionary? AvaloniaResources { get; } + string? Name { get; } +} + +internal class AssemblyDescriptor : IAssemblyDescriptor { public AssemblyDescriptor(Assembly assembly) { diff --git a/src/Avalonia.PlatformSupport/Internal/AssemblyDescriptorResolver.cs b/src/Avalonia.PlatformSupport/Internal/AssemblyDescriptorResolver.cs index a78051a9c4..28ae35d57d 100644 --- a/src/Avalonia.PlatformSupport/Internal/AssemblyDescriptorResolver.cs +++ b/src/Avalonia.PlatformSupport/Internal/AssemblyDescriptorResolver.cs @@ -5,11 +5,16 @@ using System.Reflection; namespace Avalonia.PlatformSupport.Internal; -internal class AssemblyDescriptorResolver +internal interface IAssemblyDescriptorResolver { - private readonly Dictionary _assemblyNameCache = new(); + IAssemblyDescriptor GetAssembly(string name); +} + +internal class AssemblyDescriptorResolver: IAssemblyDescriptorResolver +{ + private readonly Dictionary _assemblyNameCache = new(); - public AssemblyDescriptor GetAssembly(string name) + public IAssemblyDescriptor GetAssembly(string name) { if (name == null) throw new ArgumentNullException(nameof(name)); diff --git a/tests/Avalonia.PlatformSupport.UnitTests/AssetLoaderTests.cs b/tests/Avalonia.PlatformSupport.UnitTests/AssetLoaderTests.cs index f950fb7e99..dfd195073b 100644 --- a/tests/Avalonia.PlatformSupport.UnitTests/AssetLoaderTests.cs +++ b/tests/Avalonia.PlatformSupport.UnitTests/AssetLoaderTests.cs @@ -16,7 +16,7 @@ public class AssetLoaderTests static AssetLoaderTests() { - var resolver = Mock.Of(); + var resolver = Mock.Of(); var descriptor = CreateAssemblyDescriptor(AssemblyNameWithWhitespace); Mock.Get(resolver).Setup(x => x.GetAssembly(AssemblyNameWithWhitespace)).Returns(descriptor); @@ -49,13 +49,13 @@ public class AssetLoaderTests Assert.Equal(AssemblyNameWithNonAscii, assemblyActual?.FullName); } - private static AssemblyDescriptor CreateAssemblyDescriptor(string assemblyName) + private static IAssemblyDescriptor CreateAssemblyDescriptor(string assemblyName) { var assembly = Mock.Of(); Mock.Get(assembly).Setup(x => x.GetName()).Returns(new AssemblyName(assemblyName)); Mock.Get(assembly).Setup(x => x.FullName).Returns(assemblyName); - var descriptor = Mock.Of(); + var descriptor = Mock.Of(); Mock.Get(descriptor).Setup(x => x.Assembly).Returns(assembly); return descriptor; } From fd5e2169c143fb38b32edb1f2e2825bb2a35a513 Mon Sep 17 00:00:00 2001 From: Tako Date: Fri, 15 Apr 2022 14:56:02 +0300 Subject: [PATCH 20/38] reimplementation and some tests --- src/Avalonia.Controls/ComboBox.cs | 19 +---- src/Avalonia.Controls/ItemsControl.cs | 1 - src/Avalonia.Controls/StackPanel.cs | 2 +- .../ComboBoxTests.cs | 75 +++++++++++++++++++ 4 files changed, 77 insertions(+), 20 deletions(-) diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index 582f90e472..cbf9b35a05 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -455,24 +455,7 @@ namespace Avalonia.Controls { if (ItemCount >= 1) { - if (SelectedIndex == -1) - { - if (MoveSelection(NavigationDirection.First, WrapSelection) == false) - { - // MoveSelection works only with indexes starting from 0 - // so to make it search further than the first item we need to set SelectedIndex to 0. - SelectedIndex = 0; - var isSelectionMoved = MoveSelection(NavigationDirection.Next, WrapSelection); - if (isSelectionMoved == false) - { - SelectedIndex = -1; - } - } - } - else - { - MoveSelection(NavigationDirection.Next, WrapSelection); - } + MoveSelection(NavigationDirection.Next, WrapSelection); } } diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 0cd72dc91c..afe4a08446 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -508,7 +508,6 @@ namespace Avalonia.Controls do { result = container.GetControl(direction, c, wrap); - from = from ?? result; if (result != null && result.Focusable && diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index feb425a9c3..50c48d2bb0 100644 --- a/src/Avalonia.Controls/StackPanel.cs +++ b/src/Avalonia.Controls/StackPanel.cs @@ -123,7 +123,7 @@ namespace Avalonia.Controls index = Children.Count - 1; break; case NavigationDirection.Next: - if (index != -1) ++index; + ++index; break; case NavigationDirection.Previous: if (index != -1) --index; diff --git a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs index cb2fd11175..98695fe88e 100644 --- a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs @@ -36,6 +36,81 @@ namespace Avalonia.Controls.UnitTests Assert.False(target.IsDropDownOpen); } + [Fact] + public void WrapSelection_Should_Work() + { + using (UnitTestApplication.Start(TestServices.RealFocus)) + { + var items = new[] + { + new ComboBoxItem() { Content = "bla" }, + new ComboBoxItem() { Content = "dd" }, + new ComboBoxItem() { Content = "sdf", IsEnabled = false } + }; + var target = new ComboBox + { + Items = items, + Template = GetTemplate(), + WrapSelection = true + }; + var root = new TestRoot(target); + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + target.Focus(); + Assert.Equal(target.SelectedIndex, -1); + Assert.True(target.IsFocused); + target.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = Key.Up, + }); + Assert.Equal(target.SelectedIndex, 1); + target.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = Key.Down, + }); + Assert.Equal(target.SelectedIndex, 0); + } + } + + [Fact] + public void Focuses_Next_Item_On_Key_Down() + { + using (UnitTestApplication.Start(TestServices.RealFocus)) + { + var items = new[] + { + new ComboBoxItem() { Content = "bla" }, + new ComboBoxItem() { Content = "dd", IsEnabled = false }, + new ComboBoxItem() { Content = "sdf" } + }; + var target = new ComboBox + { + Items = items, + Template = GetTemplate() + }; + var root = new TestRoot(target); + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + target.Focus(); + Assert.Equal(target.SelectedIndex, -1); + Assert.True(target.IsFocused); + target.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = Key.Down, + }); + Assert.Equal(target.SelectedIndex, 0); + target.RaiseEvent(new KeyEventArgs + { + RoutedEvent = InputElement.KeyDownEvent, + Key = Key.Down, + }); + Assert.Equal(target.SelectedIndex, 2); + } + } + [Fact] public void SelectionBoxItem_Is_Rectangle_With_VisualBrush_When_Selection_Is_Control() { From 2a2e4ca6db9651a334de740033eb507aea6f3c3b Mon Sep 17 00:00:00 2001 From: Steven He Date: Fri, 15 Apr 2022 23:53:10 +0800 Subject: [PATCH 21/38] Apply code review feedbacks --- Avalonia.sln | 1 - 1 file changed, 1 deletion(-) diff --git a/Avalonia.sln b/Avalonia.sln index 8a48566653..1e2a3c6027 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -38,7 +38,6 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Shared", "Shared", "{A689DEF5-D50F-4975-8B72-124C9EB54066}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig - src\Shared\EnumHelper.cs = src\Shared\EnumHelper.cs src\Shared\ModuleInitializer.cs = src\Shared\ModuleInitializer.cs src\Shared\SourceGeneratorAttributes.cs = src\Shared\SourceGeneratorAttributes.cs EndProjectSection From be8bcd2be7bb31321af83a6c0ba99f87a07d67cc Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 15 Apr 2022 12:06:45 -0400 Subject: [PATCH 22/38] Update src/Avalonia.Base/Input/PointerOverPreProcessor.cs --- src/Avalonia.Base/Input/PointerOverPreProcessor.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Avalonia.Base/Input/PointerOverPreProcessor.cs b/src/Avalonia.Base/Input/PointerOverPreProcessor.cs index a38364197d..d22252893d 100644 --- a/src/Avalonia.Base/Input/PointerOverPreProcessor.cs +++ b/src/Avalonia.Base/Input/PointerOverPreProcessor.cs @@ -60,7 +60,6 @@ namespace Avalonia.Input public void SceneInvalidated(Rect dirtyRect) { - // Pointer is outside of the target area if (_lastPointer is (var pointer, var position)) { var clientPoint = _inputRoot.PointToClient(position); From 7530e20be04366fd621bca9650faba41294cdbb7 Mon Sep 17 00:00:00 2001 From: Steven He Date: Sat, 16 Apr 2022 00:21:42 +0800 Subject: [PATCH 23/38] Move EnumHelper to Avalonia.Base --- src/Avalonia.Base/Avalonia.Base.csproj | 1 - src/Avalonia.Base/Input/KeyGesture.cs | 1 + src/Avalonia.Base/Properties/AssemblyInfo.cs | 1 + src/{Shared => Avalonia.Base/Utilities}/EnumHelper.cs | 2 +- src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj | 1 - .../Remote/HtmlTransport/HtmlTransport.cs | 1 + 6 files changed, 4 insertions(+), 3 deletions(-) rename src/{Shared => Avalonia.Base/Utilities}/EnumHelper.cs (93%) diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj index a9a606de8d..8e4755b4b7 100644 --- a/src/Avalonia.Base/Avalonia.Base.csproj +++ b/src/Avalonia.Base/Avalonia.Base.csproj @@ -7,7 +7,6 @@ - diff --git a/src/Avalonia.Base/Input/KeyGesture.cs b/src/Avalonia.Base/Input/KeyGesture.cs index cb6d12ac0d..3b7a828b86 100644 --- a/src/Avalonia.Base/Input/KeyGesture.cs +++ b/src/Avalonia.Base/Input/KeyGesture.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Text; +using Avalonia.Utilities; namespace Avalonia.Input { diff --git a/src/Avalonia.Base/Properties/AssemblyInfo.cs b/src/Avalonia.Base/Properties/AssemblyInfo.cs index 4d7f487e00..a0560924e7 100644 --- a/src/Avalonia.Base/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Base/Properties/AssemblyInfo.cs @@ -21,6 +21,7 @@ using Avalonia.Metadata; [assembly: InternalsVisibleTo("Avalonia.Controls, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("Avalonia.Controls.DataGrid, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("Avalonia.Controls.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] +[assembly: InternalsVisibleTo("Avalonia.DesignerSupport, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("Avalonia.Direct2D1.RenderTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("Avalonia.LeakTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("Avalonia.Markup.Xaml.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] diff --git a/src/Shared/EnumHelper.cs b/src/Avalonia.Base/Utilities/EnumHelper.cs similarity index 93% rename from src/Shared/EnumHelper.cs rename to src/Avalonia.Base/Utilities/EnumHelper.cs index fddcf7ebc5..c857033ef1 100644 --- a/src/Shared/EnumHelper.cs +++ b/src/Avalonia.Base/Utilities/EnumHelper.cs @@ -1,6 +1,6 @@ using System; -namespace Avalonia +namespace Avalonia.Utilities { internal class EnumHelper { diff --git a/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj b/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj index 586d13f179..0270000d8c 100644 --- a/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj +++ b/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj @@ -19,7 +19,6 @@ - diff --git a/src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs b/src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs index 794bc94256..f100be5d5b 100644 --- a/src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs +++ b/src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs @@ -10,6 +10,7 @@ using System.Threading; using System.Threading.Tasks; using Avalonia.Remote.Protocol; using Avalonia.Remote.Protocol.Viewport; +using Avalonia.Utilities; using InputProtocol = Avalonia.Remote.Protocol.Input; namespace Avalonia.DesignerSupport.Remote.HtmlTransport From 37f678e96a7b35c2d0c79d6871d5e06fd540948b Mon Sep 17 00:00:00 2001 From: robloo Date: Fri, 15 Apr 2022 19:05:10 -0400 Subject: [PATCH 24/38] Fix missing color alpha rounding --- src/Avalonia.Base/Media/Color.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Media/Color.cs b/src/Avalonia.Base/Media/Color.cs index eaa886ccbd..9ad75b3290 100644 --- a/src/Avalonia.Base/Media/Color.cs +++ b/src/Avalonia.Base/Media/Color.cs @@ -375,7 +375,7 @@ namespace Avalonia.Media byte.TryParse(components[2], NumberStyles.Number, CultureInfo.InvariantCulture, out byte blue) && TryInternalParse(components[3], out double alpha)) { - color = new Color((byte)(alpha * 255), red, green, blue); + color = new Color((byte)Math.Round(alpha * 255.0), red, green, blue); return true; } } From c7e0a68f2779361ac23bf13003860ed5634c8278 Mon Sep 17 00:00:00 2001 From: robloo Date: Fri, 15 Apr 2022 19:07:01 -0400 Subject: [PATCH 25/38] Support RGB component percentages in CSS format parsing --- src/Avalonia.Base/Media/Color.cs | 43 +++++++++++++++---- .../Media/ColorTests.cs | 24 ++++++++--- 2 files changed, 53 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.Base/Media/Color.cs b/src/Avalonia.Base/Media/Color.cs index 9ad75b3290..2f0def58a4 100644 --- a/src/Avalonia.Base/Media/Color.cs +++ b/src/Avalonia.Base/Media/Color.cs @@ -360,9 +360,9 @@ namespace Avalonia.Media if (components.Length == 3) // RGB { - if (byte.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out byte red) && - byte.TryParse(components[1], NumberStyles.Number, CultureInfo.InvariantCulture, out byte green) && - byte.TryParse(components[2], NumberStyles.Number, CultureInfo.InvariantCulture, out byte blue)) + if (InternalTryParseByte(components[0], out byte red) && + InternalTryParseByte(components[1], out byte green) && + InternalTryParseByte(components[2], out byte blue)) { color = new Color(0xFF, red, green, blue); return true; @@ -370,18 +370,45 @@ namespace Avalonia.Media } else if (components.Length == 4) // RGBA { - if (byte.TryParse(components[0], NumberStyles.Number, CultureInfo.InvariantCulture, out byte red) && - byte.TryParse(components[1], NumberStyles.Number, CultureInfo.InvariantCulture, out byte green) && - byte.TryParse(components[2], NumberStyles.Number, CultureInfo.InvariantCulture, out byte blue) && - TryInternalParse(components[3], out double alpha)) + if (InternalTryParseByte(components[0], out byte red) && + InternalTryParseByte(components[1], out byte green) && + InternalTryParseByte(components[2], out byte blue) && + InternalTryParseDouble(components[3], out double alpha)) { color = new Color((byte)Math.Round(alpha * 255.0), red, green, blue); return true; } } + // Local function to specially parse a byte value with an optional percentage sign + bool InternalTryParseByte(string inString, out byte outByte) + { + // The percent sign, if it exists, must be at the end of the number + int percentIndex = inString.IndexOf("%", StringComparison.Ordinal); + + if (percentIndex >= 0) + { + var result = double.TryParse( + inString.Substring(0, percentIndex), + NumberStyles.Number, + CultureInfo.InvariantCulture, + out double percentage); + + outByte = (byte)Math.Round((percentage / 100.0) * 255.0); + return result; + } + else + { + return byte.TryParse( + inString, + NumberStyles.Number, + CultureInfo.InvariantCulture, + out outByte); + } + } + // Local function to specially parse a double value with an optional percentage sign - bool TryInternalParse(string inString, out double outDouble) + bool InternalTryParseDouble(string inString, out double outDouble) { // The percent sign, if it exists, must be at the end of the number int percentIndex = inString.IndexOf("%", StringComparison.Ordinal); diff --git a/tests/Avalonia.Base.UnitTests/Media/ColorTests.cs b/tests/Avalonia.Base.UnitTests/Media/ColorTests.cs index 1392635b32..36929d5e95 100644 --- a/tests/Avalonia.Base.UnitTests/Media/ColorTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/ColorTests.cs @@ -216,8 +216,8 @@ namespace Avalonia.Base.UnitTests.Media Tuple.Create("hsl(-1000, -1000, -1000)", new HslColor(1, 0, 0, 0)), // Clamps to min Tuple.Create("hsl(-1000, -1000%, -1000%)", new HslColor(1, 0, 0, 0)), // Clamps to min - Tuple.Create("hsl(1000, 1000, 1000)", new HslColor(1, 0, 1, 1)), // Clamps to max - Tuple.Create("hsl(1000, 1000%, 1000%)", new HslColor(1, 0, 1, 1)), // Clamps to max + Tuple.Create("hsl(1000, 1000, 1000)", new HslColor(1, 0, 1, 1)), // Clamps to max (Hue wraps to zero) + Tuple.Create("hsl(1000, 1000%, 1000%)", new HslColor(1, 0, 1, 1)), // Clamps to max (Hue wraps to zero) Tuple.Create("hsl(300, 0.8, 0.2)", new HslColor(1.0, 300, 0.8, 0.2)), Tuple.Create("hsl(300, 80%, 20%)", new HslColor(1.0, 300, 0.8, 0.2)), @@ -262,8 +262,8 @@ namespace Avalonia.Base.UnitTests.Media Tuple.Create("hsv(-1000, -1000, -1000)", new HsvColor(1, 0, 0, 0)), // Clamps to min Tuple.Create("hsv(-1000, -1000%, -1000%)", new HsvColor(1, 0, 0, 0)), // Clamps to min - Tuple.Create("hsv(1000, 1000, 1000)", new HsvColor(1, 0, 1, 1)), // Clamps to max - Tuple.Create("hsv(1000, 1000%, 1000%)", new HsvColor(1, 0, 1, 1)), // Clamps to max + Tuple.Create("hsv(1000, 1000, 1000)", new HsvColor(1, 0, 1, 1)), // Clamps to max (Hue wraps to zero) + Tuple.Create("hsv(1000, 1000%, 1000%)", new HsvColor(1, 0, 1, 1)), // Clamps to max (Hue wraps to zero) Tuple.Create("hsv(300, 0.8, 0.2)", new HsvColor(1.0, 300, 0.8, 0.2)), Tuple.Create("hsv(300, 80%, 20%)", new HsvColor(1.0, 300, 0.8, 0.2)), @@ -303,8 +303,20 @@ namespace Avalonia.Base.UnitTests.Media Tuple.Create("#123456", new Color(0xff, 0x12, 0x34, 0x56)), Tuple.Create("rgb(100, 30, 45)", new Color(255, 100, 30, 45)), - Tuple.Create("rgba(100, 30, 45, 0.9)", new Color(229, 100, 30, 45)), - Tuple.Create("rgba(100, 30, 45, 90%)", new Color(229, 100, 30, 45)), + Tuple.Create("rgba(100, 30, 45, 0.9)", new Color(230, 100, 30, 45)), + Tuple.Create("rgba(100, 30, 45, 90%)", new Color(230, 100, 30, 45)), + + Tuple.Create("rgb(255,0,0)", new Color(255, 255, 0, 0)), + Tuple.Create("rgb(0,255,0)", new Color(255, 0, 255, 0)), + Tuple.Create("rgb(0,0,255)", new Color(255, 0, 0, 255)), + + Tuple.Create("rgb(100%, 0, 0)", new Color(255, 255, 0, 0)), + Tuple.Create("rgb(0, 100%, 0)", new Color(255, 0, 255, 0)), + Tuple.Create("rgb(0, 0, 100%)", new Color(255, 0, 0, 255)), + + Tuple.Create("rgba(0, 0, 100%, 50%)", new Color(128, 0, 0, 255)), + Tuple.Create("rgba(50%, 10%, 80%, 50%)", new Color(128, 128, 26, 204)), + Tuple.Create("rgba(50%, 10%, 80%, 0.5)", new Color(128, 128, 26, 204)), // HSL Tuple.Create("hsl(296, 85%, 12%)", new Color(255, 53, 5, 57)), From 467f78b58d3ee2554a79df40693e260cd736d3ca Mon Sep 17 00:00:00 2001 From: robloo Date: Fri, 15 Apr 2022 19:16:17 -0400 Subject: [PATCH 26/38] Improve color format length checking for performance --- src/Avalonia.Base/Media/Color.cs | 22 ++++++++++++++-------- src/Avalonia.Base/Media/HslColor.cs | 11 +++++++++-- src/Avalonia.Base/Media/HsvColor.cs | 11 +++++++++-- 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/src/Avalonia.Base/Media/Color.cs b/src/Avalonia.Base/Media/Color.cs index 2f0def58a4..dc22f87d49 100644 --- a/src/Avalonia.Base/Media/Color.cs +++ b/src/Avalonia.Base/Media/Color.cs @@ -166,7 +166,10 @@ namespace Avalonia.Media return true; } - if (s.Length > 5 && + // Note: The length checks are also an important optimization. + // The shortest possible CSS format is "rbg(0,0,0)", Length = 10. + + if (s.Length >= 10 && (s[0] == 'r' || s[0] == 'R') && (s[1] == 'g' || s[1] == 'G') && (s[2] == 'b' || s[2] == 'B') && @@ -175,7 +178,7 @@ namespace Avalonia.Media return true; } - if (s.Length > 5 && + if (s.Length >= 10 && (s[0] == 'h' || s[0] == 'H') && (s[1] == 's' || s[1] == 'S') && (s[2] == 'l' || s[2] == 'L') && @@ -185,7 +188,7 @@ namespace Avalonia.Media return true; } - if (s.Length > 5 && + if (s.Length >= 10 && (s[0] == 'h' || s[0] == 'H') && (s[1] == 's' || s[1] == 'S') && (s[2] == 'v' || s[2] == 'V') && @@ -229,7 +232,10 @@ namespace Avalonia.Media // At this point all parsing uses strings var str = s.ToString(); - if (s.Length > 5 && + // Note: The length checks are also an important optimization. + // The shortest possible CSS format is "rbg(0,0,0)", Length = 10. + + if (s.Length >= 10 && (s[0] == 'r' || s[0] == 'R') && (s[1] == 'g' || s[1] == 'G') && (s[2] == 'b' || s[2] == 'B') && @@ -238,7 +244,7 @@ namespace Avalonia.Media return true; } - if (s.Length > 5 && + if (s.Length >= 10 && (s[0] == 'h' || s[0] == 'H') && (s[1] == 's' || s[1] == 'S') && (s[2] == 'l' || s[2] == 'L') && @@ -248,7 +254,7 @@ namespace Avalonia.Media return true; } - if (s.Length > 5 && + if (s.Length >= 10 && (s[0] == 'h' || s[0] == 'H') && (s[1] == 's' || s[1] == 'S') && (s[2] == 'v' || s[2] == 'V') && @@ -342,14 +348,14 @@ namespace Avalonia.Media return false; } - if (workingString.Length > 6 && + if (workingString.Length >= 11 && workingString.StartsWith("rgba(", StringComparison.OrdinalIgnoreCase) && workingString.EndsWith(")", StringComparison.Ordinal)) { workingString = workingString.Substring(5, workingString.Length - 6); } - if (workingString.Length > 5 && + if (workingString.Length >= 10 && workingString.StartsWith("rgb(", StringComparison.OrdinalIgnoreCase) && workingString.EndsWith(")", StringComparison.Ordinal)) { diff --git a/src/Avalonia.Base/Media/HslColor.cs b/src/Avalonia.Base/Media/HslColor.cs index e27a4f3106..622d3ac2b7 100644 --- a/src/Avalonia.Base/Media/HslColor.cs +++ b/src/Avalonia.Base/Media/HslColor.cs @@ -241,19 +241,26 @@ namespace Avalonia.Media return false; } - if (workingString.Length > 6 && + // Note: The length checks are also an important optimization. + // The shortest possible format is "hsl(0,0,0)", Length = 10. + + if (workingString.Length >= 11 && workingString.StartsWith("hsla(", StringComparison.OrdinalIgnoreCase) && workingString.EndsWith(")", StringComparison.Ordinal)) { workingString = workingString.Substring(5, workingString.Length - 6); } - if (workingString.Length > 5 && + if (workingString.Length >= 10 && workingString.StartsWith("hsl(", StringComparison.OrdinalIgnoreCase) && workingString.EndsWith(")", StringComparison.Ordinal)) { workingString = workingString.Substring(4, workingString.Length - 5); } + else + { + return false; + } string[] components = workingString.Split(','); diff --git a/src/Avalonia.Base/Media/HsvColor.cs b/src/Avalonia.Base/Media/HsvColor.cs index 164aeb1df1..1e718b5739 100644 --- a/src/Avalonia.Base/Media/HsvColor.cs +++ b/src/Avalonia.Base/Media/HsvColor.cs @@ -241,19 +241,26 @@ namespace Avalonia.Media return false; } - if (workingString.Length > 6 && + // Note: The length checks are also an important optimization. + // The shortest possible format is "hsv(0,0,0)", Length = 10. + + if (workingString.Length >= 11 && workingString.StartsWith("hsva(", StringComparison.OrdinalIgnoreCase) && workingString.EndsWith(")", StringComparison.Ordinal)) { workingString = workingString.Substring(5, workingString.Length - 6); } - if (workingString.Length > 5 && + if (workingString.Length >= 10 && workingString.StartsWith("hsv(", StringComparison.OrdinalIgnoreCase) && workingString.EndsWith(")", StringComparison.Ordinal)) { workingString = workingString.Substring(4, workingString.Length - 5); } + else + { + return false; + } string[] components = workingString.Split(','); From 02e7e8702ed3e9866701755edb0d4814da7ac88d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20Komosi=C5=84ski?= Date: Sat, 16 Apr 2022 01:24:32 +0200 Subject: [PATCH 27/38] Allow for parsing relative points during compile time. (#7987) * Allow for parsing relative points during compile time. * Revert extra target framework. --- src/Avalonia.Base/RelativePoint.cs | 15 +++++++++--- .../Avalonia.Build.Tasks.csproj | 3 +++ .../Properties/launchSettings.json | 10 ++++++++ .../AvaloniaXamlIlLanguageParseIntrinsics.cs | 23 +++++++++++++++++++ .../AvaloniaXamlIlWellKnownTypes.cs | 7 ++++++ 5 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 src/Avalonia.Build.Tasks/Properties/launchSettings.json diff --git a/src/Avalonia.Base/RelativePoint.cs b/src/Avalonia.Base/RelativePoint.cs index 4550dbd54b..e1fd0093b6 100644 --- a/src/Avalonia.Base/RelativePoint.cs +++ b/src/Avalonia.Base/RelativePoint.cs @@ -1,7 +1,8 @@ using System; using System.Globalization; - +#if !BUILDTASK using Avalonia.Animation.Animators; +#endif using Avalonia.Utilities; namespace Avalonia @@ -10,7 +11,10 @@ namespace Avalonia /// Defines the reference point units of an or /// . /// - public enum RelativeUnit +#if !BUILDTASK + public +#endif + enum RelativeUnit { /// /// The point is expressed as a fraction of the containing element's size. @@ -26,7 +30,10 @@ namespace Avalonia /// /// Defines a point that may be defined relative to a containing element. /// - public readonly struct RelativePoint : IEquatable +#if !BUILDTASK + public +#endif + readonly struct RelativePoint : IEquatable { /// /// A point at the top left of the containing element. @@ -49,7 +56,9 @@ namespace Avalonia static RelativePoint() { +#if !BUILDTASK Animation.Animation.RegisterAnimator(prop => typeof(RelativePoint).IsAssignableFrom(prop.PropertyType)); +#endif } /// diff --git a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj index 6267c74df9..e9b99c9aa8 100644 --- a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj +++ b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj @@ -95,6 +95,9 @@ Markup/%(RecursiveDir)%(FileName)%(Extension) + + Markup/%(RecursiveDir)%(FileName)%(Extension) + diff --git a/src/Avalonia.Build.Tasks/Properties/launchSettings.json b/src/Avalonia.Build.Tasks/Properties/launchSettings.json new file mode 100644 index 0000000000..e9f5af46d6 --- /dev/null +++ b/src/Avalonia.Build.Tasks/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "Compile Sandbox": { + "commandName": "Project", + "executablePath": "$(SolutionDir)\\src\\Avalonia.Build.Tasks\\bin\\Debug\\net6.0\\Avalonia.Build.Tasks.exe", + "commandLineArgs": "$(SolutionDir)\\samples\\Sandbox\\obj\\Debug\\net6.0\\Avalonia\\original.dll $(SolutionDir)\\samples\\Sandbox\\bin\\Debug\\net6.0\\Sandbox.dll.refs $(SolutionDir)\\out.dll" + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs index 88529ae3a0..d907bcbef9 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs @@ -160,6 +160,29 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions return true; } + if (type.Equals(types.RelativePoint)) + { + try + { + var relativePoint = RelativePoint.Parse(text); + + var relativePointTypeRef = new XamlAstClrTypeReference(node, types.RelativePoint, false); + + result = new XamlAstNewClrObjectNode(node, relativePointTypeRef, types.RelativePointFullConstructor, new List + { + new XamlConstantNode(node, types.XamlIlTypes.Double, relativePoint.Point.X), + new XamlConstantNode(node, types.XamlIlTypes.Double, relativePoint.Point.Y), + new XamlConstantNode(node, types.RelativeUnit, (int) relativePoint.Unit), + }); + + return true; + } + catch + { + throw new XamlX.XamlLoadException($"Unable to parse \"{text}\" as a relative point", node); + } + } + if (type.Equals(types.GridLength)) { try diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index 99072ace02..76f3cc071f 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -71,6 +71,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlConstructor MatrixFullConstructor { get; } public IXamlType CornerRadius { get; } public IXamlConstructor CornerRadiusFullConstructor { get; } + public IXamlType RelativeUnit { get; } + public IXamlType RelativePoint { get; } + public IXamlConstructor RelativePointFullConstructor { get; } public IXamlType GridLength { get; } public IXamlConstructor GridLengthConstructorValueType { get; } public IXamlType Color { get; } @@ -175,6 +178,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers (Matrix, MatrixFullConstructor) = GetNumericTypeInfo("Avalonia.Matrix", XamlIlTypes.Double, 6); (CornerRadius, CornerRadiusFullConstructor) = GetNumericTypeInfo("Avalonia.CornerRadius", XamlIlTypes.Double, 4); + RelativeUnit = cfg.TypeSystem.GetType("Avalonia.RelativeUnit"); + RelativePoint = cfg.TypeSystem.GetType("Avalonia.RelativePoint"); + RelativePointFullConstructor = RelativePoint.GetConstructor(new List { XamlIlTypes.Double, XamlIlTypes.Double, RelativeUnit }); + GridLength = cfg.TypeSystem.GetType("Avalonia.Controls.GridLength"); GridLengthConstructorValueType = GridLength.GetConstructor(new List { XamlIlTypes.Double, cfg.TypeSystem.GetType("Avalonia.Controls.GridUnitType") }); Color = cfg.TypeSystem.GetType("Avalonia.Media.Color"); From 024dd47943e2b3c2461be965e8c102ff7fa87339 Mon Sep 17 00:00:00 2001 From: robloo Date: Fri, 15 Apr 2022 19:39:05 -0400 Subject: [PATCH 28/38] Improve color comments --- src/Avalonia.Base/Media/Color.cs | 6 +++++ src/Avalonia.Base/Media/HslColor.cs | 38 ++++++++++++++++++++++++++--- src/Avalonia.Base/Media/HsvColor.cs | 38 ++++++++++++++++++++++++++--- 3 files changed, 74 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Base/Media/Color.cs b/src/Avalonia.Base/Media/Color.cs index dc22f87d49..208e359f80 100644 --- a/src/Avalonia.Base/Media/Color.cs +++ b/src/Avalonia.Base/Media/Color.cs @@ -277,6 +277,9 @@ namespace Avalonia.Media return false; } + /// + /// Parses the given span of characters representing a hex color value into a new . + /// private static bool TryParseHexFormat(ReadOnlySpan s, out Color color) { static bool TryParseCore(ReadOnlySpan input, ref Color color) @@ -331,6 +334,9 @@ namespace Avalonia.Media return TryParseCore(input, ref color); } + /// + /// Parses the given string representing a CSS color value into a new . + /// private static bool TryParseCssFormat(string s, out Color color) { color = default; diff --git a/src/Avalonia.Base/Media/HslColor.cs b/src/Avalonia.Base/Media/HslColor.cs index 622d3ac2b7..cd98c72e3f 100644 --- a/src/Avalonia.Base/Media/HslColor.cs +++ b/src/Avalonia.Base/Media/HslColor.cs @@ -12,6 +12,7 @@ namespace Avalonia.Media { /// /// Defines a color using the hue/saturation/lightness (HSL) model. + /// This uses a cylindrical-coordinate representation of a color. /// #if !BUILDTASK public @@ -98,24 +99,53 @@ namespace Avalonia.Media } /// - /// Gets the Alpha (transparency) component in the range from 0..1. + /// Gets the Alpha (transparency) component in the range from 0..1 (percentage). /// + /// + /// + /// 0 is fully transparent. + /// 1 is fully opaque. + /// + /// public double A { get; } /// - /// Gets the Hue component in the range from 0..360. + /// Gets the Hue component in the range from 0..360 (degrees). + /// This is the color's location, in degrees, on a color wheel/circle from 0 to 360. /// Note that 360 is equivalent to 0 and will be adjusted automatically. /// + /// + /// + /// 0/360 degrees is Red. + /// 60 degrees is Yellow. + /// 120 degrees is Green. + /// 180 degrees is Cyan. + /// 240 degrees is Blue. + /// 300 degrees is Magenta. + /// + /// public double H { get; } /// - /// Gets the Saturation component in the range from 0..1. + /// Gets the Saturation component in the range from 0..1 (percentage). /// + /// + /// + /// 0 is a shade of gray (no color). + /// 1 is the full color. + /// + /// public double S { get; } /// - /// Gets the Lightness component in the range from 0..1. + /// Gets the Lightness component in the range from 0..1 (percentage). /// + /// + /// + /// 0 is fully black. + /// 1 is fully white. + /// + /// public double L { get; } /// diff --git a/src/Avalonia.Base/Media/HsvColor.cs b/src/Avalonia.Base/Media/HsvColor.cs index 1e718b5739..1ef0bcc742 100644 --- a/src/Avalonia.Base/Media/HsvColor.cs +++ b/src/Avalonia.Base/Media/HsvColor.cs @@ -12,6 +12,7 @@ namespace Avalonia.Media { /// /// Defines a color using the hue/saturation/value (HSV) model. + /// This uses a cylindrical-coordinate representation of a color. /// #if !BUILDTASK public @@ -98,24 +99,53 @@ namespace Avalonia.Media } /// - /// Gets the Alpha (transparency) component in the range from 0..1. + /// Gets the Alpha (transparency) component in the range from 0..1 (percentage). /// + /// + /// + /// 0 is fully transparent. + /// 1 is fully opaque. + /// + /// public double A { get; } /// - /// Gets the Hue component in the range from 0..360. + /// Gets the Hue component in the range from 0..360 (degrees). + /// This is the color's location, in degrees, on a color wheel/circle from 0 to 360. /// Note that 360 is equivalent to 0 and will be adjusted automatically. /// + /// + /// + /// 0/360 degrees is Red. + /// 60 degrees is Yellow. + /// 120 degrees is Green. + /// 180 degrees is Cyan. + /// 240 degrees is Blue. + /// 300 degrees is Magenta. + /// + /// public double H { get; } /// - /// Gets the Saturation component in the range from 0..1. + /// Gets the Saturation component in the range from 0..1 (percentage). /// + /// + /// + /// 0 is a shade of gray (no color). + /// 1 is the full color. + /// + /// public double S { get; } /// - /// Gets the Value component in the range from 0..1. + /// Gets the Value (or Brightness/Intensity) component in the range from 0..1 (percentage). /// + /// + /// + /// 0 is fully black and shows no color. + /// 1 is the brightest and shows full color. + /// + /// public double V { get; } /// From a7dc94b3cde14fa5faa8b5d38f2a07341a7702c3 Mon Sep 17 00:00:00 2001 From: robloo Date: Fri, 15 Apr 2022 19:54:11 -0400 Subject: [PATCH 29/38] More strictly match color prefixes --- src/Avalonia.Base/Media/Color.cs | 12 +++++++++++- src/Avalonia.Base/Media/HslColor.cs | 10 ++++++++-- src/Avalonia.Base/Media/HsvColor.cs | 10 ++++++++-- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Base/Media/Color.cs b/src/Avalonia.Base/Media/Color.cs index 208e359f80..cb90404f6d 100644 --- a/src/Avalonia.Base/Media/Color.cs +++ b/src/Avalonia.Base/Media/Color.cs @@ -339,6 +339,8 @@ namespace Avalonia.Media /// private static bool TryParseCssFormat(string s, out Color color) { + bool prefixMatched = false; + color = default; if (s is null) @@ -359,13 +361,21 @@ namespace Avalonia.Media workingString.EndsWith(")", StringComparison.Ordinal)) { workingString = workingString.Substring(5, workingString.Length - 6); + prefixMatched = true; } - if (workingString.Length >= 10 && + if (prefixMatched == false && + workingString.Length >= 10 && workingString.StartsWith("rgb(", StringComparison.OrdinalIgnoreCase) && workingString.EndsWith(")", StringComparison.Ordinal)) { workingString = workingString.Substring(4, workingString.Length - 5); + prefixMatched = true; + } + + if (prefixMatched == false) + { + return false; } string[] components = workingString.Split(','); diff --git a/src/Avalonia.Base/Media/HslColor.cs b/src/Avalonia.Base/Media/HslColor.cs index cd98c72e3f..e8a4d6f94f 100644 --- a/src/Avalonia.Base/Media/HslColor.cs +++ b/src/Avalonia.Base/Media/HslColor.cs @@ -256,6 +256,8 @@ namespace Avalonia.Media /// True if parsing was successful; otherwise, false. public static bool TryParse(string s, out HslColor hslColor) { + bool prefixMatched = false; + hslColor = default; if (s is null) @@ -279,15 +281,19 @@ namespace Avalonia.Media workingString.EndsWith(")", StringComparison.Ordinal)) { workingString = workingString.Substring(5, workingString.Length - 6); + prefixMatched = true; } - if (workingString.Length >= 10 && + if (prefixMatched == false && + workingString.Length >= 10 && workingString.StartsWith("hsl(", StringComparison.OrdinalIgnoreCase) && workingString.EndsWith(")", StringComparison.Ordinal)) { workingString = workingString.Substring(4, workingString.Length - 5); + prefixMatched = true; } - else + + if (prefixMatched == false) { return false; } diff --git a/src/Avalonia.Base/Media/HsvColor.cs b/src/Avalonia.Base/Media/HsvColor.cs index 1ef0bcc742..924ef4778b 100644 --- a/src/Avalonia.Base/Media/HsvColor.cs +++ b/src/Avalonia.Base/Media/HsvColor.cs @@ -256,6 +256,8 @@ namespace Avalonia.Media /// True if parsing was successful; otherwise, false. public static bool TryParse(string s, out HsvColor hsvColor) { + bool prefixMatched = false; + hsvColor = default; if (s is null) @@ -279,15 +281,19 @@ namespace Avalonia.Media workingString.EndsWith(")", StringComparison.Ordinal)) { workingString = workingString.Substring(5, workingString.Length - 6); + prefixMatched = true; } - if (workingString.Length >= 10 && + if (prefixMatched == false && + workingString.Length >= 10 && workingString.StartsWith("hsv(", StringComparison.OrdinalIgnoreCase) && workingString.EndsWith(")", StringComparison.Ordinal)) { workingString = workingString.Substring(4, workingString.Length - 5); + prefixMatched = true; } - else + + if (prefixMatched == false) { return false; } From 26e219404cbb920849e818f8f5e8eb804b302dd0 Mon Sep 17 00:00:00 2001 From: robloo Date: Fri, 15 Apr 2022 21:37:23 -0400 Subject: [PATCH 30/38] Add TemplatePart attributes for DataGrid controls --- src/Avalonia.Controls.DataGrid/DataGrid.cs | 8 ++++++++ src/Avalonia.Controls.DataGrid/DataGridCell.cs | 1 + src/Avalonia.Controls.DataGrid/DataGridRow.cs | 5 +++++ src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs | 6 ++++++ src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs | 1 + 5 files changed, 21 insertions(+) diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index 9b67c9b096..aaac3f8f9c 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -32,6 +32,14 @@ namespace Avalonia.Controls /// /// Displays data in a customizable grid. /// + [TemplatePart(DATAGRID_elementBottomRightCornerHeaderName, typeof(IVisual))] + [TemplatePart(DATAGRID_elementColumnHeadersPresenterName, typeof(DataGridColumnHeadersPresenter))] + [TemplatePart(DATAGRID_elementFrozenColumnScrollBarSpacerName, typeof(Control))] + [TemplatePart(DATAGRID_elementHorizontalScrollbarName, typeof(ScrollBar))] + [TemplatePart(DATAGRID_elementRowsPresenterName, typeof(DataGridRowsPresenter))] + [TemplatePart(DATAGRID_elementTopLeftCornerHeaderName, typeof(ContentControl))] + [TemplatePart(DATAGRID_elementTopRightCornerHeaderName, typeof(ContentControl))] + [TemplatePart(DATAGRID_elementVerticalScrollbarName, typeof(ScrollBar))] [PseudoClasses(":invalid", ":empty-rows", ":empty-columns")] public partial class DataGrid : TemplatedControl { diff --git a/src/Avalonia.Controls.DataGrid/DataGridCell.cs b/src/Avalonia.Controls.DataGrid/DataGridCell.cs index e3f150f5c4..67183781d3 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridCell.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridCell.cs @@ -13,6 +13,7 @@ namespace Avalonia.Controls /// /// Represents an individual cell. /// + [TemplatePart(DATAGRIDCELL_elementRightGridLine, typeof(Rectangle))] [PseudoClasses(":selected", ":current", ":edited", ":invalid")] public class DataGridCell : ContentControl { diff --git a/src/Avalonia.Controls.DataGrid/DataGridRow.cs b/src/Avalonia.Controls.DataGrid/DataGridRow.cs index a6faec752d..db5d428942 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRow.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRow.cs @@ -21,6 +21,11 @@ namespace Avalonia.Controls /// /// Represents a row. /// + [TemplatePart(DATAGRIDROW_elementBottomGridLine, typeof(Rectangle))] + [TemplatePart(DATAGRIDROW_elementCells, typeof(DataGridCellsPresenter))] + [TemplatePart(DATAGRIDROW_elementDetails, typeof(DataGridDetailsPresenter))] + [TemplatePart(DATAGRIDROW_elementRoot, typeof(Panel))] + [TemplatePart(DATAGRIDROW_elementRowHeader, typeof(DataGridRowHeader))] [PseudoClasses(":selected", ":editing", ":invalid")] public class DataGridRow : TemplatedControl { diff --git a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs index 49ca23d34c..a3dfa44fc9 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs @@ -14,6 +14,12 @@ using System.Reactive.Linq; namespace Avalonia.Controls { + [TemplatePart(DATAGRIDROWGROUPHEADER_expanderButton, typeof(ToggleButton))] + [TemplatePart(DATAGRIDROWGROUPHEADER_indentSpacer, typeof(Control))] + [TemplatePart(DATAGRIDROWGROUPHEADER_itemCountElement, typeof(TextBlock))] + [TemplatePart(DATAGRIDROWGROUPHEADER_propertyNameElement, typeof(TextBlock))] + [TemplatePart(DataGridRow.DATAGRIDROW_elementRoot, typeof(Panel))] + [TemplatePart(DataGridRow.DATAGRIDROW_elementRowHeader, typeof(DataGridRowHeader))] [PseudoClasses(":pressed", ":current", ":expanded")] public class DataGridRowGroupHeader : TemplatedControl { diff --git a/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs index 510072174f..03299bbf35 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs @@ -13,6 +13,7 @@ namespace Avalonia.Controls.Primitives /// /// Represents an individual row header. /// + [TemplatePart(DATAGRIDROWHEADER_elementRootName, typeof(Control))] [PseudoClasses(":invalid", ":selected", ":editing", ":current")] public class DataGridRowHeader : ContentControl { From 18e7a11a023b5283373303030b353a2fed8bb1c9 Mon Sep 17 00:00:00 2001 From: Steven He Date: Sat, 16 Apr 2022 15:41:16 +0800 Subject: [PATCH 31/38] Implement IncrementalGenerator --- src/Avalonia.Base/Animation/Easings/Easing.cs | 11 +- .../IsExternalInit.cs | 14 ++ .../SubtypesFactoryGenerator.cs | 234 +++++++++--------- 3 files changed, 131 insertions(+), 128 deletions(-) create mode 100644 src/Avalonia.SourceGenerator/IsExternalInit.cs diff --git a/src/Avalonia.Base/Animation/Easings/Easing.cs b/src/Avalonia.Base/Animation/Easings/Easing.cs index 6dfc4c86b3..d4f817ccc3 100644 --- a/src/Avalonia.Base/Animation/Easings/Easing.cs +++ b/src/Avalonia.Base/Animation/Easings/Easing.cs @@ -37,14 +37,9 @@ namespace Avalonia.Animation.Easings return new SplineEasing(KeySpline.Parse(e, CultureInfo.InvariantCulture)); } - if (TryCreateEasingInstance(e, out var easing)) - { - return easing; - } - else - { - throw new FormatException($"Easing \"{e}\" was not found in {Namespace} namespace."); - } + return TryCreateEasingInstance(e, out var easing) + ? easing + : throw new FormatException($"Easing \"{e}\" was not found in {Namespace} namespace."); } } } diff --git a/src/Avalonia.SourceGenerator/IsExternalInit.cs b/src/Avalonia.SourceGenerator/IsExternalInit.cs new file mode 100644 index 0000000000..c6ddf762ad --- /dev/null +++ b/src/Avalonia.SourceGenerator/IsExternalInit.cs @@ -0,0 +1,14 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace System.Runtime.CompilerServices +{ + /// + /// Reserved to be used by the compiler for tracking metadata. + /// This class should not be used by developers in source code. + /// + [ExcludeFromCodeCoverage, DebuggerNonUserCode] + internal static class IsExternalInit + { + } +} diff --git a/src/Avalonia.SourceGenerator/SubtypesFactoryGenerator.cs b/src/Avalonia.SourceGenerator/SubtypesFactoryGenerator.cs index 7bc240853e..6ba1f6ac18 100644 --- a/src/Avalonia.SourceGenerator/SubtypesFactoryGenerator.cs +++ b/src/Avalonia.SourceGenerator/SubtypesFactoryGenerator.cs @@ -1,140 +1,48 @@ -using System.Collections.Generic; +using System; +using System.Collections.Immutable; using System.Linq; +using System.Threading; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace Avalonia.SourceGenerator { - internal class GenerateSubtypesSyntaxReceiver : ISyntaxReceiver + [Generator(LanguageNames.CSharp)] + public class SubtypesFactoryGenerator : IIncrementalGenerator { - public List<(MethodDeclarationSyntax, AttributeSyntax)> CandidateMethods { get; } = new(); - public List Types { get; } = new(); - - public void OnVisitSyntaxNode(SyntaxNode syntaxNode) - { - if (syntaxNode is MethodDeclarationSyntax declarationSyntax) - { - foreach (var attribute in declarationSyntax.AttributeLists.SelectMany(i => i.Attributes)) - { - CandidateMethods.Add((declarationSyntax, attribute)); - } - } - - if (syntaxNode is ClassDeclarationSyntax or StructDeclarationSyntax) - { - Types.Add(syntaxNode); - } - } - } - - [Generator] - internal class SubtypesFactoryGenerator : ISourceGenerator - { - private readonly GenerateSubtypesSyntaxReceiver _receiver = new(); + private record MethodTarget(IMethodSymbol Method, ITypeSymbol BaseType, string Namespace); private static readonly string s_attributeName = typeof(SubtypesFactoryAttribute).FullName; - public void Execute(GeneratorExecutionContext context) + private static bool IsSubtypeOf(ITypeSymbol type, ITypeSymbol baseType) { - var methods = new List<(IMethodSymbol, ITypeSymbol, string)>(); - - foreach (var (method, attribute) in _receiver.CandidateMethods) - { - var semanticModel = context.Compilation.GetSemanticModel(method.SyntaxTree); - var attributeTypeInfo = semanticModel.GetTypeInfo(attribute); - if (attributeTypeInfo.Type is null || - attributeTypeInfo.Type.ToString() != s_attributeName || - attribute.ArgumentList is null) - { - continue; - } - - var arguments = attribute.ArgumentList.Arguments; - if (arguments.Count != 2) - { - continue; - } - - if (arguments[0].Expression is not TypeOfExpressionSyntax typeOfExpr || - arguments[1].Expression is not LiteralExpressionSyntax and not IdentifierNameSyntax) - { - continue; - } - - var type = semanticModel.GetTypeInfo(typeOfExpr.Type); - var ns = semanticModel.GetConstantValue(arguments[1].Expression); - var methodDeclInfo = semanticModel.GetDeclaredSymbol(method); - - if (type.Type is not ITypeSymbol baseType || - ns.HasValue is false || - ns.Value is not string nsValue || - methodDeclInfo is not IMethodSymbol methodSymbol || - methodSymbol.Parameters.Length != 2 || - methodSymbol.Parameters[1].RefKind != RefKind.Out) - { - continue; - } - - methods.Add((methodSymbol, baseType, nsValue)); - } - - var types = new List(); - foreach (var type in _receiver.Types) - { - var semanticModel = context.Compilation.GetSemanticModel(type.SyntaxTree); - var decl = semanticModel.GetDeclaredSymbol(type); - if (decl is ITypeSymbol typeSymbol) - { - types.Add(typeSymbol); - } - } - - GenerateSubTypes(context, methods, types); + return type.BaseType is not null && (SymbolEqualityComparer.Default.Equals(type.BaseType, baseType) || IsSubtypeOf(type.BaseType, baseType)); } - private bool IsSubtypeOf(ITypeSymbol type, ITypeSymbol baseType) + private static void GenerateSubTypes(SourceProductionContext context, MethodTarget methodTarget, ImmutableArray types) { - if (type.BaseType is null) - { - return false; - } + var (method, baseType, @namespace) = methodTarget; + var candidateTypes = types.Where(i => IsSubtypeOf(i, baseType)).Where(i => $"{i.ContainingNamespace}.".StartsWith($"{@namespace}.")).ToArray(); + var type = method.ContainingType; + var isGeneric = type.TypeParameters.Length > 0; + var isClass = type.TypeKind == TypeKind.Class; - if (SymbolEqualityComparer.Default.Equals(type.BaseType, baseType)) + if (method.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax() is not MethodDeclarationSyntax methodDecl) { - return true; + return; } - return IsSubtypeOf(type.BaseType, baseType); - } + var parameters = new SeparatedSyntaxList().AddRange(methodDecl.ParameterList.Parameters.Select(i => i.WithAttributeLists(new SyntaxList()))); - private void GenerateSubTypes( - GeneratorExecutionContext context, - List<(IMethodSymbol Method, ITypeSymbol BaseType, string Namespace)> methods, - List types) - { - foreach (var (method, baseType, @namespace) in methods) - { - var candidateTypes = types.Where(i => IsSubtypeOf(i, baseType)).Where(i => $"{i.ContainingNamespace}.".StartsWith($"{@namespace}.")).ToArray(); - var type = method.ContainingType; - var isGeneric = type.TypeParameters.Length > 0; - var isClass = type.TypeKind == TypeKind.Class; - - if (method.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax() is not MethodDeclarationSyntax methodDecl) - { - continue; - } + var methodDeclText = methodDecl + .WithAttributeLists(new SyntaxList()) + .WithParameterList(methodDecl.ParameterList.WithParameters(parameters)) + .WithBody(null) + .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.None)) + .WithoutTrivia().ToString(); - var parameters = new SeparatedSyntaxList().AddRange(methodDecl.ParameterList.Parameters.Select(i => i.WithAttributeLists(new SyntaxList()))); - - var methodDeclText = methodDecl - .WithAttributeLists(new SyntaxList()) - .WithParameterList(methodDecl.ParameterList.WithParameters(parameters)) - .WithBody(null) - .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.None)) - .WithoutTrivia().ToString(); - - var typeDecl = $"partial {(isClass ? "class" : "struct")} {type.Name}{(isGeneric ? $"<{string.Join(", ", type.TypeParameters)}>" : "")}"; - var source = $@"using System; + var typeDecl = $"partial {(isClass ? "class" : "struct")} {type.Name}{(isGeneric ? $"<{string.Join(", ", type.TypeParameters)}>" : "")}"; + var source = $@"using System; using System.Collections.Generic; namespace {method.ContainingNamespace} @@ -155,13 +63,99 @@ namespace {method.ContainingNamespace} }} }}"; - context.AddSource($"{type}.{method.MetadataName}.gen.cs", source); + context.AddSource($"{type}.{method.MetadataName}.gen.cs", source); + } + + private static MethodTarget? PopulateMethodTargets(GeneratorSyntaxContext context, CancellationToken token) + { + token.ThrowIfCancellationRequested(); + if (context.Node is MethodDeclarationSyntax method) + { + var attributes = method.AttributeLists.SelectMany(i => i.Attributes); + var semanticModel = context.SemanticModel; + foreach (var attribute in attributes) + { + var attributeTypeInfo = semanticModel.GetTypeInfo(attribute); + if (attributeTypeInfo.Type is null || + attributeTypeInfo.Type.ToString() != s_attributeName || + attribute.ArgumentList is null) + { + continue; + } + + var arguments = attribute.ArgumentList.Arguments; + if (arguments.Count != 2) + { + continue; + } + + if (arguments[0].Expression is not TypeOfExpressionSyntax typeOfExpr || + arguments[1].Expression is not LiteralExpressionSyntax and not IdentifierNameSyntax) + { + continue; + } + + var type = semanticModel.GetTypeInfo(typeOfExpr.Type); + var ns = semanticModel.GetConstantValue(arguments[1].Expression); + var methodDeclInfo = semanticModel.GetDeclaredSymbol(method); + + if (type.Type is not ITypeSymbol baseType || + ns.HasValue is false || + ns.Value is not string nsValue || + methodDeclInfo is not IMethodSymbol methodSymbol || + methodSymbol.Parameters.Length != 2 || + methodSymbol.Parameters[1].RefKind != RefKind.Out) + { + continue; + } + + return new MethodTarget(methodSymbol, baseType, nsValue); + } } + + return null; } - public void Initialize(GeneratorInitializationContext context) + public void Initialize(IncrementalGeneratorInitializationContext context) { - context.RegisterForSyntaxNotifications(() => _receiver); + var typesProvider = context.SyntaxProvider.CreateSyntaxProvider( + static (syntaxNode, token) => + { + token.ThrowIfCancellationRequested(); + return syntaxNode is ClassDeclarationSyntax or StructDeclarationSyntax; + }, + static (syntaxContext, token) => + { + token.ThrowIfCancellationRequested(); + return syntaxContext.Node is ClassDeclarationSyntax or StructDeclarationSyntax && + syntaxContext.SemanticModel.GetDeclaredSymbol(syntaxContext.Node) is ITypeSymbol typeSymbol + ? typeSymbol : null; + }) + .SelectMany((type, token) => + { + token.ThrowIfCancellationRequested(); + return type is null ? Array.Empty() : new ITypeSymbol[] { type }; + }); + + var methodsProvider = context.SyntaxProvider.CreateSyntaxProvider( + static (syntaxNode, token) => + { + token.ThrowIfCancellationRequested(); + return syntaxNode is ClassDeclarationSyntax or StructDeclarationSyntax or MethodDeclarationSyntax { AttributeLists.Count: > 0 }; + }, PopulateMethodTargets) + .SelectMany((method, token) => + { + token.ThrowIfCancellationRequested(); + return method is null ? Array.Empty() : new MethodTarget[] { method }; + }); + + var generateContext = methodsProvider.Combine(typesProvider.Collect()); + + context.RegisterSourceOutput(generateContext, static (sourceContext, source) => + { + sourceContext.CancellationToken.ThrowIfCancellationRequested(); + GenerateSubTypes(sourceContext, source.Left, source.Right); + }); } } } From b4a5d2e88a51e52b9fadb3f1bfe1969d73ea8826 Mon Sep 17 00:00:00 2001 From: Steven He Date: Sat, 16 Apr 2022 15:45:25 +0800 Subject: [PATCH 32/38] Use record struct --- src/Avalonia.SourceGenerator/SubtypesFactoryGenerator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.SourceGenerator/SubtypesFactoryGenerator.cs b/src/Avalonia.SourceGenerator/SubtypesFactoryGenerator.cs index 6ba1f6ac18..dbdef5e089 100644 --- a/src/Avalonia.SourceGenerator/SubtypesFactoryGenerator.cs +++ b/src/Avalonia.SourceGenerator/SubtypesFactoryGenerator.cs @@ -11,7 +11,7 @@ namespace Avalonia.SourceGenerator [Generator(LanguageNames.CSharp)] public class SubtypesFactoryGenerator : IIncrementalGenerator { - private record MethodTarget(IMethodSymbol Method, ITypeSymbol BaseType, string Namespace); + private record struct MethodTarget(IMethodSymbol Method, ITypeSymbol BaseType, string Namespace); private static readonly string s_attributeName = typeof(SubtypesFactoryAttribute).FullName; private static bool IsSubtypeOf(ITypeSymbol type, ITypeSymbol baseType) @@ -146,7 +146,7 @@ namespace {method.ContainingNamespace} .SelectMany((method, token) => { token.ThrowIfCancellationRequested(); - return method is null ? Array.Empty() : new MethodTarget[] { method }; + return method is null ? Array.Empty() : new MethodTarget[] { method.Value }; }); var generateContext = methodsProvider.Combine(typesProvider.Collect()); From de771d6fc49cdd1b8d5043e4a6b0730ff5350698 Mon Sep 17 00:00:00 2001 From: Steven He Date: Sat, 16 Apr 2022 15:46:33 +0800 Subject: [PATCH 33/38] Remove unnecessary nodes in MethodTargets --- src/Avalonia.SourceGenerator/SubtypesFactoryGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.SourceGenerator/SubtypesFactoryGenerator.cs b/src/Avalonia.SourceGenerator/SubtypesFactoryGenerator.cs index dbdef5e089..10c18580b3 100644 --- a/src/Avalonia.SourceGenerator/SubtypesFactoryGenerator.cs +++ b/src/Avalonia.SourceGenerator/SubtypesFactoryGenerator.cs @@ -141,7 +141,7 @@ namespace {method.ContainingNamespace} static (syntaxNode, token) => { token.ThrowIfCancellationRequested(); - return syntaxNode is ClassDeclarationSyntax or StructDeclarationSyntax or MethodDeclarationSyntax { AttributeLists.Count: > 0 }; + return syntaxNode is MethodDeclarationSyntax { AttributeLists.Count: > 0 }; }, PopulateMethodTargets) .SelectMany((method, token) => { From d7a098b75e39c84db552aa620e4bd34d2d448758 Mon Sep 17 00:00:00 2001 From: Steven He Date: Sat, 16 Apr 2022 15:51:05 +0800 Subject: [PATCH 34/38] Avoid reparse source in codegen stage --- .../SubtypesFactoryGenerator.cs | 30 ++++++++----------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/src/Avalonia.SourceGenerator/SubtypesFactoryGenerator.cs b/src/Avalonia.SourceGenerator/SubtypesFactoryGenerator.cs index 10c18580b3..4fc9397e7a 100644 --- a/src/Avalonia.SourceGenerator/SubtypesFactoryGenerator.cs +++ b/src/Avalonia.SourceGenerator/SubtypesFactoryGenerator.cs @@ -11,7 +11,7 @@ namespace Avalonia.SourceGenerator [Generator(LanguageNames.CSharp)] public class SubtypesFactoryGenerator : IIncrementalGenerator { - private record struct MethodTarget(IMethodSymbol Method, ITypeSymbol BaseType, string Namespace); + private record struct MethodTarget(IMethodSymbol Method, string MethodDecl, ITypeSymbol BaseType, string Namespace); private static readonly string s_attributeName = typeof(SubtypesFactoryAttribute).FullName; private static bool IsSubtypeOf(ITypeSymbol type, ITypeSymbol baseType) @@ -21,26 +21,12 @@ namespace Avalonia.SourceGenerator private static void GenerateSubTypes(SourceProductionContext context, MethodTarget methodTarget, ImmutableArray types) { - var (method, baseType, @namespace) = methodTarget; + var (method, methodDecl, baseType, @namespace) = methodTarget; var candidateTypes = types.Where(i => IsSubtypeOf(i, baseType)).Where(i => $"{i.ContainingNamespace}.".StartsWith($"{@namespace}.")).ToArray(); var type = method.ContainingType; var isGeneric = type.TypeParameters.Length > 0; var isClass = type.TypeKind == TypeKind.Class; - if (method.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax() is not MethodDeclarationSyntax methodDecl) - { - return; - } - - var parameters = new SeparatedSyntaxList().AddRange(methodDecl.ParameterList.Parameters.Select(i => i.WithAttributeLists(new SyntaxList()))); - - var methodDeclText = methodDecl - .WithAttributeLists(new SyntaxList()) - .WithParameterList(methodDecl.ParameterList.WithParameters(parameters)) - .WithBody(null) - .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.None)) - .WithoutTrivia().ToString(); - var typeDecl = $"partial {(isClass ? "class" : "struct")} {type.Name}{(isGeneric ? $"<{string.Join(", ", type.TypeParameters)}>" : "")}"; var source = $@"using System; using System.Collections.Generic; @@ -49,7 +35,7 @@ namespace {method.ContainingNamespace} {{ {typeDecl} {{ - {methodDeclText} + {methodDecl} {{ var hasMatch = false; (hasMatch, {method.Parameters[1].Name}) = {method.Parameters[0].Name} switch @@ -109,7 +95,15 @@ namespace {method.ContainingNamespace} continue; } - return new MethodTarget(methodSymbol, baseType, nsValue); + var parameters = new SeparatedSyntaxList().AddRange(method.ParameterList.Parameters.Select(i => i.WithAttributeLists(new SyntaxList()))); + var methodDecl = method + .WithAttributeLists(new SyntaxList()) + .WithParameterList(method.ParameterList.WithParameters(parameters)) + .WithBody(null) + .WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.None)) + .WithoutTrivia().ToString(); + + return new MethodTarget(methodSymbol, methodDecl, baseType, nsValue); } } From d79854ec54df61a7377cd475e012771aa49a55f2 Mon Sep 17 00:00:00 2001 From: Steven He Date: Sat, 16 Apr 2022 21:38:51 +0800 Subject: [PATCH 35/38] Remove rd.xml --- .../ControlCatalog.NetCore/ControlCatalog.NetCore.csproj | 5 ++--- samples/ControlCatalog.NetCore/rd.xml | 7 ------- 2 files changed, 2 insertions(+), 10 deletions(-) delete mode 100644 samples/ControlCatalog.NetCore/rd.xml diff --git a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj index d1b657722c..2b45ac1508 100644 --- a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj +++ b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj @@ -6,7 +6,7 @@ true - + true https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet7/nuget/v3/index.json 7.0.0-* @@ -22,12 +22,11 @@ - + - diff --git a/samples/ControlCatalog.NetCore/rd.xml b/samples/ControlCatalog.NetCore/rd.xml deleted file mode 100644 index 27db7f34ca..0000000000 --- a/samples/ControlCatalog.NetCore/rd.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file From d5e049e70a6f9d8a3c6c0fdbda8686dfa82456ee Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 16 Apr 2022 17:41:38 +0300 Subject: [PATCH 36/38] Fixed path to DevAnalyzers.csproj Linux uses case-sensetive file systems, so MSBuild was unable to find the file --- build/DevAnalyzers.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/DevAnalyzers.props b/build/DevAnalyzers.props index 28959dbd47..14e4f6a563 100644 --- a/build/DevAnalyzers.props +++ b/build/DevAnalyzers.props @@ -1,6 +1,6 @@ - Date: Tue, 19 Apr 2022 19:22:58 +0200 Subject: [PATCH 37/38] 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 38/38] 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)]