From ba2747b897204d3c7389adbb7ddf97928ce1bf7d Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Thu, 7 Jul 2022 10:45:37 +0200 Subject: [PATCH 001/334] feat: StringBuilderCache --- src/Avalonia.Base/Avalonia.Base.csproj | 1 + src/Avalonia.Base/Input/KeyGesture.cs | 4 +- src/Avalonia.Base/Logging/TraceLogSink.cs | 6 +- src/Avalonia.Base/Media/BoxShadow.cs | 4 +- src/Avalonia.Base/Media/BoxShadows.cs | 4 +- .../Media/Fonts/FamilyNameCollection.cs | 4 +- src/Avalonia.Base/Media/HslColor.cs | 4 +- src/Avalonia.Base/Media/HsvColor.cs | 4 +- src/Avalonia.Base/StringBuilderCache.cs | 68 +++++++++++++++++++ src/Avalonia.Base/Styling/NthChildSelector.cs | 5 +- .../Styling/PropertyEqualsSelector.cs | 4 +- .../Styling/TypeNameAndClassSelector.cs | 4 +- .../Avalonia.Build.Tasks.csproj | 1 + .../Helpers/ColorHelper.cs | 4 +- src/Avalonia.Controls.DataGrid/DataGrid.cs | 8 +-- .../Converters/PlatformKeyGestureConverter.cs | 8 +-- .../Documents/InlineCollection.cs | 4 +- .../Diagnostics/VisualTreeDebug.cs | 2 +- .../Avalonia.Markup.Xaml.Loader.csproj | 3 + .../Avalonia.Win32/ClipboardFormats.cs | 4 +- .../Input/WindowsKeyboardDevice.cs | 4 +- src/Windows/Avalonia.Win32/OleDataObject.cs | 4 +- .../Avalonia.Designer.HostApp.csproj | 1 + 23 files changed, 115 insertions(+), 40 deletions(-) create mode 100644 src/Avalonia.Base/StringBuilderCache.cs diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj index a07e0e3667..0018d40f66 100644 --- a/src/Avalonia.Base/Avalonia.Base.csproj +++ b/src/Avalonia.Base/Avalonia.Base.csproj @@ -31,6 +31,7 @@ + diff --git a/src/Avalonia.Base/Input/KeyGesture.cs b/src/Avalonia.Base/Input/KeyGesture.cs index 3b7a828b86..2123886cb1 100644 --- a/src/Avalonia.Base/Input/KeyGesture.cs +++ b/src/Avalonia.Base/Input/KeyGesture.cs @@ -106,7 +106,7 @@ namespace Avalonia.Input public override string ToString() { - var s = new StringBuilder(); + var s = StringBuilderCache.Acquire(); static void Plus(StringBuilder s) { @@ -142,7 +142,7 @@ namespace Avalonia.Input Plus(s); s.Append(Key); - return s.ToString(); + return StringBuilderCache.GetStringAndRelease(s); } public bool Matches(KeyEventArgs keyEvent) => diff --git a/src/Avalonia.Base/Logging/TraceLogSink.cs b/src/Avalonia.Base/Logging/TraceLogSink.cs index 05e4b8bc5a..fc3897fade 100644 --- a/src/Avalonia.Base/Logging/TraceLogSink.cs +++ b/src/Avalonia.Base/Logging/TraceLogSink.cs @@ -46,7 +46,7 @@ namespace Avalonia.Logging object? source, object?[]? values) { - var result = new StringBuilder(template.Length); + var result = StringBuilderCache.Acquire(template.Length); var r = new CharacterReader(template.AsSpan()); var i = 0; @@ -89,7 +89,7 @@ namespace Avalonia.Logging result.Append(')'); } - return result.ToString(); + return StringBuilderCache.GetStringAndRelease(result); } private static string Format( @@ -98,7 +98,7 @@ namespace Avalonia.Logging object? source, object?[] v) { - var result = new StringBuilder(template.Length); + var result = StringBuilderCache.Acquire(template.Length); var r = new CharacterReader(template.AsSpan()); var i = 0; diff --git a/src/Avalonia.Base/Media/BoxShadow.cs b/src/Avalonia.Base/Media/BoxShadow.cs index b01f59f5f8..cc97d89cfc 100644 --- a/src/Avalonia.Base/Media/BoxShadow.cs +++ b/src/Avalonia.Base/Media/BoxShadow.cs @@ -80,7 +80,7 @@ namespace Avalonia.Media public override string ToString() { - var sb = new StringBuilder(); + var sb = StringBuilderCache.Acquire(); if (IsEmpty) { @@ -114,7 +114,7 @@ namespace Avalonia.Media sb.AppendFormat(" {0}", Color.ToString()); - return sb.ToString(); + return StringBuilderCache.GetStringAndRelease(sb); } public static unsafe BoxShadow Parse(string s) diff --git a/src/Avalonia.Base/Media/BoxShadows.cs b/src/Avalonia.Base/Media/BoxShadows.cs index 4614ea4e3c..44288d89cf 100644 --- a/src/Avalonia.Base/Media/BoxShadows.cs +++ b/src/Avalonia.Base/Media/BoxShadows.cs @@ -45,7 +45,7 @@ namespace Avalonia.Media public override string ToString() { - var sb = new StringBuilder(); + var sb = StringBuilderCache.Acquire(); if (Count == 0) { @@ -57,7 +57,7 @@ namespace Avalonia.Media sb.AppendFormat("{0} ", boxShadow.ToString()); } - return sb.ToString(); + return StringBuilderCache.GetStringAndRelease(sb); } diff --git a/src/Avalonia.Base/Media/Fonts/FamilyNameCollection.cs b/src/Avalonia.Base/Media/Fonts/FamilyNameCollection.cs index 99daaf2143..eb42f6443b 100644 --- a/src/Avalonia.Base/Media/Fonts/FamilyNameCollection.cs +++ b/src/Avalonia.Base/Media/Fonts/FamilyNameCollection.cs @@ -77,7 +77,7 @@ namespace Avalonia.Media.Fonts /// public override string ToString() { - var builder = new StringBuilder(); + var builder = StringBuilderCache.Acquire(); for (var index = 0; index < Names.Count; index++) { @@ -91,7 +91,7 @@ namespace Avalonia.Media.Fonts builder.Append(", "); } - return builder.ToString(); + return StringBuilderCache.GetStringAndRelease(builder); } /// diff --git a/src/Avalonia.Base/Media/HslColor.cs b/src/Avalonia.Base/Media/HslColor.cs index e8a4d6f94f..485bb1db16 100644 --- a/src/Avalonia.Base/Media/HslColor.cs +++ b/src/Avalonia.Base/Media/HslColor.cs @@ -202,7 +202,7 @@ namespace Avalonia.Media /// public override string ToString() { - var sb = new StringBuilder(); + var sb = StringBuilderCache.Acquire(); // Use a format similar to CSS. However: // - To ensure precision is never lost, allow decimal places. @@ -225,7 +225,7 @@ namespace Avalonia.Media sb.Append(A.ToString(CultureInfo.InvariantCulture)); sb.Append(')'); - return sb.ToString(); + return StringBuilderCache.GetStringAndRelease(sb); } /// diff --git a/src/Avalonia.Base/Media/HsvColor.cs b/src/Avalonia.Base/Media/HsvColor.cs index 924ef4778b..512e57ae07 100644 --- a/src/Avalonia.Base/Media/HsvColor.cs +++ b/src/Avalonia.Base/Media/HsvColor.cs @@ -202,7 +202,7 @@ namespace Avalonia.Media /// public override string ToString() { - var sb = new StringBuilder(); + var sb = StringBuilderCache.Acquire(); // Use a format similar to CSS. However: // - To ensure precision is never lost, allow decimal places. @@ -225,7 +225,7 @@ namespace Avalonia.Media sb.Append(A.ToString(CultureInfo.InvariantCulture)); sb.Append(')'); - return sb.ToString(); + return StringBuilderCache.GetStringAndRelease(sb); } /// diff --git a/src/Avalonia.Base/StringBuilderCache.cs b/src/Avalonia.Base/StringBuilderCache.cs new file mode 100644 index 0000000000..060d76090a --- /dev/null +++ b/src/Avalonia.Base/StringBuilderCache.cs @@ -0,0 +1,68 @@ +// This file is imported from dotnet/runtime +// Source Link: https://github.com/dotnet/runtime/blob/e63d21947e734db2da5093510a6636b5b7fb45b5/src/libraries/Common/src/System/Text/StringBuilderCache.cs +// Commit: a9c5ead on Feb 10, 2021, https://github.com/dotnet/runtime/commit/a9c5eadd951dcba73167f72cc624eb790573663a +// +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Text; + +namespace Avalonia; + +// Provide a cached reusable instance of stringbuilder per thread. +internal static class StringBuilderCache +{ + // The value 360 was chosen in discussion with performance experts as a compromise between using + // as little memory per thread as possible and still covering a large part of short-lived + // StringBuilder creations on the startup path of VS designers. + internal const int MaxBuilderSize = 360; + private const int DefaultCapacity = 16; // == StringBuilder.DefaultCapacity + + // WARNING: We allow diagnostic tools to directly inspect this member (t_cachedInstance). + // See https://github.com/dotnet/corert/blob/master/Documentation/design-docs/diagnostics/diagnostics-tools-contract.md for more details. + // Please do not change the type, the name, or the semantic usage of this member without understanding the implication for tools. + // Get in touch with the diagnostics team if you have questions. + [ThreadStatic] + private static StringBuilder? t_cachedInstance; + + /// Get a StringBuilder for the specified capacity. + /// If a StringBuilder of an appropriate size is cached, it will be returned and the cache emptied. + public static StringBuilder Acquire(int capacity = DefaultCapacity) + { + if (capacity <= MaxBuilderSize) + { + StringBuilder? sb = t_cachedInstance; + if (sb != null) + { + // Avoid stringbuilder block fragmentation by getting a new StringBuilder + // when the requested size is larger than the current capacity + if (capacity <= sb.Capacity) + { + t_cachedInstance = null; + sb.Clear(); + return sb; + } + } + } + + return new StringBuilder(capacity); + } + + /// Place the specified builder in the cache if it is not too big. + public static void Release(StringBuilder sb) + { + if (sb.Capacity <= MaxBuilderSize) + { + t_cachedInstance = sb; + } + } + + /// ToString() the stringbuilder, Release it to the cache, and return the resulting string. + public static string GetStringAndRelease(StringBuilder sb) + { + string result = sb.ToString(); + Release(sb); + return result; + } +} diff --git a/src/Avalonia.Base/Styling/NthChildSelector.cs b/src/Avalonia.Base/Styling/NthChildSelector.cs index 047bf434da..a7af27f4bf 100644 --- a/src/Avalonia.Base/Styling/NthChildSelector.cs +++ b/src/Avalonia.Base/Styling/NthChildSelector.cs @@ -110,7 +110,8 @@ namespace Avalonia.Styling public override string ToString() { var expectedCapacity = NthLastChildSelectorName.Length + 8; - var stringBuilder = new StringBuilder(_previous?.ToString(), expectedCapacity); + var stringBuilder = StringBuilderCache.Acquire(expectedCapacity); + stringBuilder.Append(_previous?.ToString()); stringBuilder.Append(':'); stringBuilder.Append(_reversed ? NthLastChildSelectorName : NthChildSelectorName); @@ -140,7 +141,7 @@ namespace Avalonia.Styling stringBuilder.Append(')'); - return stringBuilder.ToString(); + return StringBuilderCache.GetStringAndRelease(stringBuilder); } } } diff --git a/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs b/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs index 7a37daf087..6663ed8887 100644 --- a/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs +++ b/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs @@ -42,7 +42,7 @@ namespace Avalonia.Styling { if (_selectorString == null) { - var builder = new StringBuilder(); + var builder = StringBuilderCache.Acquire(); if (_previous != null) { @@ -67,7 +67,7 @@ namespace Avalonia.Styling builder.Append(_value ?? string.Empty); builder.Append(']'); - _selectorString = builder.ToString(); + _selectorString = StringBuilderCache.GetStringAndRelease(builder); } return _selectorString; diff --git a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs index 24d5d6bbbf..5f004e91df 100644 --- a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs +++ b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs @@ -144,7 +144,7 @@ namespace Avalonia.Styling private string BuildSelectorString() { - var builder = new StringBuilder(); + var builder = StringBuilderCache.Acquire(); if (_previous != null) { @@ -184,7 +184,7 @@ namespace Avalonia.Styling } } - return builder.ToString(); + return StringBuilderCache.GetStringAndRelease(builder); } } } diff --git a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj index a801d338c3..1d717d5694 100644 --- a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj +++ b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj @@ -50,6 +50,7 @@ Markup/%(RecursiveDir)%(FileName)%(Extension) + Markup/%(RecursiveDir)%(FileName)%(Extension) diff --git a/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs b/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs index 32a898ee71..38fa58e7bb 100644 --- a/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs +++ b/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs @@ -109,7 +109,7 @@ namespace Avalonia.Controls.Primitives // Cache results for next time as well if (closestKnownColor != KnownColor.None) { - StringBuilder sb = new StringBuilder(); + var sb = StringBuilderCache.Acquire(); string name = closestKnownColor.ToString(); // Add spaces converting PascalCase to human-readable names @@ -124,7 +124,7 @@ namespace Avalonia.Controls.Primitives sb.Append(name[i]); } - string displayName = sb.ToString(); + string displayName = StringBuilderCache.GetStringAndRelease(sb); lock (cacheMutex) { diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index d42468f47e..554b1c371b 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -5990,7 +5990,7 @@ namespace Avalonia.Controls /// The formatted string. private string FormatClipboardContent(DataGridRowClipboardEventArgs e) { - var text = new StringBuilder(); + var text = StringBuilderCache.Acquire(); var clipboardRowContent = e.ClipboardRowContent; var numberOfItem = clipboardRowContent.Count; for (int cellIndex = 0; cellIndex < numberOfItem; cellIndex++) @@ -6007,7 +6007,7 @@ namespace Avalonia.Controls text.Append('\n'); } } - return text.ToString(); + return StringBuilderCache.GetStringAndRelease(text); } /// @@ -6022,7 +6022,7 @@ namespace Avalonia.Controls if (ctrl && !shift && !alt && ClipboardCopyMode != DataGridClipboardCopyMode.None && SelectedItems.Count > 0) { - StringBuilder textBuilder = new StringBuilder(); + var textBuilder = StringBuilderCache.Acquire(); if (ClipboardCopyMode == DataGridClipboardCopyMode.IncludeHeader) { @@ -6048,7 +6048,7 @@ namespace Avalonia.Controls textBuilder.Append(FormatClipboardContent(itemArgs)); } - string text = textBuilder.ToString(); + string text = StringBuilderCache.GetStringAndRelease(textBuilder); if (!string.IsNullOrEmpty(text)) { diff --git a/src/Avalonia.Controls/Converters/PlatformKeyGestureConverter.cs b/src/Avalonia.Controls/Converters/PlatformKeyGestureConverter.cs index 9a657cce68..47c2f94e18 100644 --- a/src/Avalonia.Controls/Converters/PlatformKeyGestureConverter.cs +++ b/src/Avalonia.Controls/Converters/PlatformKeyGestureConverter.cs @@ -62,7 +62,7 @@ namespace Avalonia.Controls.Converters private static string ToString(KeyGesture gesture, string meta) { - var s = new StringBuilder(); + var s = StringBuilderCache.Acquire(); static void Plus(StringBuilder s) { @@ -98,12 +98,12 @@ namespace Avalonia.Controls.Converters Plus(s); s.Append(ToString(gesture.Key)); - return s.ToString(); + return StringBuilderCache.GetStringAndRelease(s); } private static string ToOSXString(KeyGesture gesture) { - var s = new StringBuilder(); + var s = StringBuilderCache.Acquire(); if (gesture.KeyModifiers.HasAllFlags(KeyModifiers.Control)) { @@ -127,7 +127,7 @@ namespace Avalonia.Controls.Converters s.Append(ToOSXString(gesture.Key)); - return s.ToString(); + return StringBuilderCache.GetStringAndRelease(s); } private static string ToString(Key key) diff --git a/src/Avalonia.Controls/Documents/InlineCollection.cs b/src/Avalonia.Controls/Documents/InlineCollection.cs index dc688fc359..11225a87a1 100644 --- a/src/Avalonia.Controls/Documents/InlineCollection.cs +++ b/src/Avalonia.Controls/Documents/InlineCollection.cs @@ -78,14 +78,14 @@ namespace Avalonia.Controls.Documents return _text; } - var builder = new StringBuilder(); + var builder = StringBuilderCache.Acquire(); foreach (var inline in this) { inline.AppendText(builder); } - return builder.ToString(); + return StringBuilderCache.GetStringAndRelease(builder); } set { diff --git a/src/Avalonia.Diagnostics/Diagnostics/VisualTreeDebug.cs b/src/Avalonia.Diagnostics/Diagnostics/VisualTreeDebug.cs index 4adcd32302..d1f871d76f 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/VisualTreeDebug.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/VisualTreeDebug.cs @@ -10,7 +10,7 @@ namespace Avalonia.Diagnostics { public static string PrintVisualTree(IVisual visual) { - StringBuilder result = new StringBuilder(); + var result = new StringBuilder(); PrintVisualTree(visual, result, 0); return result.ToString(); } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/Avalonia.Markup.Xaml.Loader.csproj b/src/Markup/Avalonia.Markup.Xaml.Loader/Avalonia.Markup.Xaml.Loader.csproj index b89ea8399a..0b6b77e540 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/Avalonia.Markup.Xaml.Loader.csproj +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/Avalonia.Markup.Xaml.Loader.csproj @@ -7,6 +7,9 @@ $(DefineConstants);XAMLX_INTERNAL + + + diff --git a/src/Windows/Avalonia.Win32/ClipboardFormats.cs b/src/Windows/Avalonia.Win32/ClipboardFormats.cs index 7538dedfca..f5b8cd6b96 100644 --- a/src/Windows/Avalonia.Win32/ClipboardFormats.cs +++ b/src/Windows/Avalonia.Win32/ClipboardFormats.cs @@ -35,9 +35,9 @@ namespace Avalonia.Win32 private static string QueryFormatName(ushort format) { - StringBuilder sb = new StringBuilder(MAX_FORMAT_NAME_LENGTH); + var sb = StringBuilderCache.Acquire(MAX_FORMAT_NAME_LENGTH); if (UnmanagedMethods.GetClipboardFormatName(format, sb, sb.Capacity) > 0) - return sb.ToString(); + return StringBuilderCache.GetStringAndRelease(sb); return null; } diff --git a/src/Windows/Avalonia.Win32/Input/WindowsKeyboardDevice.cs b/src/Windows/Avalonia.Win32/Input/WindowsKeyboardDevice.cs index 1258bb0109..878011b5aa 100644 --- a/src/Windows/Avalonia.Win32/Input/WindowsKeyboardDevice.cs +++ b/src/Windows/Avalonia.Win32/Input/WindowsKeyboardDevice.cs @@ -49,7 +49,7 @@ namespace Avalonia.Win32.Input public string StringFromVirtualKey(uint virtualKey) { - StringBuilder result = new StringBuilder(256); + var result = StringBuilderCache.Acquire(256); int length = UnmanagedMethods.ToUnicode( virtualKey, 0, @@ -57,7 +57,7 @@ namespace Avalonia.Win32.Input result, 256, 0); - return result.ToString(); + return StringBuilderCache.GetStringAndRelease(result); } private void UpdateKeyStates() diff --git a/src/Windows/Avalonia.Win32/OleDataObject.cs b/src/Windows/Avalonia.Win32/OleDataObject.cs index ba17177473..837b21e34f 100644 --- a/src/Windows/Avalonia.Win32/OleDataObject.cs +++ b/src/Windows/Avalonia.Win32/OleDataObject.cs @@ -103,11 +103,11 @@ namespace Avalonia.Win32 for (int i = 0; i < fileCount; i++) { int pathLen = UnmanagedMethods.DragQueryFile(hGlobal, i, null, 0); - StringBuilder sb = new StringBuilder(pathLen+1); + var sb = StringBuilderCache.Acquire(pathLen+1); if (UnmanagedMethods.DragQueryFile(hGlobal, i, sb, sb.Capacity) == pathLen) { - files.Add(sb.ToString()); + files.Add(StringBuilderCache.GetStringAndRelease(sb)); } } } diff --git a/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj b/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj index 1cf68c1605..3dfef234a9 100644 --- a/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj +++ b/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj @@ -16,6 +16,7 @@ + From 94ecd84c8f24d210f537c495e044ded266f1147b Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Mon, 18 Jul 2022 11:25:13 +0200 Subject: [PATCH 002/334] feat(LibInputBackend): minimal linux boot with no input --- .../Input/LibInput/LibInputBackend.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs index 15d42789d4..6e26794b24 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs @@ -1,16 +1,15 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using System.IO; using System.Threading; using Avalonia.Input; using Avalonia.Input.Raw; -using Avalonia.Threading; using static Avalonia.LinuxFramebuffer.Input.LibInput.LibInputNativeUnsafeMethods; namespace Avalonia.LinuxFramebuffer.Input.LibInput { public class LibInputBackend : IInputBackend { + private const string LibInput = nameof(Logging.LogArea.X11Platform) + "/" + nameof(LibInput); private IScreenInfoProvider _screen; private IInputRoot _inputRoot; private readonly Queue _inputThreadActions = new Queue(); @@ -29,15 +28,21 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput new Thread(()=>InputThread(ctx)).Start(); } - - private unsafe void InputThread(IntPtr ctx) { var fd = libinput_get_fd(ctx); var timeval = stackalloc IntPtr[2]; - + if (!Directory.Exists("/dev/input")) + { + if (Logging.Logger.IsEnabled(Logging.LogEventLevel.Warning,LibInput)) + { + Logging.Logger.TryGet(Logging.LogEventLevel.Warning, LibInput) + ?.Log(this, "Not connect any input device."); + } + return; + } foreach (var f in Directory.GetFiles("/dev/input", "event*")) libinput_path_add_device(ctx, f); while (true) From 01e7bd2a523c2f119d1ccace9975de10e103c5d0 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Mon, 1 Aug 2022 09:51:58 +0200 Subject: [PATCH 003/334] fix: move StringBuilderCache to Avalonia.Utilities --- src/Avalonia.Base/Media/BoxShadows.cs | 2 +- src/Avalonia.Base/Styling/NthChildSelector.cs | 2 +- src/Avalonia.Base/Styling/PropertyEqualsSelector.cs | 2 +- src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs | 2 +- src/Avalonia.Base/{ => Utilities}/StringBuilderCache.cs | 2 +- src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj | 2 +- src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs | 2 +- src/Avalonia.Controls/Converters/PlatformKeyGestureConverter.cs | 1 + src/Avalonia.Controls/Documents/InlineCollection.cs | 2 +- src/Windows/Avalonia.Win32/ClipboardFormats.cs | 2 +- src/Windows/Avalonia.Win32/Input/WindowsKeyboardDevice.cs | 2 +- src/Windows/Avalonia.Win32/OleDataObject.cs | 2 +- .../Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj | 2 +- 13 files changed, 13 insertions(+), 12 deletions(-) rename src/Avalonia.Base/{ => Utilities}/StringBuilderCache.cs (98%) diff --git a/src/Avalonia.Base/Media/BoxShadows.cs b/src/Avalonia.Base/Media/BoxShadows.cs index 44288d89cf..ab2694389f 100644 --- a/src/Avalonia.Base/Media/BoxShadows.cs +++ b/src/Avalonia.Base/Media/BoxShadows.cs @@ -1,7 +1,7 @@ using System; using System.ComponentModel; -using System.Text; using Avalonia.Animation.Animators; +using Avalonia.Utilities; namespace Avalonia.Media { diff --git a/src/Avalonia.Base/Styling/NthChildSelector.cs b/src/Avalonia.Base/Styling/NthChildSelector.cs index a7ba79696e..c872a40ad4 100644 --- a/src/Avalonia.Base/Styling/NthChildSelector.cs +++ b/src/Avalonia.Base/Styling/NthChildSelector.cs @@ -1,8 +1,8 @@ #nullable enable using System; -using System.Text; using Avalonia.LogicalTree; using Avalonia.Styling.Activators; +using Avalonia.Utilities; namespace Avalonia.Styling { diff --git a/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs b/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs index 8922b939f7..e98ff3f9c9 100644 --- a/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs +++ b/src/Avalonia.Base/Styling/PropertyEqualsSelector.cs @@ -1,8 +1,8 @@ using System; using System.ComponentModel; using System.Globalization; -using System.Text; using Avalonia.Styling.Activators; +using Avalonia.Utilities; #nullable enable diff --git a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs index 698d96d0aa..1833f0d133 100644 --- a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs +++ b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; -using System.Text; using Avalonia.Controls; using Avalonia.Styling.Activators; +using Avalonia.Utilities; #nullable enable diff --git a/src/Avalonia.Base/StringBuilderCache.cs b/src/Avalonia.Base/Utilities/StringBuilderCache.cs similarity index 98% rename from src/Avalonia.Base/StringBuilderCache.cs rename to src/Avalonia.Base/Utilities/StringBuilderCache.cs index 060d76090a..be8b24c848 100644 --- a/src/Avalonia.Base/StringBuilderCache.cs +++ b/src/Avalonia.Base/Utilities/StringBuilderCache.cs @@ -8,7 +8,7 @@ using System; using System.Text; -namespace Avalonia; +namespace Avalonia.Utilities; // Provide a cached reusable instance of stringbuilder per thread. internal static class StringBuilderCache diff --git a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj index a5e38a86a2..7e1cb76911 100644 --- a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj +++ b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj @@ -50,7 +50,7 @@ Markup/%(RecursiveDir)%(FileName)%(Extension) - + Markup/%(RecursiveDir)%(FileName)%(Extension) diff --git a/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs b/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs index 68aa30ea7a..c1a03b1b77 100644 --- a/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs +++ b/src/Avalonia.Controls.ColorPicker/Helpers/ColorHelper.cs @@ -2,7 +2,7 @@ using System.Globalization; using System.Collections.Generic; using Avalonia.Media; -using System.Text; +using Avalonia.Utilities; namespace Avalonia.Controls.Primitives { diff --git a/src/Avalonia.Controls/Converters/PlatformKeyGestureConverter.cs b/src/Avalonia.Controls/Converters/PlatformKeyGestureConverter.cs index 47c2f94e18..0fa43809ac 100644 --- a/src/Avalonia.Controls/Converters/PlatformKeyGestureConverter.cs +++ b/src/Avalonia.Controls/Converters/PlatformKeyGestureConverter.cs @@ -4,6 +4,7 @@ using System.Runtime.InteropServices; using System.Text; using Avalonia.Data.Converters; using Avalonia.Input; +using Avalonia.Utilities; namespace Avalonia.Controls.Converters { diff --git a/src/Avalonia.Controls/Documents/InlineCollection.cs b/src/Avalonia.Controls/Documents/InlineCollection.cs index 81ccff4cbd..190373169b 100644 --- a/src/Avalonia.Controls/Documents/InlineCollection.cs +++ b/src/Avalonia.Controls/Documents/InlineCollection.cs @@ -1,8 +1,8 @@ using System; -using System.Text; using Avalonia.Collections; using Avalonia.LogicalTree; using Avalonia.Metadata; +using Avalonia.Utilities; namespace Avalonia.Controls.Documents { diff --git a/src/Windows/Avalonia.Win32/ClipboardFormats.cs b/src/Windows/Avalonia.Win32/ClipboardFormats.cs index f5b8cd6b96..7bd7765f8c 100644 --- a/src/Windows/Avalonia.Win32/ClipboardFormats.cs +++ b/src/Windows/Avalonia.Win32/ClipboardFormats.cs @@ -2,9 +2,9 @@ using System.Collections.Generic; using System.ComponentModel; using System.Linq; -using System.Text; using Avalonia.Input; using Avalonia.Win32.Interop; +using Avalonia.Utilities; namespace Avalonia.Win32 { diff --git a/src/Windows/Avalonia.Win32/Input/WindowsKeyboardDevice.cs b/src/Windows/Avalonia.Win32/Input/WindowsKeyboardDevice.cs index 878011b5aa..7e1e22579b 100644 --- a/src/Windows/Avalonia.Win32/Input/WindowsKeyboardDevice.cs +++ b/src/Windows/Avalonia.Win32/Input/WindowsKeyboardDevice.cs @@ -1,6 +1,6 @@ -using System.Text; using Avalonia.Controls; using Avalonia.Input; +using Avalonia.Utilities; using Avalonia.Win32.Interop; namespace Avalonia.Win32.Input diff --git a/src/Windows/Avalonia.Win32/OleDataObject.cs b/src/Windows/Avalonia.Win32/OleDataObject.cs index 837b21e34f..f7345b3ff7 100644 --- a/src/Windows/Avalonia.Win32/OleDataObject.cs +++ b/src/Windows/Avalonia.Win32/OleDataObject.cs @@ -6,9 +6,9 @@ using System.Linq; using System.Runtime.InteropServices; using System.Runtime.InteropServices.ComTypes; using System.Runtime.Serialization.Formatters.Binary; -using System.Text; using Avalonia.Input; using Avalonia.MicroCom; +using Avalonia.Utilities; using Avalonia.Win32.Interop; using IDataObject = Avalonia.Input.IDataObject; diff --git a/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj b/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj index 3dfef234a9..3293066dd1 100644 --- a/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj +++ b/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj @@ -16,7 +16,7 @@ - + From 3fb4e926430f275c1fd7ac03644b35a7fe0fcb79 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Mon, 1 Aug 2022 10:00:24 +0200 Subject: [PATCH 004/334] fix: PrintVisualTree using StringBuilderCache --- src/Avalonia.Base/Avalonia.Base.csproj | 1 + src/Avalonia.Diagnostics/Diagnostics/VisualTreeDebug.cs | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj index f722330db6..ede79c2943 100644 --- a/src/Avalonia.Base/Avalonia.Base.csproj +++ b/src/Avalonia.Base/Avalonia.Base.csproj @@ -37,6 +37,7 @@ + diff --git a/src/Avalonia.Diagnostics/Diagnostics/VisualTreeDebug.cs b/src/Avalonia.Diagnostics/Diagnostics/VisualTreeDebug.cs index d1f871d76f..13c8e070e8 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/VisualTreeDebug.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/VisualTreeDebug.cs @@ -2,6 +2,7 @@ using System; using System.Text; using Avalonia.Controls; using Avalonia.Data; +using Avalonia.Utilities; using Avalonia.VisualTree; namespace Avalonia.Diagnostics @@ -10,9 +11,9 @@ namespace Avalonia.Diagnostics { public static string PrintVisualTree(IVisual visual) { - var result = new StringBuilder(); + var result = StringBuilderCache.Acquire(); PrintVisualTree(visual, result, 0); - return result.ToString(); + return StringBuilderCache.GetStringAndRelease(result); } private static void PrintVisualTree(IVisual visual, StringBuilder builder, int indent) From 3db08330124ecee997b129dca0e77dbffa173f56 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Mon, 1 Aug 2022 10:20:13 +0200 Subject: [PATCH 005/334] fix: Avalonia.Markup.Xaml.Loader --- .../Avalonia.Markup.Xaml.Loader.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/Avalonia.Markup.Xaml.Loader.csproj b/src/Markup/Avalonia.Markup.Xaml.Loader/Avalonia.Markup.Xaml.Loader.csproj index 0b6b77e540..ce931b9c14 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/Avalonia.Markup.Xaml.Loader.csproj +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/Avalonia.Markup.Xaml.Loader.csproj @@ -8,7 +8,7 @@ - + From ef7ecb7c065719349f9c38b29001c29005f4d86f Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Tue, 2 Aug 2022 10:35:53 +0200 Subject: [PATCH 006/334] Revert "feat(LibInputBackend): minimal linux boot with no input" This reverts commit 94ecd84c8f24d210f537c495e044ded266f1147b. --- .../Input/LibInput/LibInputBackend.cs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs index 6e26794b24..15d42789d4 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs @@ -1,15 +1,16 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.IO; using System.Threading; using Avalonia.Input; using Avalonia.Input.Raw; +using Avalonia.Threading; using static Avalonia.LinuxFramebuffer.Input.LibInput.LibInputNativeUnsafeMethods; namespace Avalonia.LinuxFramebuffer.Input.LibInput { public class LibInputBackend : IInputBackend { - private const string LibInput = nameof(Logging.LogArea.X11Platform) + "/" + nameof(LibInput); private IScreenInfoProvider _screen; private IInputRoot _inputRoot; private readonly Queue _inputThreadActions = new Queue(); @@ -28,21 +29,15 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput new Thread(()=>InputThread(ctx)).Start(); } + + private unsafe void InputThread(IntPtr ctx) { var fd = libinput_get_fd(ctx); var timeval = stackalloc IntPtr[2]; - if (!Directory.Exists("/dev/input")) - { - if (Logging.Logger.IsEnabled(Logging.LogEventLevel.Warning,LibInput)) - { - Logging.Logger.TryGet(Logging.LogEventLevel.Warning, LibInput) - ?.Log(this, "Not connect any input device."); - } - return; - } + foreach (var f in Directory.GetFiles("/dev/input", "event*")) libinput_path_add_device(ctx, f); while (true) From 197f514ae7fbb94849ce048c28434de4f1f2aee0 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Tue, 2 Aug 2022 10:47:11 +0200 Subject: [PATCH 007/334] feat(X11): NullInputBackend --- .../Input/LibInput/LibInputBackend.cs | 6 ------ .../Input/NullInput/NullInputBackend.cs | 17 +++++++++++++++++ 2 files changed, 17 insertions(+), 6 deletions(-) create mode 100644 src/Linux/Avalonia.LinuxFramebuffer/Input/NullInput/NullInputBackend.cs diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs index 15d42789d4..702ae3f8e5 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs @@ -1,11 +1,9 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using System.IO; using System.Threading; using Avalonia.Input; using Avalonia.Input.Raw; -using Avalonia.Threading; using static Avalonia.LinuxFramebuffer.Input.LibInput.LibInputNativeUnsafeMethods; namespace Avalonia.LinuxFramebuffer.Input.LibInput { @@ -29,8 +27,6 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput new Thread(()=>InputThread(ctx)).Start(); } - - private unsafe void InputThread(IntPtr ctx) { var fd = libinput_get_fd(ctx); @@ -143,8 +139,6 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput } } - - public void Initialize(IScreenInfoProvider screen, Action onInput) { diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/NullInput/NullInputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/NullInput/NullInputBackend.cs new file mode 100644 index 0000000000..551c0995a2 --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/NullInput/NullInputBackend.cs @@ -0,0 +1,17 @@ +using System; +using Avalonia.Input; +using Avalonia.Input.Raw; + +namespace Avalonia.LinuxFramebuffer.Input.NullInput; + +internal class NullInputBackend : IInputBackend +{ + + public void Initialize(IScreenInfoProvider screen, Action onInput) + { + } + + public void SetInputRoot(IInputRoot root) + { + } +} From 46185b6a6eb78a887567dc483c5a1351fab61e30 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Tue, 2 Aug 2022 10:59:33 +0200 Subject: [PATCH 008/334] feat: add overload thath accept IInputBackend to Linux AppBuilder --- .../LinuxFramebufferPlatform.cs | 41 +++++++++---------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs index a642766809..c819407cc9 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs @@ -15,7 +15,6 @@ using Avalonia.LinuxFramebuffer.Output; using Avalonia.OpenGL; using Avalonia.Platform; using Avalonia.Rendering; -using Avalonia.Threading; using JetBrains.Annotations; namespace Avalonia.LinuxFramebuffer @@ -37,9 +36,9 @@ namespace Avalonia.LinuxFramebuffer Threading = new InternalPlatformThreadingInterface(); if (_fb is IGlOutputBackend gl) AvaloniaLocator.CurrentMutable.Bind().ToConstant(gl.PlatformOpenGlInterface); - + var opts = AvaloniaLocator.Current.GetService() ?? new LinuxFramebufferPlatformOptions(); - + AvaloniaLocator.CurrentMutable .Bind().ToConstant(Threading) .Bind().ToConstant(new DefaultRenderTimer(opts.Fps)) @@ -50,12 +49,12 @@ namespace Avalonia.LinuxFramebuffer .Bind().ToSingleton(); } - - internal static LinuxFramebufferLifetime Initialize(T builder, IOutputBackend outputBackend) where T : AppBuilderBase, new() + + internal static LinuxFramebufferLifetime Initialize(T builder, IOutputBackend outputBackend, IInputBackend inputBackend) where T : AppBuilderBase, new() { var platform = new LinuxFramebufferPlatform(outputBackend); builder.UseSkia().UseWindowingSubsystem(platform.Initialize, "fbdev"); - return new LinuxFramebufferLifetime(platform._fb); + return new LinuxFramebufferLifetime(platform._fb, inputBackend); } } @@ -71,13 +70,13 @@ namespace Avalonia.LinuxFramebuffer { _fb = fb; } - + public LinuxFramebufferLifetime(IOutputBackend fb, IInputBackend input) { _fb = fb; _inputBackend = input; } - + public Control MainView { get => (Control)_topLevel?.Content; @@ -117,7 +116,7 @@ namespace Avalonia.LinuxFramebuffer { Startup?.Invoke(this, new ControlledApplicationLifetimeStartupEventArgs(args)); } - + public void Shutdown(int exitCode) { ExitCode = exitCode; @@ -131,22 +130,22 @@ namespace Avalonia.LinuxFramebuffer public static class LinuxFramebufferPlatformExtensions { - public static int StartLinuxFbDev(this T builder, string[] args, string fbdev = null, double scaling = 1) + public static int StartLinuxFbDev(this T builder, string[] args, string fbdev = null, double scaling = 1, IInputBackend inputBackend = default) where T : AppBuilderBase, new() => - StartLinuxDirect(builder, args, new FbdevOutput(fileName: fbdev, format: null) { Scaling = scaling }); - public static int StartLinuxFbDev(this T builder, string[] args, string fbdev, PixelFormat? format, double scaling) + StartLinuxDirect(builder, args, new FbdevOutput(fileName: fbdev, format: null) { Scaling = scaling }, inputBackend); + public static int StartLinuxFbDev(this T builder, string[] args, string fbdev, PixelFormat? format, double scaling, IInputBackend inputBackend = default) where T : AppBuilderBase, new() => - StartLinuxDirect(builder, args, new FbdevOutput(fileName: fbdev, format: format) { Scaling = scaling }); - - public static int StartLinuxDrm(this T builder, string[] args, string card = null, double scaling = 1) - where T : AppBuilderBase, new() => StartLinuxDirect(builder, args, new DrmOutput(card) {Scaling = scaling}); - public static int StartLinuxDrm(this T builder, string[] args, string card = null, bool connectorsForceProbe = false, [CanBeNull] DrmOutputOptions options = null) - where T : AppBuilderBase, new() => StartLinuxDirect(builder, args, new DrmOutput(card, connectorsForceProbe, options)); - - public static int StartLinuxDirect(this T builder, string[] args, IOutputBackend backend) + StartLinuxDirect(builder, args, new FbdevOutput(fileName: fbdev, format: format) { Scaling = scaling }, inputBackend); + + public static int StartLinuxDrm(this T builder, string[] args, string card = null, double scaling = 1, IInputBackend inputBackend = default) + where T : AppBuilderBase, new() => StartLinuxDirect(builder, args, new DrmOutput(card) { Scaling = scaling }, inputBackend); + public static int StartLinuxDrm(this T builder, string[] args, string card = null, bool connectorsForceProbe = false, [CanBeNull] DrmOutputOptions options = null, IInputBackend inputBackend = default) + where T : AppBuilderBase, new() => StartLinuxDirect(builder, args, new DrmOutput(card, connectorsForceProbe, options), inputBackend); + + public static int StartLinuxDirect(this T builder, string[] args, IOutputBackend outputBackend, IInputBackend inputBackend = default) where T : AppBuilderBase, new() { - var lifetime = LinuxFramebufferPlatform.Initialize(builder, backend); + var lifetime = LinuxFramebufferPlatform.Initialize(builder, outputBackend, inputBackend); builder.SetupWithLifetime(lifetime); lifetime.Start(args); builder.Instance.Run(lifetime.Token); From 64c94ca3415e3c5f9961c62eb6ca090ee7ab5c0a Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Wed, 3 Aug 2022 09:24:17 +0200 Subject: [PATCH 009/334] fix: NullInputBackend visibility --- .../Input/NullInput/NullInputBackend.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/NullInput/NullInputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/NullInput/NullInputBackend.cs index 551c0995a2..05dd0195b1 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Input/NullInput/NullInputBackend.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/NullInput/NullInputBackend.cs @@ -4,9 +4,8 @@ using Avalonia.Input.Raw; namespace Avalonia.LinuxFramebuffer.Input.NullInput; -internal class NullInputBackend : IInputBackend +public class NullInputBackend : IInputBackend { - public void Initialize(IScreenInfoProvider screen, Action onInput) { } From 75870f751eef9beb95ea81a4cf3b31faf7fd6159 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 3 Aug 2022 15:30:01 +0200 Subject: [PATCH 010/334] Rendering part of PreeditText --- samples/Sandbox/MainWindow.axaml | 5 ++ samples/Sandbox/MainWindow.axaml.cs | 1 + .../Presenters/TextPresenter.cs | 57 ++++++++++++++++++- .../TextBoxTextInputMethodClient.cs | 25 ++++++-- 4 files changed, 80 insertions(+), 8 deletions(-) diff --git a/samples/Sandbox/MainWindow.axaml b/samples/Sandbox/MainWindow.axaml index 6929f192c7..20d7a53a11 100644 --- a/samples/Sandbox/MainWindow.axaml +++ b/samples/Sandbox/MainWindow.axaml @@ -1,4 +1,9 @@ + + + + + diff --git a/samples/Sandbox/MainWindow.axaml.cs b/samples/Sandbox/MainWindow.axaml.cs index 3d54036d29..23d45edf6a 100644 --- a/samples/Sandbox/MainWindow.axaml.cs +++ b/samples/Sandbox/MainWindow.axaml.cs @@ -1,5 +1,6 @@ using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.Presenters; using Avalonia.Markup.Xaml; using Avalonia.Win32.WinRT.Composition; diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index e540f58195..3f4d881735 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -54,6 +54,15 @@ namespace Avalonia.Controls.Presenters o => o.Text, (o, v) => o.Text = v); + /// + /// Defines the property. + /// + public static readonly DirectProperty PreeditTextProperty = + AvaloniaProperty.RegisterDirect( + nameof(PreeditText), + o => o.PreeditText, + (o, v) => o.PreeditText = v); + /// /// Defines the property. /// @@ -90,6 +99,7 @@ namespace Avalonia.Controls.Presenters private CharacterHit _lastCharacterHit; private Rect _caretBounds; private Point _navigationPosition; + private string? _preeditText; static TextPresenter() { @@ -124,6 +134,12 @@ namespace Avalonia.Controls.Presenters set => SetAndRaise(TextProperty, ref _text, value); } + public string? PreeditText + { + get => _preeditText; + set => SetAndRaise(PreeditTextProperty, ref _preeditText, value); + } + /// /// Gets or sets the font family. /// @@ -479,6 +495,18 @@ namespace Avalonia.Controls.Presenters } } + private string? GetText() + { + if (!string.IsNullOrEmpty(_preeditText)) + { + var text = _text?.Substring(0, _caretIndex) + _preeditText + _text?.Substring(_caretIndex); + + return text; + } + + return _text; + } + /// /// Creates the used to render the text. /// @@ -487,7 +515,7 @@ namespace Avalonia.Controls.Presenters { TextLayout result; - var text = Text; + var text = GetText(); var typeface = new Typeface(FontFamily, FontStyle, FontWeight); @@ -496,11 +524,11 @@ namespace Avalonia.Controls.Presenters var start = Math.Min(selectionStart, selectionEnd); var length = Math.Max(selectionStart, selectionEnd) - start; - IReadOnlyList>? textStyleOverrides = null; + List>? textStyleOverrides = null; if (length > 0 && SelectionForegroundBrush != null) { - textStyleOverrides = new[] + textStyleOverrides = new List> { new ValueSpan(start, length, new GenericTextRunProperties(typeface, FontSize, @@ -508,6 +536,28 @@ namespace Avalonia.Controls.Presenters }; } + var foreground = Foreground; + + if (!string.IsNullOrEmpty(_preeditText)) + { + var preeditHighlight = new ValueSpan(_caretIndex, _preeditText.Length, + new GenericTextRunProperties(typeface, FontSize, + foregroundBrush: foreground, + textDecorations: TextDecorations.Underline)); + + if (textStyleOverrides == null) + { + textStyleOverrides = new List> + { + preeditHighlight + }; + } + else + { + textStyleOverrides.Add(preeditHighlight); + } + } + if (PasswordChar != default(char) && !RevealPassword) { result = CreateTextLayoutInternal(_constraint, new string(PasswordChar, text?.Length ?? 0), typeface, @@ -814,6 +864,7 @@ namespace Avalonia.Controls.Presenters case nameof (FontStretch): case nameof (Text): + case nameof (PreeditText): case nameof (TextAlignment): case nameof (TextWrapping): diff --git a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs index 4d2ed03440..f9eb90611d 100644 --- a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs +++ b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs @@ -9,7 +9,7 @@ namespace Avalonia.Controls { internal class TextBoxTextInputMethodClient : ITextInputMethodClient { - private InputElement? _parent; + private TextBox? _parent; private TextPresenter? _presenter; public Rect CursorRectangle @@ -36,19 +36,29 @@ namespace Avalonia.Controls public event EventHandler? CursorRectangleChanged; public IVisual TextViewVisual => _presenter!; public event EventHandler? TextViewVisualChanged; - public bool SupportsPreedit => false; - public void SetPreeditText(string text) => throw new NotSupportedException(); + public bool SupportsPreedit => true; + public void SetPreeditText(string? text) + { + if(_presenter == null) + { + return; + } + + _presenter.PreeditText = text; + } public bool SupportsSurroundingText => false; - public TextInputMethodSurroundingText SurroundingText => throw new NotSupportedException(); + + public event EventHandler? SurroundingTextChanged { add { } remove { } } + public TextInputMethodSurroundingText SurroundingText => throw new NotSupportedException(); public string? TextBeforeCursor => null; public string? TextAfterCursor => null; private void OnCaretBoundsChanged(object? sender, EventArgs e) => CursorRectangleChanged?.Invoke(this, EventArgs.Empty); - public void SetPresenter(TextPresenter? presenter, InputElement? parent) + public void SetPresenter(TextPresenter? presenter, TextBox? parent) { _parent = parent; @@ -63,6 +73,11 @@ namespace Avalonia.Controls { _presenter.CaretBoundsChanged += OnCaretBoundsChanged; } + + if(presenter == null) + { + SetPreeditText(null); + } TextViewVisualChanged?.Invoke(this, EventArgs.Empty); CursorRectangleChanged?.Invoke(this, EventArgs.Empty); From e8719e018db82a6998900e61bea3f11470eade9f Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Thu, 4 Aug 2022 14:24:53 +0200 Subject: [PATCH 011/334] Fix GlyphRun.GetTralingWhitespaceLengthRightToLeft --- src/Avalonia.Base/Media/GlyphRun.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index cae7a8fe75..f207b3c636 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -786,14 +786,15 @@ namespace Avalonia.Media var clusterLength = 1; - while (i - 1 >= 0) + var j = i; + + while (j - 1 >= 0) { - var nextCluster = GlyphClusters[i - 1]; + var nextCluster = GlyphClusters[--j]; if (currentCluster == nextCluster) { - clusterLength++; - i--; + clusterLength++; continue; } @@ -808,7 +809,7 @@ namespace Avalonia.Media trailingWhitespaceLength += clusterLength; - glyphCount++; + glyphCount += clusterLength; } } From 0856cfaff985c15614b3bd647cf827b021a0444a Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Thu, 4 Aug 2022 14:25:12 +0200 Subject: [PATCH 012/334] Remove redundant comment --- src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index fa1ab6fd29..7495956cd2 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -266,10 +266,6 @@ namespace Avalonia.Media.TextFormatting { offset = Math.Max(0, currentPosition - shapedRun.Text.Start); } - //else - //{ - // offset = Math.Max(0, currentPosition - shapedRun.Text.Start + shapedRun.Text.Length); - //} characterHit = new CharacterHit(characterHit.FirstCharacterIndex + offset, characterHit.TrailingLength); From f009c316e208bc7ade2159b10f4501501732f65d Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Mon, 8 Aug 2022 17:24:15 +0200 Subject: [PATCH 013/334] Imm32 preedit --- .../Presenters/TextPresenter.cs | 144 ++++++------ src/Avalonia.Controls/TextBox.cs | 2 +- .../Avalonia.Win32/Input/Imm32CaretManager.cs | 8 +- .../Avalonia.Win32/Input/Imm32InputMethod.cs | 205 ++++++++++++------ .../Interop/UnmanagedMethods.cs | 71 +++++- .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 44 ++-- 6 files changed, 303 insertions(+), 171 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 3f4d881735..8b82530022 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -34,7 +34,7 @@ namespace Avalonia.Controls.Presenters public static readonly StyledProperty CaretBrushProperty = AvaloniaProperty.Register(nameof(CaretBrush)); - + public static readonly DirectProperty SelectionStartProperty = TextBox.SelectionStartProperty.AddOwner( o => o.SelectionStart, @@ -44,7 +44,7 @@ namespace Avalonia.Controls.Presenters TextBox.SelectionEndProperty.AddOwner( o => o.SelectionEnd, (o, v) => o.SelectionEnd = v); - + /// /// Defines the property. /// @@ -80,7 +80,7 @@ namespace Avalonia.Controls.Presenters /// public static readonly StyledProperty LineHeightProperty = TextBlock.LineHeightProperty.AddOwner(); - + /// /// Defines the property. /// @@ -114,7 +114,7 @@ namespace Avalonia.Controls.Presenters } public event EventHandler? CaretBoundsChanged; - + /// /// Gets or sets a brush used to paint the control's background. /// @@ -202,7 +202,7 @@ namespace Avalonia.Controls.Presenters get => GetValue(TextWrappingProperty); set => SetValue(TextWrappingProperty, value); } - + /// /// Gets or sets the line height. By default, this is set to , which determines the appropriate height automatically. /// @@ -232,11 +232,11 @@ namespace Avalonia.Controls.Presenters { return _textLayout; } - + _textLayout = CreateTextLayout(); UpdateCaret(_lastCharacterHit); - + return _textLayout; } } @@ -285,7 +285,7 @@ namespace Avalonia.Controls.Presenters get => GetValue(CaretBrushProperty); set => SetValue(CaretBrushProperty, value); } - + public int SelectionStart { get @@ -313,7 +313,7 @@ namespace Avalonia.Controls.Presenters SetAndRaise(SelectionEndProperty, ref _selectionEnd, value); } } - + protected override bool BypassFlowDirectionPolicies => true; /// @@ -330,9 +330,9 @@ namespace Avalonia.Controls.Presenters var foreground = Foreground; var maxWidth = MathUtilities.IsZero(constraint.Width) ? double.PositiveInfinity : constraint.Width; var maxHeight = MathUtilities.IsZero(constraint.Height) ? double.PositiveInfinity : constraint.Height; - + var textLayout = new TextLayout(text, typeface, FontSize, foreground, TextAlignment, - TextWrapping, maxWidth: maxWidth, maxHeight: maxHeight, textStyleOverrides: textStyleOverrides, + TextWrapping, maxWidth: maxWidth, maxHeight: maxHeight, textStyleOverrides: textStyleOverrides, flowDirection: FlowDirection, lineHeight: LineHeight); return textLayout; @@ -398,7 +398,7 @@ namespace Avalonia.Controls.Presenters { return; } - + var caretBrush = CaretBrush?.ToImmutable(); if (caretBrush is null) @@ -423,13 +423,13 @@ namespace Avalonia.Controls.Presenters context.DrawLine(new ImmutablePen(caretBrush), p1, p2); } - + private (Point, Point) GetCaretPoints() { var x = Math.Floor(_caretBounds.X) + 0.5; var y = Math.Floor(_caretBounds.Y) + 0.5; var b = Math.Ceiling(_caretBounds.Bottom) - 0.5; - + var caretIndex = _lastCharacterHit.FirstCharacterIndex + _lastCharacterHit.TrailingLength; var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(caretIndex, _lastCharacterHit.TrailingLength > 0); var textLine = TextLayout.TextLines[lineIndex]; @@ -438,7 +438,7 @@ namespace Avalonia.Controls.Presenters { x -= 1; } - + return (new Point(x, y), new Point(x, b)); } @@ -524,17 +524,7 @@ namespace Avalonia.Controls.Presenters var start = Math.Min(selectionStart, selectionEnd); var length = Math.Max(selectionStart, selectionEnd) - start; - List>? textStyleOverrides = null; - - if (length > 0 && SelectionForegroundBrush != null) - { - textStyleOverrides = new List> - { - new ValueSpan(start, length, - new GenericTextRunProperties(typeface, FontSize, - foregroundBrush: SelectionForegroundBrush)) - }; - } + IReadOnlyList>? textStyleOverrides = null; var foreground = Foreground; @@ -542,20 +532,26 @@ namespace Avalonia.Controls.Presenters { var preeditHighlight = new ValueSpan(_caretIndex, _preeditText.Length, new GenericTextRunProperties(typeface, FontSize, - foregroundBrush: foreground, - textDecorations: TextDecorations.Underline)); + foregroundBrush: Foreground, + textDecorations: TextDecorations.Underline, + backgroundBrush: SelectionBrush)); - if (textStyleOverrides == null) + textStyleOverrides = new[] + { + preeditHighlight + }; + } + else + { + if (length > 0 && SelectionForegroundBrush != null) { - textStyleOverrides = new List> + textStyleOverrides = new[] { - preeditHighlight + new ValueSpan(start, length, + new GenericTextRunProperties(typeface, FontSize, + foregroundBrush: SelectionForegroundBrush)) }; } - else - { - textStyleOverrides.Add(preeditHighlight); - } } if (PasswordChar != default(char) && !RevealPassword) @@ -574,7 +570,7 @@ namespace Avalonia.Controls.Presenters protected virtual void InvalidateTextLayout() { _textLayout = null; - + InvalidateMeasure(); } @@ -623,7 +619,7 @@ namespace Avalonia.Controls.Presenters private void CaretTimerTick(object? sender, EventArgs e) { _caretBlink = !_caretBlink; - + InvalidateVisual(); } @@ -633,7 +629,7 @@ namespace Avalonia.Controls.Presenters var textLine = TextLayout.TextLines[lineIndex]; var characterHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(textPosition)); - + var nextCaretCharacterHit = textLine.GetNextCaretCharacterHit(characterHit); if (nextCaretCharacterHit.FirstCharacterIndex <= textPosition) @@ -653,8 +649,8 @@ namespace Avalonia.Controls.Presenters _navigationPosition = _caretBounds.Position; CaretChanged(); - } - + } + public void MoveCaretToPoint(Point point) { var hit = TextLayout.HitTestPoint(point); @@ -685,7 +681,7 @@ namespace Avalonia.Controls.Presenters } var textLine = TextLayout.TextLines[lineIndex]; - + currentY += textLine.Height; } else @@ -701,9 +697,9 @@ namespace Avalonia.Controls.Presenters } var navigationPosition = _navigationPosition; - + MoveCaretToPoint(new Point(currentX, currentY)); - + _navigationPosition = navigationPosition.WithY(_caretBounds.Y); CaretChanged(); @@ -714,11 +710,11 @@ namespace Avalonia.Controls.Presenters if (Text is null) { return default; - } + } var characterHit = _lastCharacterHit; var caretIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; - + var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(caretIndex, false); if (lineIndex < 0) @@ -740,11 +736,11 @@ namespace Avalonia.Controls.Presenters { characterHit = new CharacterHit(caretIndex); } - + if (caretIndex >= Text.Length) { characterHit = new CharacterHit(Text.Length); - + break; } @@ -756,10 +752,10 @@ namespace Avalonia.Controls.Presenters if (caretIndex <= CaretIndex) { lineIndex++; - + continue; } - + break; } } @@ -786,7 +782,7 @@ namespace Avalonia.Controls.Presenters return characterHit; } - + public void MoveCaretHorizontal(LogicalDirection direction = LogicalDirection.Forward) { if (FlowDirection == FlowDirection.RightToLeft) @@ -808,9 +804,9 @@ namespace Avalonia.Controls.Presenters private void UpdateCaret(CharacterHit characterHit) { _lastCharacterHit = characterHit; - + var caretIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; - + var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(caretIndex, characterHit.TrailingLength > 0); var textLine = TextLayout.TextLines[lineIndex]; var distanceX = textLine.GetDistanceFromCharacterHit(characterHit); @@ -825,7 +821,7 @@ namespace Avalonia.Controls.Presenters } var caretBounds = new Rect(distanceX, distanceY, 0, textLine.Height); - + if (caretBounds != _caretBounds) { _caretBounds = caretBounds; @@ -846,40 +842,40 @@ namespace Avalonia.Controls.Presenters base.OnDetachedFromVisualTree(e); _caretTimer.Stop(); - + _caretTimer.Tick -= CaretTimerTick; } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); - + switch (change.Property.Name) { - case nameof (Foreground): - case nameof (FontSize): - case nameof (FontStyle): - case nameof (FontWeight): - case nameof (FontFamily): - case nameof (FontStretch): + case nameof(Foreground): + case nameof(FontSize): + case nameof(FontStyle): + case nameof(FontWeight): + case nameof(FontFamily): + case nameof(FontStretch): - case nameof (Text): - case nameof (PreeditText): - case nameof (TextAlignment): - case nameof (TextWrapping): + case nameof(Text): + case nameof(PreeditText): + case nameof(TextAlignment): + case nameof(TextWrapping): - case nameof (SelectionStart): - case nameof (SelectionEnd): - case nameof (SelectionForegroundBrush): + case nameof(SelectionStart): + case nameof(SelectionEnd): + case nameof(SelectionForegroundBrush): - case nameof (PasswordChar): - case nameof (RevealPassword): + case nameof(PasswordChar): + case nameof(RevealPassword): case nameof(FlowDirection): - { - InvalidateTextLayout(); - break; - } + { + InvalidateTextLayout(); + break; + } } } } diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 4c9e9327d4..20b11b3060 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -1142,7 +1142,7 @@ namespace Avalonia.Controls protected override void OnPointerPressed(PointerPressedEventArgs e) { - if (_presenter == null) + if (_presenter == null || !string.IsNullOrEmpty(_presenter.PreeditText)) { return; } diff --git a/src/Windows/Avalonia.Win32/Input/Imm32CaretManager.cs b/src/Windows/Avalonia.Win32/Input/Imm32CaretManager.cs index 38605efa22..ca52bf6e67 100644 --- a/src/Windows/Avalonia.Win32/Input/Imm32CaretManager.cs +++ b/src/Windows/Avalonia.Win32/Input/Imm32CaretManager.cs @@ -7,14 +7,11 @@ namespace Avalonia.Win32.Input { private bool _isCaretCreated; - public void TryCreate(int _langId, IntPtr hwnd) + public void TryCreate(IntPtr hwnd) { if (!_isCaretCreated) { - if (_langId == LANG_ZH || _langId == LANG_JA) - { - _isCaretCreated = CreateCaret(hwnd, IntPtr.Zero, 2, 10); - } + _isCaretCreated = CreateCaret(hwnd, IntPtr.Zero, 2, 2); } } @@ -31,6 +28,7 @@ namespace Avalonia.Win32.Input if (_isCaretCreated) { DestroyCaret(); + _isCaretCreated = false; } } diff --git a/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs b/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs index 3adefd965f..9affb4fd5f 100644 --- a/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs +++ b/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs @@ -1,4 +1,6 @@ using System; +using System.Runtime.InteropServices; +using System.Text; using Avalonia.Input.TextInput; using Avalonia.Threading; @@ -12,102 +14,125 @@ namespace Avalonia.Win32.Input class Imm32InputMethod : ITextInputMethodImpl { public IntPtr HWND { get; private set; } - private IntPtr _defaultImc; + private IntPtr _currentHimc; private WindowImpl _parent; - private bool _active; - private bool _showCompositionWindow; + private ITextInputMethodClient _client; + private Imm32CaretManager _caretManager = new(); - private bool _showCandidateList; + private ushort _langId; private const int _caretMargin = 1; - - public void SetLanguageAndWindow(WindowImpl parent, IntPtr hwnd, IntPtr HKL) + + public bool IsActive => _client != null; + + public bool IsComposing { get; set; } + + public bool ShowCompositionWindow => false; + + public void CreateCaret() + { + _caretManager.TryCreate(HWND); + } + + public void EnableImm() { - if (HWND != hwnd) + var himc = ImmGetContext(HWND); + + if(himc == IntPtr.Zero) + { + himc = ImmCreateContext(); + } + + if(himc != _currentHimc) { - _defaultImc = IntPtr.Zero; + if(_currentHimc != IntPtr.Zero) + { + DisableImm(); + } + + ImmAssociateContext(HWND, himc); + + ImmReleaseContext(HWND, himc); + + _currentHimc = himc; + + _caretManager.TryCreate(HWND); } + } + + public void DisableImm() + { + _caretManager.TryDestroy(); + + Reset(); + + ImmAssociateContext(HWND, IntPtr.Zero); + + _caretManager.TryDestroy(); + + _currentHimc = IntPtr.Zero; + } + + public void SetLanguageAndWindow(WindowImpl parent, IntPtr hwnd, IntPtr HKL) + { HWND = hwnd; + _parent = parent; - _active = false; - _langId = PRIMARYLANGID(LGID(HKL)); - _showCompositionWindow = true; - _showCandidateList = true; - IsComposing = false; + var langId= PRIMARYLANGID(LGID(HKL)); + + if(langId != _langId) + { + DisableImm(); + } + + _langId = langId; + + EnableImm(); } public void ClearLanguageAndWindow() { - if (HWND != IntPtr.Zero && _defaultImc != IntPtr.Zero) - { - ImmReleaseContext(HWND, _defaultImc); - } + DisableImm(); - _defaultImc = IntPtr.Zero; HWND = IntPtr.Zero; _parent = null; - _active = false; + _client = null; _langId = 0; - _showCompositionWindow = false; - _showCandidateList = false; IsComposing = false; } //Dependant on CurrentThread. When Avalonia will support Multiple Dispatchers - //every Dispatcher should have their own InputMethod. - public static Imm32InputMethod Current { get; } = new Imm32InputMethod(); + public static Imm32InputMethod Current { get; } = new Imm32InputMethod(); - private IntPtr DefaultImc + public void Reset() { - get + Dispatcher.UIThread.Post(() => { - if (_defaultImc == IntPtr.Zero && - HWND != IntPtr.Zero) - { - _defaultImc = ImmGetContext(HWND); - ImmReleaseContext(HWND, _defaultImc); - } + var himc = ImmGetContext(HWND); - if (_defaultImc == IntPtr.Zero) + if (IsComposing) { - _defaultImc = ImmCreateContext(); + ImmNotifyIME(himc, NI_COMPOSITIONSTR, CPS_COMPLETE, 0); + + IsComposing = false; } - return _defaultImc; - } - } - - public void Reset() - { - if (IsComposing) - { - Dispatcher.UIThread.Post(() => - { - ImmNotifyIME(DefaultImc, NI_COMPOSITIONSTR, CPS_COMPLETE, 0); - ImmReleaseContext(HWND, DefaultImc); - IsComposing = false; - }); - } + ImmReleaseContext(HWND, himc); + }); } public void SetClient(ITextInputMethodClient client) { - _active = client is { }; + _client = client; + Dispatcher.UIThread.Post(() => { - if (_active) + if (IsActive) { - if (DefaultImc != IntPtr.Zero) - { - _caretManager.TryCreate(_langId, HWND); - // Load the default IME context. - // NOTE(hbono) - // IMM ignores this call if the IME context is loaded. Therefore, we do - // not have to check whether or not the IME context is loaded. - ImmAssociateContext(HWND, _defaultImc); - } + EnableImm(); } else { @@ -116,14 +141,8 @@ namespace Avalonia.Win32.Input // mouse button and selected a password input while composing a text. // For this case, we have to complete the ongoing composition and // clean up the resources attached to this object BEFORE DISABLING THE IME. - if (IsComposing) - { - ImmNotifyIME(DefaultImc, NI_COMPOSITIONSTR, CPS_COMPLETE, 0); - ImmReleaseContext(HWND, DefaultImc); - IsComposing = false; - } - ImmAssociateContext(HWND, IntPtr.Zero); - _caretManager.TryDestroy(); + + DisableImm(); } }); } @@ -131,19 +150,23 @@ namespace Avalonia.Win32.Input public void SetCursorRect(Rect rect) { var focused = GetActiveWindow() == HWND; + if (!focused) { return; } + Dispatcher.UIThread.Post(() => { - IntPtr himc = DefaultImc; + var himc = ImmGetContext(HWND); + if (himc == IntPtr.Zero) { return; } MoveImeWindow(rect, himc); + ImmReleaseContext(HWND, himc); }); } @@ -157,8 +180,7 @@ namespace Avalonia.Win32.Input var s = _parent?.DesktopScaling ?? 1; var (x1, y1, x2, y2) = ((int) (p1.X * s), (int) (p1.Y * s), (int) (p2.X * s), (int) (p2.Y * s)); - if (!_showCompositionWindow && - _langId == LANG_ZH) + if (!ShowCompositionWindow && _langId == LANG_ZH) { // Chinese IMEs ignore function calls to ::ImmSetCandidateWindow() // when a user disables TSF (Text Service Framework) and CUAS (Cicero @@ -175,12 +197,13 @@ namespace Avalonia.Win32.Input dwStyle = CFS_CANDIDATEPOS, ptCurrentPos = new POINT {X = x2, Y = y2} }; + ImmSetCandidateWindow(himc, ref candidateForm); } - + _caretManager.TryMove(x2, y2); - if (_showCompositionWindow) + if (ShowCompositionWindow) { ConfigureCompositionWindow(x1, y1, himc, y2 - y1); // Don't need to set the position of candidate window. @@ -214,6 +237,7 @@ namespace Avalonia.Win32.Input ptCurrentPos = new POINT {X = x1, Y = y1}, rcArea = new RECT {left = x1, top = y1, right = x2, bottom = y2 + _caretMargin} }; + ImmSetCandidateWindow(himc, ref excludeRectangle); } @@ -224,6 +248,7 @@ namespace Avalonia.Win32.Input dwStyle = CFS_POINT, ptCurrentPos = new POINT {X = x1, Y = y1}, }; + ImmSetCompositionWindow(himc, ref compForm); var logFont = new LOGFONT() @@ -231,6 +256,7 @@ namespace Avalonia.Win32.Input lfHeight = height, lfQuality = 5 //CLEARTYPE_QUALITY }; + ImmSetCompositionFont(himc, ref logFont); } @@ -238,8 +264,43 @@ namespace Avalonia.Win32.Input { // we're skipping this. not usable on windows } + + public void CompositionChanged() + { + if (!IsComposing) + { + return; + } + + if(!IsActive || !_client.SupportsPreedit) + { + return; + } + + var composition = GetCompositionString(); + + _client.SetPreeditText(composition); + } - public bool IsComposing { get; set; } + private string GetCompositionString() + { + var himc = ImmGetContext(HWND); + + var length = ImmGetCompositionString(himc, GCS.GCS_COMPSTR, IntPtr.Zero, 0); + + var buffer = new byte[length]; + + unsafe + { + fixed (byte* bufferPtr = buffer) + { + var error = ImmGetCompositionString(himc, GCS.GCS_COMPSTR, (IntPtr)bufferPtr, (uint)length); + + return Encoding.Unicode.GetString(buffer, 0, buffer.Length); + } + } + + } ~Imm32InputMethod() { diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index ea01d5cbdf..9d1920498b 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1766,6 +1766,46 @@ namespace Avalonia.Win32.Interop [DllImport("user32.dll")] internal static extern int SetWindowCompositionAttribute(IntPtr hwnd, ref WindowCompositionAttributeData data); + [Flags] + public enum GCS : uint + { + /// Retrieve or update the attribute of the composition string. + GCS_COMPATTR = 0x0010, + + /// Retrieve or update clause information of the composition string. + GCS_COMPCLAUSE = 0x0020, + + /// Retrieve or update the attributes of the reading string of the current composition. + GCS_COMPREADATTR = 0x0002, + + /// Retrieve or update the clause information of the reading string of the composition string. + GCS_COMPREADCLAUSE = 0x0004, + + /// Retrieve or update the reading string of the current composition. + GCS_COMPREADSTR = 0x0001, + + /// Retrieve or update the current composition string. + GCS_COMPSTR = 0x0008, + + /// Retrieve or update the cursor position in composition string. + GCS_CURSORPOS = 0x0080, + + /// Retrieve or update the starting position of any changes in composition string. + GCS_DELTASTART = 0x0100, + + /// Retrieve or update clause information of the result string. + GCS_RESULTCLAUSE = 0x1000, + + /// Retrieve or update clause information of the reading string. + GCS_RESULTREADCLAUSE = 0x0400, + + /// Retrieve or update the reading string. + GCS_RESULTREADSTR = 0x0200, + + /// Retrieve or update the string of the composition result. + GCS_RESULTSTR = 0x0800, + } + [DllImport("imm32.dll", SetLastError = true)] public static extern IntPtr ImmGetContext(IntPtr hWnd); [DllImport("imm32.dll", SetLastError = true)] @@ -1788,6 +1828,29 @@ namespace Avalonia.Win32.Interop public static extern bool ImmSetCompositionWindow(IntPtr hIMC, ref COMPOSITIONFORM lpComp); [DllImport("imm32.dll")] public static extern bool ImmSetCompositionFont(IntPtr hIMC, ref LOGFONT lf); + + [DllImport("imm32.dll", SetLastError = false, CharSet = CharSet.Unicode)] + public static extern int ImmGetCompositionString(IntPtr hIMC, GCS dwIndex, [Out, Optional] IntPtr lpBuf, uint dwBufLen); + + public static string ImmGetCompositionString(IntPtr hIMC, GCS dwIndex) + { + int bufferLength = ImmGetCompositionString(hIMC, dwIndex, IntPtr.Zero, 0); + + if (bufferLength > 0) + { + var buffer = new byte[bufferLength]; + + fixed(byte* bufferPtr = buffer) + { + var error = ImmGetCompositionString(hIMC, dwIndex, (IntPtr)bufferPtr, (uint)bufferLength); + + return Marshal.PtrToStringUni((IntPtr)bufferPtr); + } + } + + return null; + } + [DllImport("imm32.dll")] public static extern bool ImmNotifyIME(IntPtr hIMC, int dwAction, int dwIndex, int dwValue); [DllImport("user32.dll")] @@ -1827,7 +1890,13 @@ namespace Avalonia.Win32.Interop public const int CFS_EXCLUDE = 0x0080; public const int CFS_POINT = 0x0002; public const int CFS_RECT = 0x0001; - public const uint ISC_SHOWUICOMPOSITIONWINDOW = 0x80000000; + + // lParam for WM_IME_SETCONTEXT + public const long ISC_SHOWUICANDIDATEWINDOW = 0x00000001; + public const long ISC_SHOWUICOMPOSITIONWINDOW = 0x80000000; + public const long ISC_SHOWUIGUIDELINE = 0x40000000; + public const long ISC_SHOWUIALLCANDIDATEWINDOW = 0x0000000F; + public const long ISC_SHOWUIALL = 0xC000000F; public const int NI_COMPOSITIONSTR = 21; public const int CPS_COMPLETE = 1; diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 4c7b9a0348..9022bc4179 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -527,22 +527,22 @@ namespace Avalonia.Win32 } case WindowsMessage.WM_PAINT: - { - using(NonPumpingSyncContext.Use()) - using (_rendererLock.Lock()) { - if (BeginPaint(_hwnd, out PAINTSTRUCT ps) != IntPtr.Zero) + using (NonPumpingSyncContext.Use()) + using (_rendererLock.Lock()) { - var f = RenderScaling; - var r = ps.rcPaint; - Paint?.Invoke(new Rect(r.left / f, r.top / f, (r.right - r.left) / f, - (r.bottom - r.top) / f)); - EndPaint(_hwnd, ref ps); + if (BeginPaint(_hwnd, out PAINTSTRUCT ps) != IntPtr.Zero) + { + var f = RenderScaling; + var r = ps.rcPaint; + Paint?.Invoke(new Rect(r.left / f, r.top / f, (r.right - r.left) / f, + (r.bottom - r.top) / f)); + EndPaint(_hwnd, ref ps); + } } - } - return IntPtr.Zero; - } + return IntPtr.Zero; + } case WindowsMessage.WM_ENTERSIZEMOVE: @@ -551,7 +551,7 @@ namespace Avalonia.Win32 case WindowsMessage.WM_SIZE: { - using(NonPumpingSyncContext.Use()) + using (NonPumpingSyncContext.Use()) using (_rendererLock.Lock()) { // Do nothing here, just block until the pending frame render is completed on the render thread @@ -651,13 +651,19 @@ namespace Avalonia.Win32 } case WindowsMessage.WM_IME_SETCONTEXT: { - // TODO if we implement preedit, disable the composition window: - // lParam = new IntPtr((int)(((uint)lParam.ToInt64()) & ~ISC_SHOWUICOMPOSITIONWINDOW)); + DefWindowProc(Hwnd, msg, wParam, (IntPtr)(lParam.ToInt64() & ~ISC_SHOWUICOMPOSITIONWINDOW)); + UpdateInputMethod(GetKeyboardLayout(0)); + + return IntPtr.Zero; + } + case WindowsMessage.WM_IME_COMPOSITION: + { + Imm32InputMethod.Current.CompositionChanged(); + break; } case WindowsMessage.WM_IME_CHAR: - case WindowsMessage.WM_IME_COMPOSITION: case WindowsMessage.WM_IME_COMPOSITIONFULL: case WindowsMessage.WM_IME_CONTROL: case WindowsMessage.WM_IME_KEYDOWN: @@ -667,6 +673,7 @@ namespace Avalonia.Win32 break; case WindowsMessage.WM_IME_STARTCOMPOSITION: Imm32InputMethod.Current.IsComposing = true; + return IntPtr.Zero; break; case WindowsMessage.WM_IME_ENDCOMPOSITION: Imm32InputMethod.Current.IsComposing = false; @@ -687,7 +694,7 @@ namespace Avalonia.Win32 return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam); #endif - if(shouldTakeFocus) + if (shouldTakeFocus) { SetFocus(_hwnd); } @@ -916,14 +923,15 @@ namespace Avalonia.Win32 { // note: for non-ime language, also create it so that emoji panel tracks cursor var langid = LGID(hkl); + if (langid == _langid && Imm32InputMethod.Current.HWND == Hwnd) { return; } + _langid = langid; Imm32InputMethod.Current.SetLanguageAndWindow(this, Hwnd, hkl); - } private static int ToInt32(IntPtr ptr) From 63539252a3cfd5152f1e89aaf05995f4d76c4342 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 9 Aug 2022 06:29:37 +0200 Subject: [PATCH 014/334] Cleanup --- samples/Sandbox/MainWindow.axaml | 6 +----- src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs | 1 - 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/samples/Sandbox/MainWindow.axaml b/samples/Sandbox/MainWindow.axaml index 20d7a53a11..43d93a9315 100644 --- a/samples/Sandbox/MainWindow.axaml +++ b/samples/Sandbox/MainWindow.axaml @@ -1,9 +1,5 @@ - - - - - + diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 9022bc4179..f8785371d9 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -674,7 +674,6 @@ namespace Avalonia.Win32 case WindowsMessage.WM_IME_STARTCOMPOSITION: Imm32InputMethod.Current.IsComposing = true; return IntPtr.Zero; - break; case WindowsMessage.WM_IME_ENDCOMPOSITION: Imm32InputMethod.Current.IsComposing = false; break; From 678620422df2e6bea1b9c7e3832a998559af2083 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 19 Aug 2022 15:59:51 +0200 Subject: [PATCH 015/334] More RTL hit testing fixes --- .../Media/TextFormatting/TextLineImpl.cs | 249 ++++++++---------- .../Media/TextFormatting/TextRunBounds.cs | 2 +- .../Documents/InlineCollection.cs | 14 +- .../Media/TextFormatting/TextLineTests.cs | 83 +++++- 4 files changed, 202 insertions(+), 146 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 7495956cd2..aba8008fb9 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -128,7 +128,7 @@ namespace Avalonia.Media.TextFormatting var collapsingProperties = collapsingPropertiesList[0]; - if(collapsingProperties is null) + if (collapsingProperties is null) { return this; } @@ -192,7 +192,7 @@ namespace Avalonia.Media.TextFormatting { var currentRun = _textRuns[i]; - if(currentRun is ShapedTextCharacters shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight) + if (currentRun is ShapedTextCharacters shapedRun && !shapedRun.ShapedBuffer.IsLeftToRight) { var rightToLeftIndex = i; currentPosition += currentRun.TextSourceLength; @@ -213,14 +213,14 @@ namespace Avalonia.Media.TextFormatting for (var j = i; i <= rightToLeftIndex; j++) { - if(j > _textRuns.Count - 1) + if (j > _textRuns.Count - 1) { break; } currentRun = _textRuns[j]; - if(currentDistance + currentRun.Size.Width <= distance) + if (currentDistance + currentRun.Size.Width <= distance) { currentDistance += currentRun.Size.Width; currentPosition -= currentRun.TextSourceLength; @@ -322,11 +322,11 @@ namespace Avalonia.Media.TextFormatting continue; } - + break; } - if(i > index) + if (i > index) { while (i >= index) { @@ -350,7 +350,7 @@ namespace Avalonia.Media.TextFormatting } } - if (currentPosition + currentRun.TextSourceLength >= characterIndex && + if (currentPosition + currentRun.TextSourceLength >= characterIndex && TryGetDistanceFromCharacterHit(currentRun, characterHit, currentPosition, remainingLength, flowDirection, out var distance, out _)) { return Math.Max(0, currentDistance + distance); @@ -530,6 +530,8 @@ namespace Avalonia.Media.TextFormatting double currentWidth = 0; var currentRect = Rect.Empty; + TextRunBounds lastRunBounds = default; + for (var index = 0; index < TextRuns.Count; index++) { if (TextRuns[index] is not DrawableTextRun currentRun) @@ -539,53 +541,93 @@ namespace Avalonia.Media.TextFormatting var characterLength = 0; var endX = startX; - var runWidth = 0.0; - TextRunBounds? currentRunBounds = null; var currentShapedRun = currentRun as ShapedTextCharacters; + TextRunBounds currentRunBounds; + + double combinedWidth; + + if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex) + { + startX += currentRun.Size.Width; + + currentPosition += currentRun.TextSourceLength; + + continue; + } + if (currentShapedRun != null && !currentShapedRun.ShapedBuffer.IsLeftToRight) { var rightToLeftIndex = index; - startX += currentShapedRun.Size.Width; + var rightToLeftWidth = currentShapedRun.Size.Width; - while (rightToLeftIndex + 1 <= _textRuns.Count - 1) + while (rightToLeftIndex + 1 <= _textRuns.Count - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextCharacters nextShapedRun) { - var nextShapedRun = _textRuns[rightToLeftIndex + 1] as ShapedTextCharacters; - if (nextShapedRun == null || nextShapedRun.ShapedBuffer.IsLeftToRight) { break; } - startX += nextShapedRun.Size.Width; - rightToLeftIndex++; + + rightToLeftWidth += nextShapedRun.Size.Width; + + if (currentPosition + nextShapedRun.TextSourceLength > firstTextSourceIndex + textLength) + { + break; + } + + currentShapedRun = nextShapedRun; } - if (TryGetTextRunBoundsRightToLeft(startX, firstTextSourceIndex, characterIndex, rightToLeftIndex, ref currentPosition, ref remainingLength, out currentRunBounds)) + startX = startX + rightToLeftWidth; + + currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength); + + remainingLength -= currentRunBounds.Length; + currentPosition = currentRunBounds.TextSourceCharacterIndex + currentRunBounds.Length; + endX = currentRunBounds.Rectangle.Right; + startX = currentRunBounds.Rectangle.Left; + + var rightToLeftRunBounds = new List { currentRunBounds }; + + for (int i = rightToLeftIndex - 1; i >= index; i--) { - startX = currentRunBounds!.Rectangle.Left; - endX = currentRunBounds.Rectangle.Right; + currentShapedRun = TextRuns[i] as ShapedTextCharacters; + + if(currentShapedRun == null) + { + continue; + } - runWidth = currentRunBounds.Rectangle.Width; + currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength); + + rightToLeftRunBounds.Insert(0, currentRunBounds); + + remainingLength -= currentRunBounds.Length; + startX = currentRunBounds.Rectangle.Left; + + currentPosition += currentRunBounds.Length; } + combinedWidth = endX - startX; + + currentRect = new Rect(startX, 0, combinedWidth, Height); + currentDirection = FlowDirection.RightToLeft; + + if (!MathUtilities.IsZero(combinedWidth)) + { + result.Add(new TextBounds(currentRect, currentDirection, rightToLeftRunBounds)); + } + + startX = endX; } else { if (currentShapedRun != null) { - if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex) - { - startX += currentRun.Size.Width; - - currentPosition += currentRun.TextSourceLength; - - continue; - } - var offset = Math.Max(0, firstTextSourceIndex - currentPosition); currentPosition += offset; @@ -661,43 +703,46 @@ namespace Avalonia.Media.TextFormatting characterLength = NewLineLength; } - runWidth = endX - startX; - currentRunBounds = new TextRunBounds(new Rect(startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); + combinedWidth = endX - startX; + + currentRunBounds = new TextRunBounds(new Rect(startX, 0, combinedWidth, Height), currentPosition, characterLength, currentRun); currentPosition += characterLength; remainingLength -= characterLength; - } - if (currentRunBounds != null && !MathUtilities.IsZero(runWidth) || NewLineLength > 0) - { - if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, startX)) + startX = endX; + + if (currentRunBounds.TextRun != null && !MathUtilities.IsZero(combinedWidth) || NewLineLength > 0) { - currentRect = currentRect.WithWidth(currentWidth + runWidth); + if (result.Count > 0 && lastDirection == currentDirection && MathUtilities.AreClose(currentRect.Left, lastRunBounds.Rectangle.Right)) + { + currentRect = currentRect.WithWidth(currentWidth + combinedWidth); - var textBounds = result[result.Count - 1]; + var textBounds = result[result.Count - 1]; - textBounds.Rectangle = currentRect; + textBounds.Rectangle = currentRect; - textBounds.TextRunBounds.Add(currentRunBounds!); - } - else - { - currentRect = currentRunBounds!.Rectangle; + textBounds.TextRunBounds.Add(currentRunBounds); + } + else + { + currentRect = currentRunBounds.Rectangle; - result.Add(new TextBounds(currentRect, currentDirection, new List { currentRunBounds })); + result.Add(new TextBounds(currentRect, currentDirection, new List { currentRunBounds })); + } } + + lastRunBounds = currentRunBounds; } - currentWidth += runWidth; - + currentWidth += combinedWidth; if (remainingLength <= 0 || currentPosition >= characterIndex) { break; } - startX = endX; lastDirection = currentDirection; } @@ -852,105 +897,45 @@ namespace Avalonia.Media.TextFormatting return result; } - private bool TryGetTextRunBoundsRightToLeft(double startX, int firstTextSourceIndex, int characterIndex, int runIndex, ref int currentPosition, ref int remainingLength, out TextRunBounds? textRunBounds) + private TextRunBounds GetRightToLeftTextRunBounds(ShapedTextCharacters currentRun, double endX, int firstTextSourceIndex, int characterIndex, int currentPosition, int remainingLength) { - textRunBounds = null; + var startX = endX; - for (var index = runIndex; index >= 0; index--) - { - if (TextRuns[index] is not DrawableTextRun currentRun) - { - continue; - } + var offset = Math.Max(0, firstTextSourceIndex - currentPosition); - if (currentPosition + currentRun.TextSourceLength <= firstTextSourceIndex) - { - startX -= currentRun.Size.Width; + currentPosition += offset; - currentPosition += currentRun.TextSourceLength; + var startIndex = currentRun.Text.Start + offset; - continue; - } + double startOffset; + double endOffset; - var characterLength = 0; - var endX = startX; - - if (currentRun is ShapedTextCharacters currentShapedRun) - { - var offset = Math.Max(0, firstTextSourceIndex - currentPosition); - - currentPosition += offset; - - var startIndex = currentRun.Text.Start + offset; - double startOffset; - double endOffset; - - if (currentShapedRun.ShapedBuffer.IsLeftToRight) - { - if (currentPosition < startIndex) - { - startOffset = endOffset = 0; - } - else - { - endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); - - startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); - } - } - else - { - endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); - - startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); - } - - startX -= currentRun.Size.Width - startOffset; - endX -= currentRun.Size.Width - endOffset; - - var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); - var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); + endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); - characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength); - } - else - { - if (currentPosition + currentRun.TextSourceLength <= characterIndex) - { - endX -= currentRun.Size.Width; - } + startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); - if (currentPosition < firstTextSourceIndex) - { - startX -= currentRun.Size.Width; + startX -= currentRun.Size.Width - startOffset; + endX -= currentRun.Size.Width - endOffset; - characterLength = currentRun.TextSourceLength; - } - } + var endHit = currentRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); + var startHit = currentRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); - if (endX < startX) - { - (endX, startX) = (startX, endX); - } + var characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength); - //Lines that only contain a linebreak need to be covered here - if (characterLength == 0) - { - characterLength = NewLineLength; - } - - var runWidth = endX - startX; - - remainingLength -= characterLength; - - currentPosition += characterLength; - - textRunBounds = new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); + if (endX < startX) + { + (endX, startX) = (startX, endX); + } - return true; + //Lines that only contain a linebreak need to be covered here + if (characterLength == 0) + { + characterLength = NewLineLength; } - return false; + var runWidth = endX - startX; + + return new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); } public override IReadOnlyList GetTextBounds(int firstTextSourceIndex, int textLength) @@ -1532,7 +1517,7 @@ namespace Avalonia.Media.TextFormatting var textAlignment = _paragraphProperties.TextAlignment; var paragraphFlowDirection = _paragraphProperties.FlowDirection; - if(textAlignment == TextAlignment.Justify) + if (textAlignment == TextAlignment.Justify) { textAlignment = TextAlignment.Start; } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextRunBounds.cs b/src/Avalonia.Base/Media/TextFormatting/TextRunBounds.cs index 91150160ed..bdc7a1ca89 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextRunBounds.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextRunBounds.cs @@ -3,7 +3,7 @@ /// /// The bounding rectangle of text run /// - public sealed class TextRunBounds + public readonly struct TextRunBounds { /// /// Constructing TextRunBounds diff --git a/src/Avalonia.Controls/Documents/InlineCollection.cs b/src/Avalonia.Controls/Documents/InlineCollection.cs index 565ed75ad9..15b4688809 100644 --- a/src/Avalonia.Controls/Documents/InlineCollection.cs +++ b/src/Avalonia.Controls/Documents/InlineCollection.cs @@ -111,7 +111,7 @@ namespace Avalonia.Controls.Documents private void AddText(string text) { - if(Parent is RichTextBlock textBlock && !textBlock.HasComplexContent) + if (Parent is RichTextBlock textBlock && !textBlock.HasComplexContent) { textBlock._text += text; } @@ -156,7 +156,17 @@ namespace Avalonia.Controls.Documents { foreach (var child in this) { - ((ISetLogicalParent)child).SetParent(parent); + var oldParent = child.Parent; + + if (oldParent != parent) + { + if (oldParent != null) + { + ((ISetLogicalParent)child).SetParent(null); + } + + ((ISetLogicalParent)child).SetParent(parent); + } } } diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index d744ede87d..251c850fc8 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -597,21 +597,82 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting textBounds = textLine.GetTextBounds(0, 20); - Assert.Equal(1, textBounds.Count); + Assert.Equal(2, textBounds.Count); - Assert.Equal(144.0234375, textBounds[0].Rectangle.Width); + Assert.Equal(144.0234375, textBounds.Sum(x => x.Rectangle.Width)); textBounds = textLine.GetTextBounds(0, 30); - Assert.Equal(1, textBounds.Count); + Assert.Equal(3, textBounds.Count); - Assert.Equal(216.03515625, textBounds[0].Rectangle.Width); + Assert.Equal(216.03515625, textBounds.Sum(x => x.Rectangle.Width)); textBounds = textLine.GetTextBounds(0, 40); - Assert.Equal(1, textBounds.Count); + Assert.Equal(4, textBounds.Count); + + Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width)); + } + } + + [Fact] + public void Should_GetTextRange() + { + var text = "שדגככעיחדגכAישדגשדגחייטYDASYWIWחיחלדשSAטויליHUHIUHUIDWKLאא'ק'קחליק/'וקןגגגלךשף'/קפוכדגכשדגשיח'/קטאגשד"; + + using (Start()) + { + var defaultProperties = new GenericTextRunProperties(Typeface.Default); + + var textSource = new SingleBufferTextSource(text, defaultProperties); + + var formatter = new TextFormatterImpl(); + + var textLine = + formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(defaultProperties)); + + var textRuns = textLine.TextRuns.Cast().ToList(); - Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds[0].Rectangle.Width); + var lineWidth = textLine.WidthIncludingTrailingWhitespace; + + var textBounds = textLine.GetTextBounds(0, text.Length); + + TextBounds lastBounds = null; + + var runBounds = textBounds.SelectMany(x => x.TextRunBounds).ToList(); + + Assert.Equal(textRuns.Count, runBounds.Count); + + for (var i = 0; i < textRuns.Count; i++) + { + var run = textRuns[i]; + var bounds = runBounds[i]; + + Assert.Equal(run.Text.Start, bounds.TextSourceCharacterIndex); + Assert.Equal(run, bounds.TextRun); + Assert.Equal(run.Size.Width, bounds.Rectangle.Width); + } + + for (var i = 0; i < textBounds.Count; i++) + { + var currentBounds = textBounds[i]; + + if (lastBounds != null) + { + Assert.Equal(lastBounds.Rectangle.Right, currentBounds.Rectangle.Left); + } + + var sumOfRunWidth = currentBounds.TextRunBounds.Sum(x => x.Rectangle.Width); + + Assert.Equal(sumOfRunWidth, currentBounds.Rectangle.Width); + + lastBounds = currentBounds; + } + + var sumOfBoundsWidth = textBounds.Sum(x => x.Rectangle.Width); + + Assert.Equal(lineWidth, sumOfBoundsWidth); } } @@ -779,7 +840,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textBounds = textLine.GetTextBounds(0, text.Length * 3 + 3); - Assert.Equal(1, textBounds.Count); + Assert.Equal(6, textBounds.Count); Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width)); textBounds = textLine.GetTextBounds(0, 1); @@ -789,8 +850,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting textBounds = textLine.GetTextBounds(0, firstRun.Text.Length + 1); - Assert.Equal(1, textBounds.Count); - Assert.Equal(firstRun.Size.Width + 14, textBounds[0].Rectangle.Width); + Assert.Equal(2, textBounds.Count); + Assert.Equal(firstRun.Size.Width + 14, textBounds.Sum(x => x.Rectangle.Width)); textBounds = textLine.GetTextBounds(1, firstRun.Text.Length); @@ -799,8 +860,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting textBounds = textLine.GetTextBounds(1, firstRun.Text.Length + 1); - Assert.Equal(1, textBounds.Count); - Assert.Equal(firstRun.Size.Width + 14, textBounds[0].Rectangle.Width); + Assert.Equal(2, textBounds.Count); + Assert.Equal(firstRun.Size.Width + 14, textBounds.Sum(x => x.Rectangle.Width)); } } From 282071751d9ebfdbf66c3be61851f23e54fae8dc Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Fri, 19 Aug 2022 18:05:42 +0200 Subject: [PATCH 016/334] fix: CS0169 Never used field --- src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs | 1 - src/Avalonia.X11/NativeDialogs/Gtk.cs | 3 --- .../Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs | 1 - 3 files changed, 5 deletions(-) diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs index dc2b2cd7cc..79cf909e55 100644 --- a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs +++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs @@ -129,7 +129,6 @@ namespace Avalonia.Controls private bool _clipValueToMinMax; private bool _isSyncingTextAndValueProperties; private bool _isTextChangedFromUI; - private CultureInfo? _cultureInfo; private NumberStyles _parsingNumberStyle = NumberStyles.Any; private NumberFormatInfo? _numberFormat; diff --git a/src/Avalonia.X11/NativeDialogs/Gtk.cs b/src/Avalonia.X11/NativeDialogs/Gtk.cs index ae04c072a5..d5eae037a9 100644 --- a/src/Avalonia.X11/NativeDialogs/Gtk.cs +++ b/src/Avalonia.X11/NativeDialogs/Gtk.cs @@ -258,9 +258,6 @@ namespace Avalonia.X11.NativeDialogs public static IntPtr GetForeignWindow(IntPtr xid) => gdk_x11_window_foreign_new_for_display(s_display, xid); - static object s_startGtkLock = new(); - static Task s_startGtkTask; - public static Task StartGtk() { return StartGtkCore(); diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs index 2eb10ae666..1e3c4bed48 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/EvDev/EvDevBackend.cs @@ -13,7 +13,6 @@ namespace Avalonia.LinuxFramebuffer.Input.EvDev private readonly EvDevDeviceDescription[] _deviceDescriptions; private readonly List _handlers = new List(); private int _epoll; - private bool _isQueueHandlerTriggered; private object _lock = new object(); private Action _onInput; private IInputRoot _inputRoot; From 404bb8745ee834d11d5a30cada7428c78d810bab Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Fri, 19 Aug 2022 18:09:41 +0200 Subject: [PATCH 017/334] fix: Warning CS0168 The variable 'e' is declared but never used --- tests/Avalonia.IntegrationTests.Appium/WindowTests.cs | 2 +- tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs index 382306ac83..9cce169744 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs @@ -230,7 +230,7 @@ namespace Avalonia.IntegrationTests.Appium PixelRect.Parse(_session.FindElementByAccessibilityId("ScreenRect").Text), double.Parse(_session.FindElementByAccessibilityId("Scaling").Text)); } - catch (OpenQA.Selenium.NoSuchElementException e) when (retry++ < 3) + catch (OpenQA.Selenium.NoSuchElementException) when (retry++ < 3) { // MacOS sometimes seems to need a bit of time to get itself back in order after switching out // of fullscreen. diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs index 4e5344dd25..facb25c18e 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs @@ -30,7 +30,7 @@ namespace Avalonia.IntegrationTests.Appium tab.Click(); return; } - catch (WebDriverException e) when (retry++ < 3) + catch (WebDriverException) when (retry++ < 3) { // MacOS sometimes seems to need a bit of time to get itself back in order after switching out // of fullscreen. From fab2fe7aa0dcc4c36bc6d9a2a2b0895015d20d63 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Fri, 19 Aug 2022 18:12:44 +0200 Subject: [PATCH 018/334] fix: Warning CS0414 The field is assigned but its value is never used --- src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs | 3 --- src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs | 3 --- 2 files changed, 6 deletions(-) diff --git a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs index 07db6013c7..338a3420fa 100644 --- a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs +++ b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs @@ -42,7 +42,6 @@ namespace Avalonia.Web.Blazor private GRGlInterface? _glInterface; private const SKColorType ColorType = SKColorType.Rgba8888; - private bool _initialised; private bool _useGL; private bool _inputElementFocused; @@ -300,8 +299,6 @@ namespace Avalonia.Web.Blazor _interop.SetCanvasSize((int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); - _initialised = true; - Threading.Dispatcher.UIThread.Post(async () => { _interop.RequestAnimationFrame(true); diff --git a/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs b/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs index 3adefd965f..92e2b0689e 100644 --- a/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs +++ b/src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs @@ -17,7 +17,6 @@ namespace Avalonia.Win32.Input private bool _active; private bool _showCompositionWindow; private Imm32CaretManager _caretManager = new(); - private bool _showCandidateList; private ushort _langId; private const int _caretMargin = 1; @@ -32,7 +31,6 @@ namespace Avalonia.Win32.Input _active = false; _langId = PRIMARYLANGID(LGID(HKL)); _showCompositionWindow = true; - _showCandidateList = true; IsComposing = false; } @@ -50,7 +48,6 @@ namespace Avalonia.Win32.Input _active = false; _langId = 0; _showCompositionWindow = false; - _showCandidateList = false; IsComposing = false; } From 011cb37712f897f11113383eb85681e879d92ba4 Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 20 Aug 2022 22:05:40 -0400 Subject: [PATCH 019/334] Update DatePicker template part names --- .../DateTimePickers/DatePicker.cs | 18 +++++++++--------- .../Controls/DatePicker.xaml | 18 +++++++++--------- .../Controls/DatePicker.xaml | 18 +++++++++--------- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/Avalonia.Controls/DateTimePickers/DatePicker.cs b/src/Avalonia.Controls/DateTimePickers/DatePicker.cs index f2b808fe0d..4b8fbaec14 100644 --- a/src/Avalonia.Controls/DateTimePickers/DatePicker.cs +++ b/src/Avalonia.Controls/DateTimePickers/DatePicker.cs @@ -14,15 +14,15 @@ namespace Avalonia.Controls /// /// A control to allow the user to select a date /// - [TemplatePart("ButtonContentGrid", typeof(Grid))] - [TemplatePart("DayText", typeof(TextBlock))] - [TemplatePart("FirstSpacer", typeof(Rectangle))] - [TemplatePart("FlyoutButton", typeof(Button))] - [TemplatePart("MonthText", typeof(TextBlock))] - [TemplatePart("PickerPresenter", typeof(DatePickerPresenter))] - [TemplatePart("Popup", typeof(Popup))] - [TemplatePart("SecondSpacer", typeof(Rectangle))] - [TemplatePart("YearText", typeof(TextBlock))] + [TemplatePart("PART_ButtonContentGrid", typeof(Grid))] + [TemplatePart("PART_DayText", typeof(TextBlock))] + [TemplatePart("PART_FirstSpacer", typeof(Rectangle))] + [TemplatePart("PART_FlyoutButton", typeof(Button))] + [TemplatePart("PART_MonthText", typeof(TextBlock))] + [TemplatePart("PART_PickerPresenter", typeof(DatePickerPresenter))] + [TemplatePart("PART_Popup", typeof(Popup))] + [TemplatePart("PART_SecondSpacer", typeof(Rectangle))] + [TemplatePart("PART_YearText", typeof(TextBlock))] [PseudoClasses(":hasnodate")] public class DatePicker : TemplatedControl { diff --git a/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml b/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml index bc47f3892e..c5afa3dfca 100644 --- a/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml @@ -101,7 +101,7 @@ HorizontalAlignment="Stretch" VerticalAlignment="Top"/> - - - + diff --git a/src/Avalonia.Themes.Simple/Controls/DatePicker.xaml b/src/Avalonia.Themes.Simple/Controls/DatePicker.xaml index a6da2ad9f8..f7310eba64 100644 --- a/src/Avalonia.Themes.Simple/Controls/DatePicker.xaml +++ b/src/Avalonia.Themes.Simple/Controls/DatePicker.xaml @@ -102,7 +102,7 @@ Content="{TemplateBinding Header}" ContentTemplate="{TemplateBinding HeaderTemplate}" /> - - - + From 66baf9c03835617f0e10dc8581bf870dfc872303 Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 20 Aug 2022 22:47:54 -0400 Subject: [PATCH 020/334] Update Date/TimePicker template part names --- .../DateTimePickers/DatePicker.cs | 18 ++--- .../DateTimePickers/DatePickerPresenter.cs | 68 +++++++++---------- .../DateTimePickers/TimePicker.cs | 48 ++++++------- .../DateTimePickers/TimePickerPresenter.cs | 56 +++++++-------- .../Controls/DatePicker.xaml | 38 +++++------ .../Controls/TimePicker.xaml | 60 ++++++++-------- .../Controls/DatePicker.xaml | 34 +++++----- .../Controls/TimePicker.xaml | 58 ++++++++-------- 8 files changed, 190 insertions(+), 190 deletions(-) diff --git a/src/Avalonia.Controls/DateTimePickers/DatePicker.cs b/src/Avalonia.Controls/DateTimePickers/DatePicker.cs index 4b8fbaec14..db59dbdb4a 100644 --- a/src/Avalonia.Controls/DateTimePickers/DatePicker.cs +++ b/src/Avalonia.Controls/DateTimePickers/DatePicker.cs @@ -280,15 +280,15 @@ namespace Avalonia.Controls } base.OnApplyTemplate(e); - _flyoutButton = e.NameScope.Find - [TemplatePart("AcceptButton", typeof(Button))] - [TemplatePart("DayDownButton", typeof(RepeatButton))] - [TemplatePart("DayHost", typeof(Panel))] - [TemplatePart("DaySelector", typeof(DateTimePickerPanel))] - [TemplatePart("DayUpButton", typeof(RepeatButton))] - [TemplatePart("DismissButton", typeof(Button))] - [TemplatePart("FirstSpacer", typeof(Rectangle))] - [TemplatePart("MonthDownButton", typeof(RepeatButton))] - [TemplatePart("MonthHost", typeof(Panel))] - [TemplatePart("MonthSelector", typeof(DateTimePickerPanel))] - [TemplatePart("MonthUpButton", typeof(RepeatButton))] - [TemplatePart("PickerContainer", typeof(Grid))] - [TemplatePart("SecondSpacer", typeof(Rectangle))] - [TemplatePart("YearDownButton", typeof(RepeatButton))] - [TemplatePart("YearHost", typeof(Panel))] - [TemplatePart("YearSelector", typeof(DateTimePickerPanel))] - [TemplatePart("YearUpButton", typeof(RepeatButton))] + [TemplatePart("PART_AcceptButton", typeof(Button))] + [TemplatePart("PART_DayDownButton", typeof(RepeatButton))] + [TemplatePart("PART_DayHost", typeof(Panel))] + [TemplatePart("PART_DaySelector", typeof(DateTimePickerPanel))] + [TemplatePart("PART_DayUpButton", typeof(RepeatButton))] + [TemplatePart("PART_DismissButton", typeof(Button))] + [TemplatePart("PART_FirstSpacer", typeof(Rectangle))] + [TemplatePart("PART_MonthDownButton", typeof(RepeatButton))] + [TemplatePart("PART_MonthHost", typeof(Panel))] + [TemplatePart("PART_MonthSelector", typeof(DateTimePickerPanel))] + [TemplatePart("PART_MonthUpButton", typeof(RepeatButton))] + [TemplatePart("PART_PickerContainer", typeof(Grid))] + [TemplatePart("PART_SecondSpacer", typeof(Rectangle))] + [TemplatePart("PART_YearDownButton", typeof(RepeatButton))] + [TemplatePart("PART_YearHost", typeof(Panel))] + [TemplatePart("PART_YearSelector", typeof(DateTimePickerPanel))] + [TemplatePart("PART_YearUpButton", typeof(RepeatButton))] public class DatePickerPresenter : PickerPresenterBase { /// @@ -253,58 +253,58 @@ namespace Avalonia.Controls { base.OnApplyTemplate(e); // These are requirements, so throw if not found - _pickerContainer = e.NameScope.Get("PickerContainer"); - _monthHost = e.NameScope.Get("MonthHost"); - _dayHost = e.NameScope.Get("DayHost"); - _yearHost = e.NameScope.Get("YearHost"); + _pickerContainer = e.NameScope.Get("PART_PickerContainer"); + _monthHost = e.NameScope.Get("PART_MonthHost"); + _dayHost = e.NameScope.Get("PART_DayHost"); + _yearHost = e.NameScope.Get("PART_YearHost"); - _monthSelector = e.NameScope.Get("MonthSelector"); + _monthSelector = e.NameScope.Get("PART_MonthSelector"); _monthSelector.SelectionChanged += OnMonthChanged; - _daySelector = e.NameScope.Get("DaySelector"); + _daySelector = e.NameScope.Get("PART_DaySelector"); _daySelector.SelectionChanged += OnDayChanged; - _yearSelector = e.NameScope.Get("YearSelector"); + _yearSelector = e.NameScope.Get("PART_YearSelector"); _yearSelector.SelectionChanged += OnYearChanged; - _acceptButton = e.NameScope.Get - [TemplatePart("AcceptButton", typeof(Button))] - [TemplatePart("DismissButton", typeof(Button))] - [TemplatePart("HourDownButton", typeof(RepeatButton))] - [TemplatePart("HourSelector", typeof(DateTimePickerPanel))] - [TemplatePart("HourUpButton", typeof(RepeatButton))] - [TemplatePart("MinuteDownButton", typeof(RepeatButton))] - [TemplatePart("MinuteSelector", typeof(DateTimePickerPanel))] - [TemplatePart("MinuteUpButton", typeof(RepeatButton))] - [TemplatePart("PeriodDownButton", typeof(RepeatButton))] - [TemplatePart("PeriodHost", typeof(Panel))] - [TemplatePart("PeriodSelector", typeof(DateTimePickerPanel))] - [TemplatePart("PeriodUpButton", typeof(RepeatButton))] - [TemplatePart("PickerContainer", typeof(Grid))] - [TemplatePart("SecondSpacer", typeof(Rectangle))] + [TemplatePart("PART_AcceptButton", typeof(Button))] + [TemplatePart("PART_DismissButton", typeof(Button))] + [TemplatePart("PART_HourDownButton", typeof(RepeatButton))] + [TemplatePart("PART_HourSelector", typeof(DateTimePickerPanel))] + [TemplatePart("PART_HourUpButton", typeof(RepeatButton))] + [TemplatePart("PART_MinuteDownButton", typeof(RepeatButton))] + [TemplatePart("PART_MinuteSelector", typeof(DateTimePickerPanel))] + [TemplatePart("PART_MinuteUpButton", typeof(RepeatButton))] + [TemplatePart("PART_PeriodDownButton", typeof(RepeatButton))] + [TemplatePart("PART_PeriodHost", typeof(Panel))] + [TemplatePart("PART_PeriodSelector", typeof(DateTimePickerPanel))] + [TemplatePart("PART_PeriodUpButton", typeof(RepeatButton))] + [TemplatePart("PART_PickerContainer", typeof(Grid))] + [TemplatePart("PART_SecondSpacer", typeof(Rectangle))] public class TimePickerPresenter : PickerPresenterBase { /// @@ -122,40 +122,40 @@ namespace Avalonia.Controls { base.OnApplyTemplate(e); - _pickerContainer = e.NameScope.Get("PickerContainer"); - _periodHost = e.NameScope.Get("PeriodHost"); + _pickerContainer = e.NameScope.Get("PART_PickerContainer"); + _periodHost = e.NameScope.Get("PART_PeriodHost"); - _hourSelector = e.NameScope.Get("HourSelector"); - _minuteSelector = e.NameScope.Get("MinuteSelector"); - _periodSelector = e.NameScope.Get("PeriodSelector"); + _hourSelector = e.NameScope.Get("PART_HourSelector"); + _minuteSelector = e.NameScope.Get("PART_MinuteSelector"); + _periodSelector = e.NameScope.Get("PART_PeriodSelector"); - _spacer2 = e.NameScope.Get("SecondSpacer"); + _spacer2 = e.NameScope.Get("PART_SecondSpacer"); - _acceptButton = e.NameScope.Get - - - + @@ -216,42 +216,42 @@ Padding="{DynamicResource DateTimeFlyoutBorderPadding}" MaxHeight="398"> - + - + - - - + + - + - - - + + - + - - - + + - - - - - - - + @@ -214,50 +214,50 @@ CornerRadius="{TemplateBinding CornerRadius}"> - + - - - - - - - - - - - - @@ -272,12 +272,12 @@ Color="{DynamicResource ThemeAccentColor}" /> - - - - - - [TemplatePart("PART_ButtonContentGrid", typeof(Grid))] - [TemplatePart("PART_DayText", typeof(TextBlock))] + [TemplatePart("PART_DayTextBlock", typeof(TextBlock))] [TemplatePart("PART_FirstSpacer", typeof(Rectangle))] [TemplatePart("PART_FlyoutButton", typeof(Button))] - [TemplatePart("PART_MonthText", typeof(TextBlock))] + [TemplatePart("PART_MonthTextBlock", typeof(TextBlock))] [TemplatePart("PART_PickerPresenter", typeof(DatePickerPresenter))] [TemplatePart("PART_Popup", typeof(Popup))] [TemplatePart("PART_SecondSpacer", typeof(Rectangle))] - [TemplatePart("PART_YearText", typeof(TextBlock))] + [TemplatePart("PART_YearTextBlock", typeof(TextBlock))] [PseudoClasses(":hasnodate")] public class DatePicker : TemplatedControl { @@ -281,9 +281,9 @@ namespace Avalonia.Controls base.OnApplyTemplate(e); _flyoutButton = e.NameScope.Find public TextInputContentType ContentType { get; set; } + + /// + /// Defines the property. + /// + public static readonly AttachedProperty ReturnKeyTypeProperty = + AvaloniaProperty.RegisterAttached( + "ReturnKeyType", + defaultValue: TextInputReturnKeyType.Default, + inherits: true); + + /// + /// Sets the value of the attached on a control. + /// + /// The control. + /// The property value to set. + public static void SetReturnKeyType(StyledElement avaloniaObject, TextInputReturnKeyType value) + { + avaloniaObject.SetValue(ReturnKeyTypeProperty, value); + } + + /// + /// Gets the value of the attached . + /// + /// The target. + /// TextInputReturnKeyType + public static TextInputReturnKeyType GetReturnKeyType(StyledElement avaloniaObject) + { + return avaloniaObject.GetValue(ReturnKeyTypeProperty); + } + + /// + /// Determines what the Return key says and how it behaves. + /// + public TextInputReturnKeyType ReturnKeyType { get; set; } + /// /// Defines the property. /// diff --git a/src/Avalonia.Base/Input/TextInput/TextInputReturnKeyType.cs b/src/Avalonia.Base/Input/TextInput/TextInputReturnKeyType.cs new file mode 100644 index 0000000000..9fd51766d3 --- /dev/null +++ b/src/Avalonia.Base/Input/TextInput/TextInputReturnKeyType.cs @@ -0,0 +1,17 @@ +namespace Avalonia.Input.TextInput; + +public enum TextInputReturnKeyType +{ + Default, + Go, + Google, + Join, + Next, + Route, + Search, + Send, + Yahoo, + Done, + EmergencyCall, + Continue, +} diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.Text.cs b/src/iOS/Avalonia.iOS/AvaloniaView.Text.cs index 8615e9b124..dbfc8c9596 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaView.Text.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaView.Text.cs @@ -137,6 +137,8 @@ public partial class AvaloniaView : ITextInputMethodImpl, IUITextInput { IsSecureEntry = true; } + + ReturnKeyType = (UIReturnKeyType)options.ReturnKeyType; } From 1174197b6d873e54e1070dc902d4efa1ee6cd778 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 1 Sep 2022 14:43:17 +0100 Subject: [PATCH 038/334] add hack implementation of surrounding text. --- .../TextBoxTextInputMethodClient.cs | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs index 7530460dea..8ee4def676 100644 --- a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs +++ b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs @@ -20,14 +20,15 @@ namespace Avalonia.Controls { return default; } + var transform = _presenter.TransformToVisual(_parent); - + if (transform == null) { return default; } - - var rect = _presenter.GetCursorRectangle().TransformToAABB(transform.Value); + + var rect = _presenter.GetCursorRectangle().TransformToAABB(transform.Value); return rect; } @@ -37,9 +38,10 @@ namespace Avalonia.Controls public IVisual TextViewVisual => _presenter!; public event EventHandler? TextViewVisualChanged; public bool SupportsPreedit => true; + public void SetPreeditText(string? text) { - if(_presenter == null) + if (_presenter == null) { return; } @@ -47,12 +49,12 @@ namespace Avalonia.Controls _presenter.PreeditText = text; } - public bool SupportsSurroundingText => false; - + public bool SupportsSurroundingText => true; + public event EventHandler? SurroundingTextChanged { add { } remove { } } - - public TextInputMethodSurroundingText SurroundingText => throw new NotSupportedException(); - + + public TextInputMethodSurroundingText SurroundingText => new TextInputMethodSurroundingText { Text = _presenter?.Text ?? "", CursorOffset = _presenter?.CaretIndex ?? 0, AnchorOffset = _presenter?.SelectionStart ?? 0}; + public string? TextBeforeCursor => null; public string? TextAfterCursor => null; From 7272fb89c7a414eb5ed7c16107fd11785d8f358f Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 1 Sep 2022 14:43:45 +0100 Subject: [PATCH 039/334] further ios ime implementation. --- src/iOS/Avalonia.iOS/AvaloniaView.Text.cs | 90 +++++++++++++++++++---- src/iOS/Avalonia.iOS/AvaloniaView.cs | 2 +- 2 files changed, 75 insertions(+), 17 deletions(-) diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.Text.cs b/src/iOS/Avalonia.iOS/AvaloniaView.Text.cs index dbfc8c9596..f0d5e5d1ef 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaView.Text.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaView.Text.cs @@ -43,16 +43,11 @@ public partial class AvaloniaView : ITextInputMethodImpl, IUITextInput } private string _markedText = ""; - private readonly IUITextInputDelegate _inputDelegate; private ITextInputMethodClient? _client; private NSDictionary? _markedTextStyle; private readonly UITextPosition _beginningOfDocument = new AvaloniaTextPosition(0); private readonly UITextInputStringTokenizer _tokenizer; - private class TextInputHandler : UITextInputDelegate - { - } - public ITextInputMethodClient? Client => _client; public bool IsActive => _client != null; @@ -63,10 +58,17 @@ public partial class AvaloniaView : ITextInputMethodImpl, IUITextInput void ITextInputMethodImpl.SetClient(ITextInputMethodClient? client) { + if (_client != null) + { + _client.CursorRectangleChanged -= ClientOnCursorRectangleChanged; + } + _client = client; if (_client is { }) { + _client.CursorRectangleChanged += ClientOnCursorRectangleChanged; + _client.SurroundingTextChanged += ClientOnSurroundingTextChanged; BecomeFirstResponder(); } else @@ -75,6 +77,23 @@ public partial class AvaloniaView : ITextInputMethodImpl, IUITextInput } } + private void ClientOnSurroundingTextChanged(object? sender, EventArgs e) + { + var _inputDelegate = UITextInputDelegate.FromObject(((IUITextInput)this).WeakInputDelegate) as IUITextInputDelegate; + + _inputDelegate?.TextWillChange(this); + _inputDelegate?.TextDidChange(this); + } + + private void ClientOnCursorRectangleChanged(object? sender, EventArgs e) + { + var _inputDelegate = Runtime.GetINativeObject(((IUITextInput)this).WeakInputDelegate.Handle.Handle, true); + + + _inputDelegate?.SelectionWillChange(this); + _inputDelegate?.SelectionDidChange(this); + } + void ITextInputMethodImpl.SetCursorRect(Rect rect) { // maybe this will be cursor / selection rect? @@ -139,6 +158,7 @@ public partial class AvaloniaView : ITextInputMethodImpl, IUITextInput } ReturnKeyType = (UIReturnKeyType)options.ReturnKeyType; + AutocorrectionType = UITextAutocorrectionType.Yes; } @@ -148,11 +168,36 @@ public partial class AvaloniaView : ITextInputMethodImpl, IUITextInput } // Traits (Optional) + [Export("autocapitalizationType")] public UITextAutocapitalizationType AutocapitalizationType { get; private set; } + + [Export("autocorrectionType")] public UITextAutocorrectionType AutocorrectionType { get; private set; } [Export("keyboardType")] public UIKeyboardType KeyboardType { get; private set; } = UIKeyboardType.Default; + [Export("keyboardAppearance")] + public UIKeyboardAppearance KeyboardAppearance { get; private set; } = UIKeyboardAppearance.Default; + + [Export("returnKeyType")] public UIReturnKeyType ReturnKeyType { get; set; } + + [Export("enablesReturnKeyAutomatically")] public bool EnablesReturnKeyAutomatically { get; set; } [Export("isSecureTextEntry")] public bool IsSecureEntry { get; private set; } - [Export("returnKeyType")] public UIReturnKeyType ReturnKeyType { get; set; } + [Export("spellCheckingType")] + public UITextSpellCheckingType SpellCheckingType { get; set; } = UITextSpellCheckingType.Default; + + [Export("textContentType")] + public NSString TextContentType { get; set; } + + [Export("smartQuotesType")] + public UITextSmartQuotesType SmartQuotesType { get; set; } = UITextSmartQuotesType.Default; + + [Export("smartDashesType")] + public UITextSmartDashesType SmartDashesType { get; set; } = UITextSmartDashesType.Default; + + [Export("smartInsertDeleteType")] + public UITextSmartInsertDeleteType SmartInsertDeleteType { get; set; } = UITextSmartInsertDeleteType.Default; + + [Export("passwordRules")] + public UITextInputPasswordRules PasswordRules { get; set; } void IUIKeyInput.InsertText(string text) { @@ -213,7 +258,9 @@ public partial class AvaloniaView : ITextInputMethodImpl, IUITextInput var start = (range.Start as AvaloniaTextPosition).Offset; int end = (range.End as AvaloniaTextPosition).Offset; - return text[start .. end]; + var result = text[start .. end]; + + return result; } void IUITextInput.ReplaceText(UITextRange range, string text) @@ -246,7 +293,7 @@ public partial class AvaloniaView : ITextInputMethodImpl, IUITextInput _markedText = ""; } - UITextRange IUITextInput.GetTextRange(UITextPosition fromPosition, UITextPosition toPosition) + public UITextRange GetTextRange(UITextPosition fromPosition, UITextPosition toPosition) { if (fromPosition is AvaloniaTextPosition f && toPosition is AvaloniaTextPosition t) { @@ -273,7 +320,7 @@ public partial class AvaloniaView : ITextInputMethodImpl, IUITextInput return new AvaloniaTextPosition(posPlusIndex); } - throw new Exception(); + return null; } UITextPosition IUITextInput.GetPosition(UITextPosition fromPosition, UITextLayoutDirection inDirection, nint offset) @@ -311,14 +358,19 @@ public partial class AvaloniaView : ITextInputMethodImpl, IUITextInput return NSComparisonResult.Same; } - throw new Exception(); + return NSComparisonResult.Descending; } nint IUITextInput.GetOffsetFromPosition(UITextPosition fromPosition, UITextPosition toPosition) { - if (fromPosition is AvaloniaTextPosition f && toPosition is AvaloniaTextPosition t) + if (fromPosition is AvaloniaTextPosition f) { - return t.Offset - f.Offset; + if (toPosition is AvaloniaTextPosition t) + { + return t.Offset - f.Offset; + } + + return f.Offset; } throw new Exception(); @@ -452,10 +504,11 @@ public partial class AvaloniaView : ITextInputMethodImpl, IUITextInput } } + NSObject? IUITextInput.WeakInputDelegate { - get => _inputDelegate as TextInputHandler; - set => throw new NotSupportedException(); + get; + set; } NSObject IUITextInput.WeakTokenizer => _tokenizer; @@ -466,10 +519,15 @@ public partial class AvaloniaView : ITextInputMethodImpl, IUITextInput { if (string.IsNullOrWhiteSpace(_markedText)) { - return null; + //return null; } - return new AvaloniaTextRange(0, _markedText.Length); + if (_client == null) + { + return null; + } + + return new AvaloniaTextRange(Math.Min(_client.SurroundingText.CursorOffset, _client.SurroundingText.AnchorOffset), Math.Max(_client.SurroundingText.CursorOffset, _client.SurroundingText.AnchorOffset)); } } } diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.cs b/src/iOS/Avalonia.iOS/AvaloniaView.cs index 77363aef7b..06201e366b 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaView.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaView.cs @@ -31,7 +31,7 @@ namespace Avalonia.iOS _topLevelImpl = new TopLevelImpl(this); _touches = new TouchHandler(this, _topLevelImpl); _topLevel = new EmbeddableControlRoot(_topLevelImpl); - _inputDelegate = new TextInputHandler(); + _tokenizer = new UITextInputStringTokenizer(this); _topLevel.Prepare(); From 60381e290aaad3429477837b500c7a8a280a34c7 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 1 Sep 2022 20:56:24 +0100 Subject: [PATCH 040/334] make implementation consistent with ios sample. https://developer.apple.com/library/archive/samplecode/SimpleTextInput/Introduction/Intro.html#//apple_ref/doc/uid/DTS40010633 --- src/iOS/Avalonia.iOS/AvaloniaView.Text.cs | 182 +++++++++++++--------- 1 file changed, 106 insertions(+), 76 deletions(-) diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.Text.cs b/src/iOS/Avalonia.iOS/AvaloniaView.Text.cs index f0d5e5d1ef..99b2b7ad71 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaView.Text.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaView.Text.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.InteropServices; using Foundation; using ObjCRuntime; using Avalonia.Input.TextInput; @@ -18,28 +19,33 @@ public partial class AvaloniaView : ITextInputMethodImpl, IUITextInput { private class AvaloniaTextRange : UITextRange { - private readonly AvaloniaTextPosition _start; - private readonly AvaloniaTextPosition _end; + private readonly NSRange _range; - public AvaloniaTextRange(int start, int end) + public AvaloniaTextRange(NSRange range) { - _start = new AvaloniaTextPosition(start); - _end = new AvaloniaTextPosition(end); + _range = range; } - public override AvaloniaTextPosition Start => _start; + public override AvaloniaTextPosition Start => new AvaloniaTextPosition((int)_range.Location); - public override AvaloniaTextPosition End => _end; + public override AvaloniaTextPosition End => new AvaloniaTextPosition((int)(_range.Location + _range.Length)); + + public NSRange Range => _range; + + public override bool IsEmpty => _range.Length == 0; } private class AvaloniaTextPosition : UITextPosition { - public AvaloniaTextPosition(int offset) + public AvaloniaTextPosition(nint index) { - Offset = offset; + Index = index; } - public int Offset { get; } + public nint Index { get; } + + //[Export("inputDelegate")] + //public UITextInputDelegate InputDelegate { get; set; } } private string _markedText = ""; @@ -76,22 +82,25 @@ public partial class AvaloniaView : ITextInputMethodImpl, IUITextInput ResignFirstResponder(); } } + + [DllImport("/usr/lib/libobjc.dylib")] + extern static void objc_msgSend(IntPtr receiver, IntPtr selector, IntPtr arg); - private void ClientOnSurroundingTextChanged(object? sender, EventArgs e) + private static readonly IntPtr SelectionWillChange = Selector.GetHandle("selectionWillChange:"); + private static readonly IntPtr SelectionDidChange = Selector.GetHandle("selectionDidChange:"); + private static readonly IntPtr TextWillChange = Selector.GetHandle("textWillChange:"); + private static readonly IntPtr TextDidChange = Selector.GetHandle("textDidChange:"); + + private void ClientOnCursorRectangleChanged(object? sender, EventArgs e) { - var _inputDelegate = UITextInputDelegate.FromObject(((IUITextInput)this).WeakInputDelegate) as IUITextInputDelegate; - - _inputDelegate?.TextWillChange(this); - _inputDelegate?.TextDidChange(this); + objc_msgSend(WeakInputDelegate.Handle.Handle, SelectionWillChange, this.Handle.Handle); + objc_msgSend(WeakInputDelegate.Handle.Handle, SelectionDidChange, this.Handle.Handle); } - private void ClientOnCursorRectangleChanged(object? sender, EventArgs e) + private void ClientOnSurroundingTextChanged(object? sender, EventArgs e) { - var _inputDelegate = Runtime.GetINativeObject(((IUITextInput)this).WeakInputDelegate.Handle.Handle, true); - - - _inputDelegate?.SelectionWillChange(this); - _inputDelegate?.SelectionDidChange(this); + objc_msgSend(WeakInputDelegate.Handle.Handle, TextWillChange, Handle.Handle); + objc_msgSend(WeakInputDelegate.Handle.Handle, TextDidChange, Handle.Handle); } void ITextInputMethodImpl.SetCursorRect(Rect rect) @@ -255,8 +264,8 @@ public partial class AvaloniaView : ITextInputMethodImpl, IUITextInput text = text[.. cursorPos] + _markedText + text[cursorPos ..]; } - var start = (range.Start as AvaloniaTextPosition).Offset; - int end = (range.End as AvaloniaTextPosition).Offset; + var start = (int)(range.Start as AvaloniaTextPosition).Index; + var end = (int)(range.End as AvaloniaTextPosition).Index; var result = text[start .. end]; @@ -298,7 +307,8 @@ public partial class AvaloniaView : ITextInputMethodImpl, IUITextInput if (fromPosition is AvaloniaTextPosition f && toPosition is AvaloniaTextPosition t) { // todo check calculation. - return new AvaloniaTextRange(f.Offset, t.Offset); + var range = new NSRange(Math.Min(f.Index, t.Index), Math.Abs(t.Index - f.Index)); + return new AvaloniaTextRange(range); } throw new Exception(); @@ -306,18 +316,15 @@ public partial class AvaloniaView : ITextInputMethodImpl, IUITextInput UITextPosition IUITextInput.GetPosition(UITextPosition fromPosition, nint offset) { - if (fromPosition is AvaloniaTextPosition f) + if (fromPosition is AvaloniaTextPosition indexedPosition) { - var position = f.Offset; - int posPlusIndex = position + (int)offset; - var length = _client.SurroundingText.Text.Length; + var end = indexedPosition.Index + offset; + // Verify position is valid in document. + //if (end > self.text.length || end < 0) { + // return nil; + //} - if (posPlusIndex < 0 || posPlusIndex > length) - { - return null; - } - - return new AvaloniaTextPosition(posPlusIndex); + return new AvaloniaTextPosition(end); } return null; @@ -327,19 +334,30 @@ public partial class AvaloniaView : ITextInputMethodImpl, IUITextInput { if (fromPosition is AvaloniaTextPosition f) { - var pos = f.Offset; + var newPosition = f.Index; switch (inDirection) { case UITextLayoutDirection.Left: - return new AvaloniaTextPosition(pos - (int)offset); + newPosition -= offset; + break; case UITextLayoutDirection.Right: - return new AvaloniaTextPosition(pos + (int)offset); + newPosition += offset; + break; + } + + if (newPosition < 0) + { + newPosition = 0; + } - default: - return fromPosition; + if (newPosition > _client.SurroundingText.Text.Length) + { + newPosition = _client.SurroundingText.Text.Length; } + + return new AvaloniaTextPosition(newPosition); } throw new Exception(); @@ -349,28 +367,23 @@ public partial class AvaloniaView : ITextInputMethodImpl, IUITextInput { if (first is AvaloniaTextPosition f && second is AvaloniaTextPosition s) { - if (f.Offset > s.Offset) + if (f.Index < s.Index) return NSComparisonResult.Ascending; - if (f.Offset < s.Offset) + if (f.Index > s.Index) return NSComparisonResult.Descending; return NSComparisonResult.Same; } - return NSComparisonResult.Descending; + throw new Exception(); } nint IUITextInput.GetOffsetFromPosition(UITextPosition fromPosition, UITextPosition toPosition) { - if (fromPosition is AvaloniaTextPosition f) + if (fromPosition is AvaloniaTextPosition f && toPosition is AvaloniaTextPosition t) { - if (toPosition is AvaloniaTextPosition t) - { - return t.Offset - f.Offset; - } - - return f.Offset; + return t.Index - f.Index; } throw new Exception(); @@ -378,16 +391,24 @@ public partial class AvaloniaView : ITextInputMethodImpl, IUITextInput UITextPosition IUITextInput.GetPositionWithinRange(UITextRange range, UITextLayoutDirection direction) { - if (range is AvaloniaTextRange r) + if (range is AvaloniaTextRange indexedRange) { + nint position = 0; + switch (direction) { + case UITextLayoutDirection.Up: + case UITextLayoutDirection.Left: + position = indexedRange.Range.Location; + break; + + case UITextLayoutDirection.Down: case UITextLayoutDirection.Right: - return r.End; - - default: - return r.Start; + position = indexedRange.Range.Location + indexedRange.Range.Length; + break; } + + return new AvaloniaTextPosition(position); } throw new Exception(); @@ -395,17 +416,24 @@ public partial class AvaloniaView : ITextInputMethodImpl, IUITextInput UITextRange IUITextInput.GetCharacterRange(UITextPosition byExtendingPosition, UITextLayoutDirection direction) { - if (byExtendingPosition is AvaloniaTextPosition p) + if (byExtendingPosition is AvaloniaTextPosition pos) { + NSRange result = new NSRange(); + switch (direction) { + case UITextLayoutDirection.Up: case UITextLayoutDirection.Left: - return new AvaloniaTextRange(0, p.Offset); + result = new NSRange(pos.Index - 1, 1); + break; - default: - // todo check this. - return new AvaloniaTextRange(p.Offset, _client.SurroundingText.Text.Length); + case UITextLayoutDirection.Right: + case UITextLayoutDirection.Down: + result = new NSRange(pos.Index, 1); + break; } + + return new AvaloniaTextRange(result); } throw new Exception(); @@ -464,23 +492,27 @@ public partial class AvaloniaView : ITextInputMethodImpl, IUITextInput UITextRange IUITextInput.GetCharacterRangeAtPoint(CGPoint point) { - // TODO check if needed, hittest? - return new AvaloniaTextRange(_client.SurroundingText.CursorOffset, _client.SurroundingText.CursorOffset); + return null; } UITextSelectionRect[] IUITextInput.GetSelectionRects(UITextRange range) { - // todo? - return Array.Empty(); + return null; + } + + [Export("textStylingAtPosition:inDirection:")] + public NSDictionary GetTextStylingAtPosition(UITextPosition position, UITextStorageDirection direction) + { + return null; } UITextRange? IUITextInput.SelectedTextRange { get { - return new AvaloniaTextRange( - Math.Min(_client.SurroundingText.CursorOffset, _client.SurroundingText.AnchorOffset), - Math.Max(_client.SurroundingText.CursorOffset, _client.SurroundingText.AnchorOffset)); + return new AvaloniaTextRange(new NSRange( + (nint)Math.Min(_client.SurroundingText.CursorOffset, _client.SurroundingText.AnchorOffset), + (nint)Math.Abs(_client.SurroundingText.CursorOffset - _client.SurroundingText.AnchorOffset))); } set { @@ -504,8 +536,8 @@ public partial class AvaloniaView : ITextInputMethodImpl, IUITextInput } } - - NSObject? IUITextInput.WeakInputDelegate + + public NSObject? WeakInputDelegate { get; set; @@ -517,17 +549,15 @@ public partial class AvaloniaView : ITextInputMethodImpl, IUITextInput { get { - if (string.IsNullOrWhiteSpace(_markedText)) - { - //return null; - } - - if (_client == null) + if (_client == null || string.IsNullOrWhiteSpace(_markedText)) { return null; } - - return new AvaloniaTextRange(Math.Min(_client.SurroundingText.CursorOffset, _client.SurroundingText.AnchorOffset), Math.Max(_client.SurroundingText.CursorOffset, _client.SurroundingText.AnchorOffset)); + + // todo + return new AvaloniaTextRange(new NSRange( + (nint)Math.Min(_client.SurroundingText.CursorOffset, _client.SurroundingText.AnchorOffset), + (nint)Math.Abs(_client.SurroundingText.CursorOffset - _client.SurroundingText.AnchorOffset))); } } } From 0cc0443d34567f0d20082eaee29625a732b93849 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 1 Sep 2022 22:33:03 +0200 Subject: [PATCH 041/334] Display window order in integration test app. The previous method of relying on the order of windows returned by appium to determine window z-order proved not to be reliable. Instead use p/invoke to read `NSWindow.orderedIndex` on a timer and display it in a text box in the `ShowWindowtest` window. This commit doesn't update the tests to use this new info though. --- .../IntegrationTestApp/MacOSIntegration.cs | 27 ++++++++++++++++++ .../IntegrationTestApp/ShowWindowTest.axaml | 8 ++++-- .../ShowWindowTest.axaml.cs | 28 +++++++++++++++++-- 3 files changed, 58 insertions(+), 5 deletions(-) create mode 100644 samples/IntegrationTestApp/MacOSIntegration.cs diff --git a/samples/IntegrationTestApp/MacOSIntegration.cs b/samples/IntegrationTestApp/MacOSIntegration.cs new file mode 100644 index 0000000000..f700a5b4e2 --- /dev/null +++ b/samples/IntegrationTestApp/MacOSIntegration.cs @@ -0,0 +1,27 @@ +using System; +using System.Runtime.InteropServices; +using Avalonia.Controls; + +namespace IntegrationTestApp +{ + public static class MacOSIntegration + { + [DllImport("/usr/lib/libobjc.dylib", EntryPoint = "sel_registerName")] + private static extern IntPtr GetHandle(string name); + + [DllImport("/usr/lib/libobjc.dylib", EntryPoint = "objc_msgSend")] + private static extern long Int64_objc_msgSend(IntPtr receiver, IntPtr selector); + + private static readonly IntPtr s_orderedIndexSelector; + + static MacOSIntegration() + { + s_orderedIndexSelector = GetHandle("orderedIndex");; + } + + public static long GetOrderedIndex(Window window) + { + return Int64_objc_msgSend(window.PlatformImpl!.Handle.Handle, s_orderedIndexSelector); + } + } +} diff --git a/samples/IntegrationTestApp/ShowWindowTest.axaml b/samples/IntegrationTestApp/ShowWindowTest.axaml index 4001bac7e2..17c359df51 100644 --- a/samples/IntegrationTestApp/ShowWindowTest.axaml +++ b/samples/IntegrationTestApp/ShowWindowTest.axaml @@ -3,7 +3,7 @@ x:Class="IntegrationTestApp.ShowWindowTest" Name="SecondaryWindow" Title="Show Window Test"> - + @@ -31,6 +31,10 @@ Maximized Fullscreen - + + + + + diff --git a/samples/IntegrationTestApp/ShowWindowTest.axaml.cs b/samples/IntegrationTestApp/ShowWindowTest.axaml.cs index 001f186761..43875dd990 100644 --- a/samples/IntegrationTestApp/ShowWindowTest.axaml.cs +++ b/samples/IntegrationTestApp/ShowWindowTest.axaml.cs @@ -1,21 +1,32 @@ using System; +using System.Runtime.InteropServices; using Avalonia; using Avalonia.Controls; -using Avalonia.Interactivity; using Avalonia.Markup.Xaml; -using Avalonia.Rendering; +using Avalonia.Threading; namespace IntegrationTestApp { public class ShowWindowTest : Window { + private readonly DispatcherTimer? _timer; + private readonly TextBox? _orderTextBox; + public ShowWindowTest() { InitializeComponent(); DataContext = this; PositionChanged += (s, e) => this.GetControl("Position").Text = $"{Position}"; - } + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + _orderTextBox = this.GetControl("Order"); + _timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(250) }; + _timer.Tick += TimerOnTick; + _timer.Start(); + } + } + private void InitializeComponent() { AvaloniaXamlLoader.Load(this); @@ -36,5 +47,16 @@ namespace IntegrationTestApp ownerRect.Text = $"{owner.Position}, {PixelSize.FromSize(owner.FrameSize!.Value, scaling)}"; } } + + protected override void OnClosed(EventArgs e) + { + base.OnClosed(e); + _timer?.Stop(); + } + + private void TimerOnTick(object? sender, EventArgs e) + { + _orderTextBox!.Text = MacOSIntegration.GetOrderedIndex(this).ToString(); + } } } From d1b652213a944e1e2b254b475b5d5c6915ca2694 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 1 Sep 2022 22:33:39 +0200 Subject: [PATCH 042/334] Make sure child windows have unique titles/ids. --- samples/IntegrationTestApp/MainWindow.axaml.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/samples/IntegrationTestApp/MainWindow.axaml.cs b/samples/IntegrationTestApp/MainWindow.axaml.cs index 2085b5da2b..f72f83fcb8 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml.cs +++ b/samples/IntegrationTestApp/MainWindow.axaml.cs @@ -1,11 +1,13 @@ using System.Collections.Generic; using System.Linq; using Avalonia; +using Avalonia.Automation; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Interactivity; using Avalonia.Markup.Xaml; using Avalonia.VisualTree; +using Microsoft.CodeAnalysis; namespace IntegrationTestApp { @@ -63,6 +65,17 @@ namespace IntegrationTestApp WindowStartupLocation = (WindowStartupLocation)locationComboBox.SelectedIndex, }; + if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime) + { + // Make sure the windows have unique names and AutomationIds. + var existing = lifetime.Windows.OfType().Count(); + if (existing > 0) + { + AutomationProperties.SetAutomationId(window, window.Name + (existing + 1)); + window.Title += $" {existing + 1}"; + } + } + if (size.HasValue) { window.Width = size.Value.Width; From 1c4a994e2c8796096821c92c8137db0f85e8249f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 1 Sep 2022 22:36:12 +0200 Subject: [PATCH 043/334] Add failing test for #8878. And update the tests to use the value of `NSWindow.orderedIndex` exposed in 0cc0443d34567f0d20082eaee29625a732b93849 instead of relying on appium to give us a z-ordered list of windows. --- .../WindowTests_MacOS.cs | 89 +++++++++---------- 1 file changed, 43 insertions(+), 46 deletions(-) diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs index 4e5344dd25..7eeab2f7f2 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs @@ -30,7 +30,7 @@ namespace Avalonia.IntegrationTests.Appium tab.Click(); return; } - catch (WebDriverException e) when (retry++ < 3) + catch (WebDriverException) when (retry++ < 3) { // MacOS sometimes seems to need a bit of time to get itself back in order after switching out // of fullscreen. @@ -49,19 +49,16 @@ namespace Avalonia.IntegrationTests.Appium { mainWindow.Click(); - var windows = _session.FindElements(By.XPath("XCUIElementTypeWindow")); - var mainWindowIndex = GetWindowOrder(windows, "MainWindow"); - var secondaryWindowIndex = GetWindowOrder(windows, "SecondaryWindow"); + var secondaryWindowIndex = GetWindowOrder("SecondaryWindow"); - Assert.Equal(0, secondaryWindowIndex); - Assert.Equal(1, mainWindowIndex); + Assert.Equal(1, secondaryWindowIndex); } } [PlatformFact(TestPlatforms.MacOS)] public void WindowOrder_Modal_Dialog_Stays_InFront_Of_Parent_When_Clicking_Resize_Grip() { - var mainWindow = FindWindow(_session, "MainWindow"); + var mainWindow = GetWindow("MainWindow"); using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Modal, WindowStartupLocation.Manual)) { @@ -70,24 +67,21 @@ namespace Avalonia.IntegrationTests.Appium .ClickAndHold() .Perform(); - var windows = _session.FindElements(By.XPath("XCUIElementTypeWindow")); - var mainWindowIndex = GetWindowOrder(windows, "MainWindow"); - var secondaryWindowIndex = GetWindowOrder(windows, "SecondaryWindow"); + var secondaryWindowIndex = GetWindowOrder("SecondaryWindow"); new Actions(_session) .MoveToElement(mainWindow, 100, 1) .Release() .Perform(); - Assert.Equal(0, secondaryWindowIndex); - Assert.Equal(1, mainWindowIndex); + Assert.Equal(1, secondaryWindowIndex); } } [PlatformFact(TestPlatforms.MacOS)] public void WindowOrder_Modal_Dialog_Stays_InFront_Of_Parent_When_In_Fullscreen() { - var mainWindow = FindWindow(_session, "MainWindow"); + var mainWindow = GetWindow("MainWindow"); var buttons = mainWindow.GetChromeButtons(); buttons.maximize.Click(); @@ -98,14 +92,8 @@ namespace Avalonia.IntegrationTests.Appium { using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Modal, WindowStartupLocation.Manual)) { - var windows = _session.FindElements(By.XPath("XCUIElementTypeWindow")); - var mainWindowIndex = GetWindowOrder(windows, "MainWindow"); - var secondaryWindowIndex = GetWindowOrder(windows, "SecondaryWindow"); - - Assert.Equal(0, secondaryWindowIndex); - Assert.Equal(1, mainWindowIndex); - - Thread.Sleep(5000); + var secondaryWindowIndex = GetWindowOrder("SecondaryWindow"); + Assert.Equal(1, secondaryWindowIndex); } } finally @@ -122,13 +110,8 @@ namespace Avalonia.IntegrationTests.Appium using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Owned, WindowStartupLocation.Manual)) { mainWindow.Click(); - - var windows = _session.FindElements(By.XPath("XCUIElementTypeWindow")); - var mainWindowIndex = GetWindowOrder(windows, "MainWindow"); - var secondaryWindowIndex = GetWindowOrder(windows, "SecondaryWindow"); - - Assert.Equal(0, secondaryWindowIndex); - Assert.Equal(1, mainWindowIndex); + var secondaryWindowIndex = GetWindowOrder("SecondaryWindow"); + Assert.Equal(1, secondaryWindowIndex); } } @@ -141,22 +124,35 @@ namespace Avalonia.IntegrationTests.Appium { mainWindow.Click(); - var windows = _session.FindElements(By.XPath("XCUIElementTypeWindow")); - var mainWindowIndex = GetWindowOrder(windows, "MainWindow"); - var secondaryWindowIndex = GetWindowOrder(windows, "SecondaryWindow"); + var secondaryWindowIndex = GetWindowOrder("SecondaryWindow"); - Assert.Equal(1, secondaryWindowIndex); - Assert.Equal(0, mainWindowIndex); + Assert.Equal(2, secondaryWindowIndex); var sendToBack = _session.FindElementByAccessibilityId("SendToBack"); sendToBack.Click(); } } + [PlatformFact(TestPlatforms.MacOS)] + public void WindowOrder_Owned_Is_Correct_After_Closing_Window() + { + using (OpenWindow(new PixelSize(300, 500), ShowWindowMode.Owned, WindowStartupLocation.CenterOwner)) + { + // Open a second child window, and close it. + using (OpenWindow(new PixelSize(200, 200), ShowWindowMode.Owned, WindowStartupLocation.CenterOwner)) + { + } + + var secondaryWindowIndex = GetWindowOrder("SecondaryWindow"); + + Assert.Equal(1, secondaryWindowIndex); + } + } + [PlatformFact(TestPlatforms.MacOS)] public void Parent_Window_Has_Disabled_ChromeButtons_When_Modal_Dialog_Shown() { - var window = FindWindow(_session, "MainWindow"); + var window = GetWindow("MainWindow"); var (closeButton, miniaturizeButton, zoomButton) = window.GetChromeButtons(); Assert.True(closeButton.Enabled); @@ -176,7 +172,7 @@ namespace Avalonia.IntegrationTests.Appium { using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Modal, WindowStartupLocation.CenterOwner)) { - var secondaryWindow = FindWindow(_session, "SecondaryWindow"); + var secondaryWindow = GetWindow("SecondaryWindow"); var (closeButton, miniaturizeButton, zoomButton) = secondaryWindow.GetChromeButtons(); Assert.True(closeButton.Enabled); @@ -192,7 +188,7 @@ namespace Avalonia.IntegrationTests.Appium { using (OpenWindow(new PixelSize(200, 100), mode, WindowStartupLocation.Manual)) { - var secondaryWindow = FindWindow(_session, "SecondaryWindow"); + var secondaryWindow = GetWindow("SecondaryWindow"); var (_, miniaturizeButton, _) = secondaryWindow.GetChromeButtons(); miniaturizeButton.Click(); @@ -220,7 +216,7 @@ namespace Avalonia.IntegrationTests.Appium // causes Appium to think it's a different window. OpenWindow(null, ShowWindowMode.Owned, WindowStartupLocation.Manual); - var secondaryWindow = FindWindow(_session, "SecondaryWindow"); + var secondaryWindow = GetWindow("SecondaryWindow"); var hideButton = secondaryWindow.FindElementByAccessibilityId("HideButton"); hideButton.Click(); @@ -236,7 +232,7 @@ namespace Avalonia.IntegrationTests.Appium _session.FindElementByAccessibilityId("RestoreAll").Click(); // Close the window manually. - secondaryWindow = FindWindow(_session, "SecondaryWindow"); + secondaryWindow = GetWindow("SecondaryWindow"); secondaryWindow.GetChromeButtons().close.Click(); } @@ -259,18 +255,19 @@ namespace Avalonia.IntegrationTests.Appium return showButton.OpenWindowWithClick(); } - private static int GetWindowOrder(IReadOnlyCollection elements, string identifier) + private AppiumWebElement GetWindow(string identifier) { - return elements.TakeWhile(x => - x.FindElementByXPath("XCUIElementTypeWindow")?.GetAttribute("identifier") != identifier).Count(); + // The Avalonia a11y tree currently exposes two nested Window elements, this is a bug and should be fixed + // but in the meantime use the `parent::' selector to return the parent "real" window. + return _session.FindElementByXPath( + $"XCUIElementTypeWindow//*[@identifier='{identifier}']/parent::XCUIElementTypeWindow"); } - private static AppiumWebElement FindWindow(AppiumDriver session, string identifier) + private int GetWindowOrder(string identifier) { - var windows = session.FindElementsByXPath("XCUIElementTypeWindow"); - return windows.First(x => - x.FindElementsByXPath("XCUIElementTypeWindow") - .Any(y => y.GetAttribute("identifier") == identifier)); + var window = GetWindow(identifier); + var order = window.FindElementByXPath("//*[@identifier='Order']"); + return int.Parse(order.Text); } public enum ShowWindowMode From b83b795f57f625b8d9e794008ea14c612dbb79f8 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 1 Sep 2022 22:38:47 +0200 Subject: [PATCH 044/334] Don't bring old parent window to front. When a window is closed, its parent window is set to null, which caused this code to run. This line caused the parent window to be brought to front, and should have also caused any other child windows to be brought to front, but it seems OSX ignored those requests, causing #8878. Simply don't bring the parent window to the front when a child window is closed. I couldn't work out why this code was necessary anyway, and all integration tests still pass. Fixes #8878. --- native/Avalonia.Native/src/OSX/WindowImpl.mm | 2 -- 1 file changed, 2 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/WindowImpl.mm b/native/Avalonia.Native/src/OSX/WindowImpl.mm index af8c53cb33..ddc50c26b6 100644 --- a/native/Avalonia.Native/src/OSX/WindowImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowImpl.mm @@ -91,8 +91,6 @@ HRESULT WindowImpl::SetParent(IAvnWindow *parent) { if(_parent != nullptr) { _parent->_children.remove(this); - - _parent->BringToFront(); } auto cparent = dynamic_cast(parent); From cc4b8e3ea08037d19ddaf5270eab428da82b28af Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 2 Sep 2022 10:33:06 +0200 Subject: [PATCH 045/334] Fix typo. --- tests/Avalonia.Controls.UnitTests/BorderTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/BorderTests.cs b/tests/Avalonia.Controls.UnitTests/BorderTests.cs index 7af7d1cee2..2cb72f148d 100644 --- a/tests/Avalonia.Controls.UnitTests/BorderTests.cs +++ b/tests/Avalonia.Controls.UnitTests/BorderTests.cs @@ -78,7 +78,7 @@ namespace Avalonia.Controls.UnitTests } }; - var root = CreatedRoot(1.5, target); + var root = CreateRoot(1.5, target); root.LayoutManager.ExecuteInitialLayoutPass(); @@ -101,7 +101,7 @@ namespace Avalonia.Controls.UnitTests } }; - var root = CreatedRoot(1.5, target); + var root = CreateRoot(1.5, target); root.LayoutManager.ExecuteInitialLayoutPass(); @@ -111,7 +111,7 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Size(104, 104), target.DesiredSize); } - private static TestRoot CreatedRoot( + private static TestRoot CreateRoot( double scaling, Control child, Size? constraint = null) From ac27138120f4b554c61c9e44473713acb0c14a7b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 2 Sep 2022 12:28:56 +0200 Subject: [PATCH 046/334] Add failing tests for #8866. --- .../BorderTests.cs | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/BorderTests.cs b/tests/Avalonia.Controls.UnitTests/BorderTests.cs index 2cb72f148d..e9ddc8e8a3 100644 --- a/tests/Avalonia.Controls.UnitTests/BorderTests.cs +++ b/tests/Avalonia.Controls.UnitTests/BorderTests.cs @@ -111,6 +111,57 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Size(104, 104), target.DesiredSize); } + [Fact] + public void Measure_Arranges_Child_To_Rounded_BorderThickness() + { + Canvas child; + var target = new Border + { + BorderThickness = new Thickness(1), + Width = 82, + Height = 82, + Child = child = new Canvas(), + }; + + var root = CreateRoot(1.5, target); + + root.LayoutManager.ExecuteInitialLayoutPass(); + + // - 1 pixel border thickness is rounded up to 1.3333; for both sides it is 2.6666 + // - Size of 82 needs no rounding + // - Minus border thickness, space for child is 82 - 2.6666 = 79.3333 + Assert.Equal(1.3333, child.Bounds.Left, 3); + Assert.Equal(1.3333, child.Bounds.Top, 3); + Assert.Equal(79.3333, child.Bounds.Width, 3); + Assert.Equal(79.3333, child.Bounds.Height, 3); + } + + [Fact] + public void Measure_Arranges_Child_With_Rounded_Margin() + { + Border child; + var target = new Border + { + Width = 220, + Height = 220, + Child = child = new Border + { + Margin = new Thickness(0, 25, 25, 25), + }, + }; + + var root = CreateRoot(1.5, target); + + root.LayoutManager.ExecuteInitialLayoutPass(); + + // - 25 margin gets rounded up to 25.3333 + // - Size of 220 needs no rounding + Assert.Equal(0, child.Bounds.Left, 3); + Assert.Equal(25.3333, child.Bounds.Top, 3); + Assert.Equal(194.6666, child.Bounds.Width, 3); + Assert.Equal(169.3333, child.Bounds.Height, 3); + } + private static TestRoot CreateRoot( double scaling, Control child, From 76747d32b4cd04ef6cce8718120f28f805a865eb Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 2 Sep 2022 12:32:51 +0200 Subject: [PATCH 047/334] Handle FP errors in RoundLayoutValueUp. If the `value` passed to `RoundLayoutValueUp` has a floating point precision error (e.g. 79.333333333333343 in the test case) then when it's multiplied by `dpiScale` and rounded up, it will be rounded up to the a value one greater than it should be. Fix this by rounding the initial value to an arbitrary precision of 8 digits. Fixes #8866 --- src/Avalonia.Base/Layout/LayoutHelper.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Avalonia.Base/Layout/LayoutHelper.cs b/src/Avalonia.Base/Layout/LayoutHelper.cs index 404d19906a..0851dbaea9 100644 --- a/src/Avalonia.Base/Layout/LayoutHelper.cs +++ b/src/Avalonia.Base/Layout/LayoutHelper.cs @@ -251,6 +251,17 @@ namespace Avalonia.Layout { double newValue; + // Round the value to avoid FP errors. This is needed because if `value` has a floating + // point precision error (e.g. 79.333333333333343) then when it's multiplied by + // `dpiScale` and rounded up, it will be rounded up to a value one greater than it + // should be. +#if NET6_0_OR_GREATER + value = Math.Round(value, 8, MidpointRounding.ToZero); +#else + // MidpointRounding.ToZero isn't available in netstandard2.0. + value = Math.Truncate(value * 1e8) / 1e8; +#endif + // If DPI == 1, don't use DPI-aware rounding. if (!MathUtilities.IsOne(dpiScale)) { From 133a95973ad3b4394c1cab63b5ed2db73feb9dd2 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 2 Sep 2022 12:11:02 +0100 Subject: [PATCH 048/334] use seperate responder class --- src/iOS/Avalonia.iOS/AvaloniaView.Text.cs | 130 +++++---------------- src/iOS/Avalonia.iOS/AvaloniaView.cs | 135 ++++++++++++++++++++-- 2 files changed, 153 insertions(+), 112 deletions(-) diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.Text.cs b/src/iOS/Avalonia.iOS/AvaloniaView.Text.cs index 99b2b7ad71..791987eef3 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaView.Text.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaView.Text.cs @@ -15,7 +15,7 @@ namespace Avalonia.iOS; [Adopts("UITextInput")] [Adopts("UITextInputTraits")] [Adopts("UIKeyInput")] -public partial class AvaloniaView : ITextInputMethodImpl, IUITextInput +public class AvaloniaResponder : UIResponder, IUITextInput { private class AvaloniaTextRange : UITextRange { @@ -46,6 +46,23 @@ public partial class AvaloniaView : ITextInputMethodImpl, IUITextInput //[Export("inputDelegate")] //public UITextInputDelegate InputDelegate { get; set; } + + [Export("foo")] + public void Foo() + { + + } + } + + public AvaloniaResponder() + { + _tokenizer = new UITextInputStringTokenizer(this); + + } + + public override bool CanPerform(Selector action, NSObject? withSender) + { + return base.CanPerform(action, withSender); } private string _markedText = ""; @@ -62,27 +79,13 @@ public partial class AvaloniaView : ITextInputMethodImpl, IUITextInput public override bool CanBecomeFirstResponder => true; - void ITextInputMethodImpl.SetClient(ITextInputMethodClient? client) - { - if (_client != null) - { - _client.CursorRectangleChanged -= ClientOnCursorRectangleChanged; - } - - _client = client; + public override UIEditingInteractionConfiguration EditingInteractionConfiguration => + UIEditingInteractionConfiguration.Default; + + public override NSString TextInputContextIdentifier => new NSString("Test"); + + public override UITextInputMode TextInputMode => UITextInputMode.CurrentInputMode; - if (_client is { }) - { - _client.CursorRectangleChanged += ClientOnCursorRectangleChanged; - _client.SurroundingTextChanged += ClientOnSurroundingTextChanged; - BecomeFirstResponder(); - } - else - { - ResignFirstResponder(); - } - } - [DllImport("/usr/lib/libobjc.dylib")] extern static void objc_msgSend(IntPtr receiver, IntPtr selector, IntPtr arg); @@ -103,79 +106,6 @@ public partial class AvaloniaView : ITextInputMethodImpl, IUITextInput objc_msgSend(WeakInputDelegate.Handle.Handle, TextDidChange, Handle.Handle); } - void ITextInputMethodImpl.SetCursorRect(Rect rect) - { - // maybe this will be cursor / selection rect? - } - - void ITextInputMethodImpl.SetOptions(TextInputOptions options) - { - IsSecureEntry = false; - - switch (options.ContentType) - { - case TextInputContentType.Normal: - KeyboardType = UIKeyboardType.Default; - break; - - case TextInputContentType.Alpha: - KeyboardType = UIKeyboardType.AsciiCapable; - break; - - case TextInputContentType.Digits: - KeyboardType = UIKeyboardType.PhonePad; - break; - - case TextInputContentType.Pin: - KeyboardType = UIKeyboardType.NumberPad; - IsSecureEntry = true; - break; - - case TextInputContentType.Number: - KeyboardType = UIKeyboardType.PhonePad; - break; - - case TextInputContentType.Email: - KeyboardType = UIKeyboardType.EmailAddress; - break; - - case TextInputContentType.Url: - KeyboardType = UIKeyboardType.Url; - break; - - case TextInputContentType.Name: - KeyboardType = UIKeyboardType.NamePhonePad; - break; - - case TextInputContentType.Password: - KeyboardType = UIKeyboardType.Default; - IsSecureEntry = true; - break; - - case TextInputContentType.Social: - KeyboardType = UIKeyboardType.Twitter; - break; - - case TextInputContentType.Search: - KeyboardType = UIKeyboardType.WebSearch; - break; - } - - if (options.IsSensitive) - { - IsSecureEntry = true; - } - - ReturnKeyType = (UIReturnKeyType)options.ReturnKeyType; - AutocorrectionType = UITextAutocorrectionType.Yes; - } - - - void ITextInputMethodImpl.Reset() - { - ResignFirstResponder(); - } - // Traits (Optional) [Export("autocapitalizationType")] public UITextAutocapitalizationType AutocapitalizationType { get; private set; } @@ -233,8 +163,8 @@ public partial class AvaloniaView : ITextInputMethodImpl, IUITextInput // TODO replace this with _client.SetCommitText? if (KeyboardDevice.Instance is { }) { - _topLevelImpl.Input?.Invoke(new RawTextInputEventArgs(KeyboardDevice.Instance, - 0, InputRoot, text)); + /*_topLevelImpl.Input?.Invoke(new RawTextInputEventArgs(KeyboardDevice.Instance, + 0, InputRoot, text));*/ } } @@ -243,11 +173,11 @@ public partial class AvaloniaView : ITextInputMethodImpl, IUITextInput if (KeyboardDevice.Instance is { }) { // TODO: pass this through IME infrastructure instead of emulating a backspace press - _topLevelImpl.Input?.Invoke(new RawKeyEventArgs(KeyboardDevice.Instance, + /*_topLevelImpl.Input?.Invoke(new RawKeyEventArgs(KeyboardDevice.Instance, 0, InputRoot, RawKeyEventType.KeyDown, Key.Back, RawInputModifiers.None)); _topLevelImpl.Input?.Invoke(new RawKeyEventArgs(KeyboardDevice.Instance, - 0, InputRoot, RawKeyEventType.KeyUp, Key.Back, RawInputModifiers.None)); + 0, InputRoot, RawKeyEventType.KeyUp, Key.Back, RawInputModifiers.None));*/ } } @@ -279,8 +209,8 @@ public partial class AvaloniaView : ITextInputMethodImpl, IUITextInput // todo _client.SetCommitText(text); if (KeyboardDevice.Instance is { }) { - _topLevelImpl.Input?.Invoke(new RawTextInputEventArgs(KeyboardDevice.Instance, - 0, InputRoot, text)); + /*_topLevelImpl.Input?.Invoke(new RawTextInputEventArgs(KeyboardDevice.Instance, + 0, InputRoot, text));*/ } } diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.cs b/src/iOS/Avalonia.iOS/AvaloniaView.cs index 06201e366b..12c1470a52 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaView.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaView.cs @@ -19,38 +19,47 @@ using UIKit; namespace Avalonia.iOS { - public partial class AvaloniaView : UIView + public partial class AvaloniaView : UIView, ITextInputMethodImpl { internal IInputRoot InputRoot { get; private set; } private TopLevelImpl _topLevelImpl; private EmbeddableControlRoot _topLevel; private TouchHandler _touches; + private ITextInputMethodClient _client; + private bool _isActive; + private AvaloniaResponder _textResponder; public AvaloniaView() { + _textResponder = new AvaloniaResponder(); _topLevelImpl = new TopLevelImpl(this); _touches = new TouchHandler(this, _topLevelImpl); _topLevel = new EmbeddableControlRoot(_topLevelImpl); - - _tokenizer = new UITextInputStringTokenizer(this); - + _topLevel.Prepare(); - + _topLevel.Renderer.Start(); - - var l = (CAEAGLLayer) Layer; + + var l = (CAEAGLLayer)Layer; l.ContentsScale = UIScreen.MainScreen.Scale; l.Opaque = true; l.DrawableProperties = new NSDictionary( EAGLDrawableProperty.RetainedBacking, false, EAGLDrawableProperty.ColorFormat, EAGLColorFormat.RGBA8 ); - _topLevelImpl.Surfaces = new[] {new EaglLayerSurface(l)}; + _topLevelImpl.Surfaces = new[] { new EaglLayerSurface(l) }; MultipleTouchEnabled = true; AddSubviews(new UIView[] { new UIKit.UIButton(UIButtonType.InfoDark) }); } - internal class TopLevelImpl : ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost, ITopLevelImplWithStorageProvider + public override bool CanBecomeFirstResponder => true; + + public override bool CanResignFirstResponder => true; + + public override UIResponder NextResponder => _textResponder; + + internal class TopLevelImpl : ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost, + ITopLevelImplWithStorageProvider { private readonly AvaloniaView _view; public AvaloniaView View => _view; @@ -68,7 +77,7 @@ namespace Avalonia.iOS } public IRenderer CreateRenderer(IRenderRoot root) => new CompositingRenderer(root, Platform.Compositor); - + public void Invalidate(Rect rect) { @@ -82,7 +91,7 @@ namespace Avalonia.iOS public Point PointToClient(PixelPoint point) => new Point(point.X, point.Y); - public PixelPoint PointToScreen(Point point) => new PixelPoint((int) point.X, (int) point.Y); + public PixelPoint PointToScreen(Point point) => new PixelPoint((int)point.X, (int)point.Y); public void SetCursor(ICursorImpl _) { @@ -121,7 +130,7 @@ namespace Avalonia.iOS new AcrylicPlatformCompensationLevels(); public ITextInputMethodImpl? TextInputMethod => _view; - public INativeControlHostImpl NativeControlHost { get; } + public INativeControlHostImpl NativeControlHost { get; } public IStorageProvider StorageProvider { get; } } @@ -150,5 +159,107 @@ namespace Avalonia.iOS get => (Control)_topLevel.Content; set => _topLevel.Content = value; } + + ITextInputMethodClient ITextInputMethodImpl.Client => _client; + + bool ITextInputMethodImpl.IsActive => _isActive; + + void ITextInputMethodImpl.SetClient(ITextInputMethodClient client) + { + if (_client != null) + { + //_client.CursorRectangleChanged -= ClientOnCursorRectangleChanged; + } + + _client = client; + + if (_client is { }) + { + //_client.CursorRectangleChanged += ClientOnCursorRectangleChanged; + //_client.SurroundingTextChanged += ClientOnSurroundingTextChanged; + _textResponder.BecomeFirstResponder(); + + var x = _textResponder.IsFirstResponder; + } + else + { + _textResponder.ResignFirstResponder(); + } + } + + void ITextInputMethodImpl.SetCursorRect(Rect rect) + { + // maybe this will be cursor / selection rect? + } + + void ITextInputMethodImpl.SetOptions(TextInputOptions options) + { + /*IsSecureEntry = false; + + switch (options.ContentType) + { + case TextInputContentType.Normal: + KeyboardType = UIKeyboardType.Default; + break; + + case TextInputContentType.Alpha: + KeyboardType = UIKeyboardType.AsciiCapable; + break; + + case TextInputContentType.Digits: + KeyboardType = UIKeyboardType.PhonePad; + break; + + case TextInputContentType.Pin: + KeyboardType = UIKeyboardType.NumberPad; + IsSecureEntry = true; + break; + + case TextInputContentType.Number: + KeyboardType = UIKeyboardType.PhonePad; + break; + + case TextInputContentType.Email: + KeyboardType = UIKeyboardType.EmailAddress; + break; + + case TextInputContentType.Url: + KeyboardType = UIKeyboardType.Url; + break; + + case TextInputContentType.Name: + KeyboardType = UIKeyboardType.NamePhonePad; + break; + + case TextInputContentType.Password: + KeyboardType = UIKeyboardType.Default; + IsSecureEntry = true; + break; + + case TextInputContentType.Social: + KeyboardType = UIKeyboardType.Twitter; + break; + + case TextInputContentType.Search: + KeyboardType = UIKeyboardType.WebSearch; + break; + } + + if (options.IsSensitive) + { + IsSecureEntry = true; + } + + ReturnKeyType = (UIReturnKeyType)options.ReturnKeyType; + AutocorrectionType = UITextAutocorrectionType.Yes; + SpellCheckingType = UITextSpellCheckingType.Yes; + KeyboardAppearance = UIKeyboardAppearance.Alert;*/ + } + + + void ITextInputMethodImpl.Reset() + { + _textResponder?.ResignFirstResponder(); + } } } From 8d9f39d3b3bdbc97c52bb91b30754e159e20fc0c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 2 Sep 2022 13:19:45 +0200 Subject: [PATCH 049/334] Fix copypasta. Fixes #8867. --- src/Avalonia.Controls/Utils/BorderRenderHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Utils/BorderRenderHelper.cs b/src/Avalonia.Controls/Utils/BorderRenderHelper.cs index ecc7cd98bb..eb9f38894d 100644 --- a/src/Avalonia.Controls/Utils/BorderRenderHelper.cs +++ b/src/Avalonia.Controls/Utils/BorderRenderHelper.cs @@ -61,7 +61,7 @@ namespace Avalonia.Controls.Utils _backgroundGeometryCache = null; } - if (boundRect.Width != 0 && innerRect.Height != 0) + if (boundRect.Width != 0 && boundRect.Height != 0) { var borderGeometryKeypoints = new BorderGeometryKeypoints(boundRect, borderThickness, cornerRadius, false); From f9b4c5042c0d330eb24eaae832e9ab51aa416e95 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 2 Sep 2022 20:25:44 +0100 Subject: [PATCH 050/334] add fix for window becoming keywindow. --- src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs | 7 +++++-- src/iOS/Avalonia.iOS/AvaloniaView.cs | 6 ++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs b/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs index f976b2feb4..b8889a4182 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaAppDelegate.cs @@ -36,6 +36,8 @@ namespace Avalonia.iOS builder.AfterSetup(_ => { Window = new UIWindow(); + + var view = new AvaloniaView(); lifetime.View = view; Window.RootViewController = new UIViewController @@ -45,8 +47,9 @@ namespace Avalonia.iOS }); builder.SetupWithLifetime(lifetime); - - Window.Hidden = false; + + Window.MakeKeyAndVisible(); + return true; } } diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.cs b/src/iOS/Avalonia.iOS/AvaloniaView.cs index 12c1470a52..e2fae8db88 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaView.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaView.cs @@ -180,6 +180,12 @@ namespace Avalonia.iOS _textResponder.BecomeFirstResponder(); var x = _textResponder.IsFirstResponder; + + var keyWindow = UIApplication.SharedApplication.KeyWindow; + + var windowNextResponder = keyWindow.NextResponder; + + var nr1 = windowNextResponder.NextResponder; } else { From 9a4cd3d8c91929de2f9f2ae3fef1380593240051 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 2 Sep 2022 22:45:27 +0100 Subject: [PATCH 051/334] fix responder issue. --- src/iOS/Avalonia.iOS/AvaloniaView.Text.cs | 11 +++++++-- src/iOS/Avalonia.iOS/AvaloniaView.cs | 28 +++++++++++------------ 2 files changed, 23 insertions(+), 16 deletions(-) diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.Text.cs b/src/iOS/Avalonia.iOS/AvaloniaView.Text.cs index 791987eef3..50f5b26728 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaView.Text.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaView.Text.cs @@ -54,12 +54,18 @@ public class AvaloniaResponder : UIResponder, IUITextInput } } - public AvaloniaResponder() + private UIResponder _nextResponder; + + public AvaloniaResponder(AvaloniaView view, ITextInputMethodClient client) { + _nextResponder = view; + _client = client; _tokenizer = new UITextInputStringTokenizer(this); } + public override UIResponder NextResponder => _nextResponder; + public override bool CanPerform(Selector action, NSObject? withSender) { return base.CanPerform(action, withSender); @@ -241,7 +247,8 @@ public class AvaloniaResponder : UIResponder, IUITextInput return new AvaloniaTextRange(range); } - throw new Exception(); + return null; + //throw new Exception(); } UITextPosition IUITextInput.GetPosition(UITextPosition fromPosition, nint offset) diff --git a/src/iOS/Avalonia.iOS/AvaloniaView.cs b/src/iOS/Avalonia.iOS/AvaloniaView.cs index e2fae8db88..0038a03911 100644 --- a/src/iOS/Avalonia.iOS/AvaloniaView.cs +++ b/src/iOS/Avalonia.iOS/AvaloniaView.cs @@ -27,11 +27,9 @@ namespace Avalonia.iOS private TouchHandler _touches; private ITextInputMethodClient _client; private bool _isActive; - private AvaloniaResponder _textResponder; public AvaloniaView() { - _textResponder = new AvaloniaResponder(); _topLevelImpl = new TopLevelImpl(this); _touches = new TouchHandler(this, _topLevelImpl); _topLevel = new EmbeddableControlRoot(_topLevelImpl); @@ -56,8 +54,6 @@ namespace Avalonia.iOS public override bool CanResignFirstResponder => true; - public override UIResponder NextResponder => _textResponder; - internal class TopLevelImpl : ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost, ITopLevelImplWithStorageProvider { @@ -164,6 +160,8 @@ namespace Avalonia.iOS bool ITextInputMethodImpl.IsActive => _isActive; + private AvaloniaResponder _currentResponder; + void ITextInputMethodImpl.SetClient(ITextInputMethodClient client) { if (_client != null) @@ -175,21 +173,23 @@ namespace Avalonia.iOS if (_client is { }) { - //_client.CursorRectangleChanged += ClientOnCursorRectangleChanged; - //_client.SurroundingTextChanged += ClientOnSurroundingTextChanged; - _textResponder.BecomeFirstResponder(); + if (_currentResponder != null && _currentResponder.IsFirstResponder) + { + _currentResponder.ResignFirstResponder(); + _currentResponder = null; + } - var x = _textResponder.IsFirstResponder; - - var keyWindow = UIApplication.SharedApplication.KeyWindow; + _currentResponder = new AvaloniaResponder(this, _client); + //_client.CursorRectangleChanged += ClientOnCursorRectangleChanged; - var windowNextResponder = keyWindow.NextResponder; + //_client.SurroundingTextChanged += ClientOnSurroundingTextChanged; + _currentResponder.BecomeFirstResponder(); - var nr1 = windowNextResponder.NextResponder; + var x = _currentResponder.IsFirstResponder; } else { - _textResponder.ResignFirstResponder(); + _currentResponder.ResignFirstResponder(); } } @@ -265,7 +265,7 @@ namespace Avalonia.iOS void ITextInputMethodImpl.Reset() { - _textResponder?.ResignFirstResponder(); + _currentResponder?.ResignFirstResponder(); } } } From f69ae35158819d7040e9dec6da9f2f840840015f Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Wed, 7 Sep 2022 16:46:22 +0300 Subject: [PATCH 052/334] Fix unpredictable choice between methods when using method binding. --- .../Data/Core/Plugins/MethodAccessorPlugin.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs index 1ca70140ec..0d51a6ed36 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs @@ -55,13 +55,20 @@ namespace Avalonia.Data.Core.Plugins var methods = type.GetMethods(bindingFlags); - foreach (MethodInfo methodInfo in methods) + foreach (var methodInfo in methods) { if (methodInfo.Name == methodName) { - found = methodInfo; - - break; + var parameters = methodInfo.GetParameters(); + if (parameters.Length == 1 && parameters[0].ParameterType == typeof(object)) + { + found = methodInfo; + break; + } + else if (parameters.Length == 0) + { + found = methodInfo; + } } } From 9545bbd6b08f38abdf2c9a877b4e0a2145b990d9 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Wed, 7 Sep 2022 15:22:51 +0200 Subject: [PATCH 053/334] fix(DevGenerators): Some null annotation --- .../CompositionRoslynGenerator.cs | 7 +++-- .../CompositionGenerator/Extensions.cs | 30 +++++++++---------- .../CompositionGenerator/Generator.Utils.cs | 4 +-- .../CompositionGenerator/Generator.cs | 16 +++++----- src/tools/DevGenerators/DevGenerators.csproj | 1 + .../GetProcAddressInitialization.cs | 11 ++++--- 6 files changed, 36 insertions(+), 33 deletions(-) diff --git a/src/tools/DevGenerators/CompositionGenerator/CompositionRoslynGenerator.cs b/src/tools/DevGenerators/CompositionGenerator/CompositionRoslynGenerator.cs index 72311b4d18..26bd4796df 100644 --- a/src/tools/DevGenerators/CompositionGenerator/CompositionRoslynGenerator.cs +++ b/src/tools/DevGenerators/CompositionGenerator/CompositionRoslynGenerator.cs @@ -11,8 +11,9 @@ namespace Avalonia.SourceGenerator.CompositionGenerator { var schema = context.AdditionalTextsProvider.Where(static file => file.Path.EndsWith("composition-schema.xml")); - var configs = schema.Select((t, _) => - (GConfig)new XmlSerializer(typeof(GConfig)).Deserialize(new StringReader(t.GetText().ToString()))); + var configs = schema.Select((t, _) => t.GetText()) + .Where(source => source is not null) + .Select((source, _) => (GConfig)new XmlSerializer(typeof(GConfig)).Deserialize(new StringReader(source!.ToString()))); context.RegisterSourceOutput(configs, (spc, config) => { var generator = new Generator(new RoslynCompositionGeneratorSink(spc), config); @@ -20,4 +21,4 @@ namespace Avalonia.SourceGenerator.CompositionGenerator }); } } -} \ No newline at end of file +} diff --git a/src/tools/DevGenerators/CompositionGenerator/Extensions.cs b/src/tools/DevGenerators/CompositionGenerator/Extensions.cs index d88e9b4600..f34765ec36 100644 --- a/src/tools/DevGenerators/CompositionGenerator/Extensions.cs +++ b/src/tools/DevGenerators/CompositionGenerator/Extensions.cs @@ -7,49 +7,47 @@ namespace Avalonia.SourceGenerator.CompositionGenerator { public static class Extensions { - public static ClassDeclarationSyntax AddModifiers(this ClassDeclarationSyntax cl, params SyntaxKind[] modifiers) + public static ClassDeclarationSyntax AddModifiers(this ClassDeclarationSyntax cl, params SyntaxKind[]? modifiers) { - if (modifiers == null) + if (modifiers is null) return cl; return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); } - public static MethodDeclarationSyntax AddModifiers(this MethodDeclarationSyntax cl, params SyntaxKind[] modifiers) + public static MethodDeclarationSyntax AddModifiers(this MethodDeclarationSyntax cl, params SyntaxKind[]? modifiers) { - if (modifiers == null) + if (modifiers is null) return cl; return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); } - public static PropertyDeclarationSyntax AddModifiers(this PropertyDeclarationSyntax cl, params SyntaxKind[] modifiers) + public static PropertyDeclarationSyntax AddModifiers(this PropertyDeclarationSyntax cl, params SyntaxKind[]? modifiers) { - if (modifiers == null) + if (modifiers is null) return cl; return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); } - public static ConstructorDeclarationSyntax AddModifiers(this ConstructorDeclarationSyntax cl, params SyntaxKind[] modifiers) + public static ConstructorDeclarationSyntax AddModifiers(this ConstructorDeclarationSyntax cl, params SyntaxKind[]? modifiers) { - if (modifiers == null) + if (modifiers is null) return cl; return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); } - public static AccessorDeclarationSyntax AddModifiers(this AccessorDeclarationSyntax cl, params SyntaxKind[] modifiers) + public static AccessorDeclarationSyntax AddModifiers(this AccessorDeclarationSyntax cl, params SyntaxKind[]? modifiers) { - if (modifiers == null) + if (modifiers is null) return cl; return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); } - public static EnumDeclarationSyntax AddModifiers(this EnumDeclarationSyntax cl, params SyntaxKind[] modifiers) + public static EnumDeclarationSyntax AddModifiers(this EnumDeclarationSyntax cl, params SyntaxKind[]? modifiers) { - if (modifiers == null) + if (modifiers is null) return cl; return cl.AddModifiers(modifiers.Select(x => SyntaxFactory.Token(x)).ToArray()); } - - public static string WithLowerFirst(this string s) { @@ -58,7 +56,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator return char.ToLowerInvariant(s[0]) + s.Substring(1); } - public static ExpressionSyntax MemberAccess(params string[] identifiers) + public static ExpressionSyntax MemberAccess(params string[]? identifiers) { if (identifiers == null || identifiers.Length == 0) throw new ArgumentException(); @@ -96,4 +94,4 @@ namespace Avalonia.SourceGenerator.CompositionGenerator ? s.Substring(prefix.Length) : s; } -} \ No newline at end of file +} diff --git a/src/tools/DevGenerators/CompositionGenerator/Generator.Utils.cs b/src/tools/DevGenerators/CompositionGenerator/Generator.Utils.cs index b53c247991..16aa15d95a 100644 --- a/src/tools/DevGenerators/CompositionGenerator/Generator.Utils.cs +++ b/src/tools/DevGenerators/CompositionGenerator/Generator.Utils.cs @@ -54,7 +54,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator FieldDeclarationSyntax DeclareField(string type, string name, params SyntaxKind[] modifiers) => DeclareField(type, name, null, modifiers); - FieldDeclarationSyntax DeclareField(string type, string name, EqualsValueClauseSyntax initializer, + FieldDeclarationSyntax DeclareField(string type, string name, EqualsValueClauseSyntax? initializer, params SyntaxKind[] modifiers) => FieldDeclaration( VariableDeclaration(ParseTypeName(type), @@ -63,4 +63,4 @@ namespace Avalonia.SourceGenerator.CompositionGenerator .WithSemicolonToken(Semicolon()) .WithModifiers(TokenList(modifiers.Select(x => Token(x)))); } -} \ No newline at end of file +} diff --git a/src/tools/DevGenerators/CompositionGenerator/Generator.cs b/src/tools/DevGenerators/CompositionGenerator/Generator.cs index 18f1d1c1e5..cbcc55ada0 100644 --- a/src/tools/DevGenerators/CompositionGenerator/Generator.cs +++ b/src/tools/DevGenerators/CompositionGenerator/Generator.cs @@ -37,8 +37,8 @@ namespace Avalonia.SourceGenerator.CompositionGenerator - string ServerName(string c) => c != null ? ("Server" + c) : "ServerObject"; - string ChangesName(string c) => c != null ? (c + "Changes") : "ChangeSet"; + string ServerName(string? c) => c != null ? ("Server" + c) : "ServerObject"; + string ChangesName(string? c) => c != null ? (c + "Changes") : "ChangeSet"; string ChangedFieldsTypeName(GClass c) => c.Name + "ChangedFields"; string ChangedFieldsFieldName(GClass c) => "_changedFieldsOf" + c.Name; string PropertyBackingFieldName(GProperty prop) => "_" + prop.Name.WithLowerFirst(); @@ -298,10 +298,10 @@ namespace Avalonia.SourceGenerator.CompositionGenerator if(cl.Implements.Count > 0) foreach (var impl in cl.Implements) { - client = client.WithBaseList(client.BaseList.AddTypes(SimpleBaseType(ParseTypeName(impl.Name)))); + client = client.WithBaseList(client.BaseList?.AddTypes(SimpleBaseType(ParseTypeName(impl.Name)))); if (impl.ServerName != null) server = server.WithBaseList( - server.BaseList.AddTypes(SimpleBaseType(ParseTypeName(impl.ServerName)))); + server.BaseList?.AddTypes(SimpleBaseType(ParseTypeName(impl.ServerName)))); client = client.AddMembers( ParseMemberDeclaration($"{impl.ServerName} {impl.Name}.Server => Server;")); @@ -527,7 +527,7 @@ var changed = reader.Read<{ChangedFieldsTypeName(cl)}>(); body = body.AddStatements( ParseStatement("return base.GetPropertyForAnimation(name);")); var method = ((MethodDeclarationSyntax) ParseMemberDeclaration( - $"public override Avalonia.Rendering.Composition.Expressions.ExpressionVariant GetPropertyForAnimation(string name){{}}")) + $"public override Avalonia.Rendering.Composition.Expressions.ExpressionVariant GetPropertyForAnimation(string name){{}}")!) .WithBody(body); return cl.AddMembers(method); @@ -540,7 +540,7 @@ var changed = reader.Read<{ChangedFieldsTypeName(cl)}>(); body = body.AddStatements( ParseStatement("return base.GetCompositionProperty(name);")); var method = ((MethodDeclarationSyntax)ParseMemberDeclaration( - $"public override CompositionProperty? GetCompositionProperty(string name){{}}")) + $"public override CompositionProperty? GetCompositionProperty(string name){{}}")!) .WithBody(body); return cl.AddMembers(method); @@ -559,11 +559,11 @@ var changed = reader.Read<{ChangedFieldsTypeName(cl)}>(); ); return cl.AddMembers( ((MethodDeclarationSyntax) ParseMemberDeclaration( - "internal override void StartAnimation(string propertyName, CompositionAnimation animation, Avalonia.Rendering.Composition.Expressions.ExpressionVariant? finalValue){}")) + "internal override void StartAnimation(string propertyName, CompositionAnimation animation, Avalonia.Rendering.Composition.Expressions.ExpressionVariant? finalValue){}")!) .WithBody(body)); } } -} \ No newline at end of file +} diff --git a/src/tools/DevGenerators/DevGenerators.csproj b/src/tools/DevGenerators/DevGenerators.csproj index 30da940514..069ff159fc 100644 --- a/src/tools/DevGenerators/DevGenerators.csproj +++ b/src/tools/DevGenerators/DevGenerators.csproj @@ -4,6 +4,7 @@ netstandard2.0 enable false + 10 diff --git a/src/tools/DevGenerators/GetProcAddressInitialization.cs b/src/tools/DevGenerators/GetProcAddressInitialization.cs index c7d03a9b50..aedc13e7f6 100644 --- a/src/tools/DevGenerators/GetProcAddressInitialization.cs +++ b/src/tools/DevGenerators/GetProcAddressInitialization.cs @@ -131,10 +131,13 @@ public class GetProcAddressInitializationGenerator : IIncrementalGenerator .Append(".GetProcAddress(") .Append("getProcAddress, ") .Append(contextName); - - var syntaxNode = (AttributeSyntax)attr.ApplicationSyntaxReference.GetSyntax(); - foreach (var arg in syntaxNode.ArgumentList.Arguments) - initializeBody.Append(", ").Append(arg.GetText()); + + if (attr.ApplicationSyntaxReference?.GetSyntax() is AttributeSyntax syntaxNode + && syntaxNode.ArgumentList is { }) + { + foreach (var arg in syntaxNode.ArgumentList.Arguments) + initializeBody.Append(", ").Append(arg.GetText()); + } initializeBody.AppendLine(");"); } } From 33de9380a5e7399494db7fe64af75d8d755b7b72 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Wed, 7 Sep 2022 18:54:10 +0200 Subject: [PATCH 054/334] feat(DevAnalyzer): OnPropertyChanged override Analyzer --- .../OnPropertyChangedOverrideAnalyzer.cs | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 src/tools/DevAnalyzers/OnPropertyChangedOverrideAnalyzer.cs diff --git a/src/tools/DevAnalyzers/OnPropertyChangedOverrideAnalyzer.cs b/src/tools/DevAnalyzers/OnPropertyChangedOverrideAnalyzer.cs new file mode 100644 index 0000000000..a16542fdc0 --- /dev/null +++ b/src/tools/DevAnalyzers/OnPropertyChangedOverrideAnalyzer.cs @@ -0,0 +1,64 @@ +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace DevAnalyzers +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + public class OnPropertyChangedOverrideAnalyzer : DiagnosticAnalyzer + { + public const string DiagnosticId = "AVADEV2001"; + + private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor( + DiagnosticId, + "Missing invoke base.OnPropertyChanged", + "Method '{0}' do not invoke base.{0}", + "Potential issue", + DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "The OnPropertyChanged of the base class was not invoked in the override method declaration, which could lead to unwanted behavior."); + + public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + context.RegisterSymbolAction(AnalyzeMethod, SymbolKind.Method); + } + + private static void AnalyzeMethod(SymbolAnalysisContext context) + { + if (context.Symbol is IMethodSymbol currentMethod + && currentMethod.Name == "OnPropertyChanged" + && currentMethod.OverriddenMethod is IMethodSymbol originalMethod) + { + var declaration = currentMethod.DeclaringSyntaxReferences.FirstOrDefault() + ?.GetSyntax(context.CancellationToken); + if (declaration is not null && context.Compilation.GetSemanticModel(declaration!.SyntaxTree) is { } semanticModel) + { + if (declaration.SyntaxTree.TryGetRoot(out var root)) + { + var baseInvocations = root.DescendantNodes().OfType(); + if (baseInvocations.Any()) + { + foreach (var baseInvocation in baseInvocations) + { + var parent = baseInvocation.Parent; + var targetSymbol = semanticModel.GetSymbolInfo(parent, context.CancellationToken); + if (SymbolEqualityComparer.Default.Equals(targetSymbol.Symbol, originalMethod)) + { + return; + } + } + } + context.ReportDiagnostic(Diagnostic.Create(Rule, currentMethod.Locations[0], currentMethod.Name)); + } + } + } + } + + } +} From b9f7270eb978258c60d95823cb5b263d17a32c38 Mon Sep 17 00:00:00 2001 From: Dmitry Zhelnin Date: Sun, 4 Sep 2022 22:22:09 +0300 Subject: [PATCH 055/334] TreeView: improve navigation with Left and Right keys --- src/Avalonia.Controls/TreeView.cs | 1 + src/Avalonia.Controls/TreeViewItem.cs | 19 ++++++++++++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 7359f3cade..d78f9c82ef 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -495,6 +495,7 @@ namespace Avalonia.Controls break; case NavigationDirection.Down: + case NavigationDirection.Right: if (from?.IsExpanded == true && intoChildren && from.ItemCount > 0) { result = (TreeViewItem)from.ItemContainerGenerator.ContainerFromIndex(0)!; diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index 2e3aa037c2..ada081b808 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -157,17 +157,26 @@ namespace Avalonia.Controls switch (e.Key) { case Key.Right: - if (Items != null && Items.Cast().Any()) + if (Items != null && Items.Cast().Any() && !IsExpanded) { IsExpanded = true; + e.Handled = true; } - - e.Handled = true; break; case Key.Left: - IsExpanded = false; - e.Handled = true; + if (Items is not null && Items.Cast().Any() && IsExpanded) + { + if (IsFocused) + { + IsExpanded = false; + } + else + { + FocusManager.Instance?.Focus(this, NavigationMethod.Directional); + } + e.Handled = true; + } break; } } From 9fc7be195aff85da4f35f844681a233efea94b62 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Thu, 8 Sep 2022 10:28:46 +0200 Subject: [PATCH 056/334] fix: CS8667 Nullability of reference types in type of --- samples/ControlCatalog/Converter/HexConverter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/ControlCatalog/Converter/HexConverter.cs b/samples/ControlCatalog/Converter/HexConverter.cs index 83a52212f6..31cce5ba67 100644 --- a/samples/ControlCatalog/Converter/HexConverter.cs +++ b/samples/ControlCatalog/Converter/HexConverter.cs @@ -7,7 +7,7 @@ namespace ControlCatalog.Converter; public class HexConverter : IValueConverter { - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { var str = value?.ToString(); if (str == null) @@ -18,7 +18,7 @@ public class HexConverter : IValueConverter } - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) { try { From 11ebe62c03b11832e596dc44a66388f3beddf675 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Thu, 8 Sep 2022 13:08:04 +0300 Subject: [PATCH 057/334] Fix tests. --- .../ExpressionObserverBuilderTests_Method.cs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Method.cs b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Method.cs index 72e0ac5e57..0e499ff256 100644 --- a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Method.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_Method.cs @@ -19,12 +19,9 @@ namespace Avalonia.Markup.UnitTests.Parsers public int MethodWithReturn() => 0; - public int MethodWithReturnAndParameters(int i) => i; + public int MethodWithReturnAndParameter(object i) => (int)i; public static void StaticMethod() { } - - public static void ManyParameters(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8, int a9) { } - public static int ManyParametersWithReturnType(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8) => 1; } [Fact] @@ -42,10 +39,8 @@ namespace Avalonia.Markup.UnitTests.Parsers [Theory] [InlineData(nameof(TestObject.MethodWithoutReturn), typeof(Action))] [InlineData(nameof(TestObject.MethodWithReturn), typeof(Func))] - [InlineData(nameof(TestObject.MethodWithReturnAndParameters), typeof(Func))] + [InlineData(nameof(TestObject.MethodWithReturnAndParameter), typeof(Func))] [InlineData(nameof(TestObject.StaticMethod), typeof(Action))] - [InlineData(nameof(TestObject.ManyParameters), typeof(Action))] - [InlineData(nameof(TestObject.ManyParametersWithReturnType), typeof(Func))] public async Task Should_Get_Method_WithCorrectDelegateType(string methodName, Type expectedType) { var data = new TestObject(); @@ -61,10 +56,10 @@ namespace Avalonia.Markup.UnitTests.Parsers public async Task Can_Call_Method_Returned_From_Observer() { var data = new TestObject(); - var observer = ExpressionObserverBuilder.Build(data, nameof(TestObject.MethodWithReturnAndParameters)); + var observer = ExpressionObserverBuilder.Build(data, nameof(TestObject.MethodWithReturnAndParameter)); var result = await observer.Take(1); - var callback = (Func)result; + var callback = (Func)result; Assert.Equal(1, callback(1)); From 426f4e1e64fb7367d102d4ac9511ef741a2becd8 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Thu, 8 Sep 2022 13:35:51 +0300 Subject: [PATCH 058/334] Fix Tests. --- .../Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Method.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Method.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Method.cs index 7931912649..f17903d24b 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Method.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Method.cs @@ -198,7 +198,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data public event PropertyChangedEventHandler PropertyChanged; public string Method() => Value = "Called"; - public string Method1(int i) => Value = $"Called {i}"; + public string Method1(object i) => Value = $"Called {i}"; public string Method2(int i, int j) => Value = $"Called {i},{j}"; public string Value { get; private set; } = "Not called"; From d59a998440002a91159cb184d85e9f8cece3d883 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Thu, 8 Sep 2022 15:32:58 +0300 Subject: [PATCH 059/334] Add test. --- .../Data/BindingTests_Method.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Method.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Method.cs index f17903d24b..d51d6122cd 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Method.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Method.cs @@ -141,6 +141,28 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data } } + [Fact] + public void Binding_Method_Preserves_Correct_Order() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + +