From 04ed8e9eef3be1603d7476e4a25da647d80a4ad7 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Mon, 30 Jan 2023 10:11:49 +0100 Subject: [PATCH 01/29] fix: xml comment --- src/Avalonia.Base/Data/InstancedBinding.cs | 2 +- .../Metadata/InheritDataTypeFromItemsAttribute.cs | 4 ++-- src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs | 2 +- src/Avalonia.Controls/TreeViewItem.cs | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Base/Data/InstancedBinding.cs b/src/Avalonia.Base/Data/InstancedBinding.cs index 00e5c3d8e6..c09c31632e 100644 --- a/src/Avalonia.Base/Data/InstancedBinding.cs +++ b/src/Avalonia.Base/Data/InstancedBinding.cs @@ -23,7 +23,7 @@ namespace Avalonia.Data /// The priority of the binding. /// /// This constructor can be used to create any type of binding and as such requires an - /// as the binding source because this is the only binding + /// as the binding source because this is the only binding /// source which can be used for all binding modes. If you wish to create an instance with /// something other than a subject, use one of the static creation methods on this class. /// diff --git a/src/Avalonia.Base/Metadata/InheritDataTypeFromItemsAttribute.cs b/src/Avalonia.Base/Metadata/InheritDataTypeFromItemsAttribute.cs index 6bb820d214..fac8cd8737 100644 --- a/src/Avalonia.Base/Metadata/InheritDataTypeFromItemsAttribute.cs +++ b/src/Avalonia.Base/Metadata/InheritDataTypeFromItemsAttribute.cs @@ -25,9 +25,9 @@ public sealed class InheritDataTypeFromItemsAttribute : Attribute /// The name of the property whose item type should be used on the target property. /// public string AncestorItemsProperty { get; } - + /// - /// The ancestor type to be used in a lookup for the . + /// The ancestor type to be used in a lookup for the . /// If null, the declaring type of the target property is used. /// public Type? AncestorType { get; set; } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs b/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs index 6d30358119..112dd436de 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs @@ -19,7 +19,7 @@ namespace Avalonia.Rendering.SceneGraph /// The point in global coordinates. /// True if the point hits the node's geometry; otherwise false. /// - /// This method does not recurse to child s, if you want + /// This method does not recurse to child s, if you want /// to hit test children they must be hit tested manually. /// bool HitTest(Point p); diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index 022e1a74b1..e6b1016696 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -264,9 +264,9 @@ namespace Avalonia.Controls Dispatcher.UIThread.Post(this.BringIntoView); // must use the Dispatcher, otherwise the TreeView doesn't scroll } } - + /// - /// Invoked when the event occurs in the header. + /// Invoked when the event occurs in the header. /// protected virtual void OnHeaderDoubleTapped(TappedEventArgs e) { From a22249b898aa621679bc79ea7fb1406a74dd0942 Mon Sep 17 00:00:00 2001 From: workgroupengineering Date: Thu, 2 Feb 2023 09:45:36 +0100 Subject: [PATCH 02/29] fix: remarks --- src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs b/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs index 112dd436de..2bfd2080c3 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs @@ -19,7 +19,7 @@ namespace Avalonia.Rendering.SceneGraph /// The point in global coordinates. /// True if the point hits the node's geometry; otherwise false. /// - /// This method does not recurse to child s, if you want + /// This method does not recurse to childs, if you want /// to hit test children they must be hit tested manually. /// bool HitTest(Point p); From b0a2ae99e44151f58ef6e77ca731f78bd5b3d660 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Thu, 2 Feb 2023 10:37:20 +0100 Subject: [PATCH 03/29] Fix default GlyphRun.BaselineOrigin Add a unit test --- src/Avalonia.Base/Media/GlyphRun.cs | 2 +- .../Avalonia.Skia/PlatformRenderInterface.cs | 2 +- .../Avalonia.Direct2D1/Direct2D1Platform.cs | 2 +- .../Controls/TextBlockTests.cs | 51 ++++++++++++++++++ .../Should_Draw_TextDecorations.expected.png | Bin 0 -> 1279 bytes .../Should_Draw_TextDecorations.expected.png | Bin 0 -> 1516 bytes 6 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 tests/TestFiles/Direct2D1/Controls/TextBlock/Should_Draw_TextDecorations.expected.png create mode 100644 tests/TestFiles/Skia/Controls/TextBlock/Should_Draw_TextDecorations.expected.png diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index 0ec7152359..2966ceee8d 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -166,7 +166,7 @@ namespace Avalonia.Media /// public Point BaselineOrigin { - get => _baselineOrigin ?? default; + get => PlatformImpl.Item.BaselineOrigin; set => Set(ref _baselineOrigin, value); } diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index d12db39ad6..e795f3d304 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -86,7 +86,7 @@ namespace Avalonia.Skia SKPath path = new SKPath(); - var (currentX, currentY) = glyphRun.PlatformImpl.Item.BaselineOrigin; + var (currentX, currentY) = glyphRun.BaselineOrigin; for (var i = 0; i < glyphRun.GlyphInfos.Count; i++) { diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index eb3f9911df..99c01dd111 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -257,7 +257,7 @@ namespace Avalonia.Direct2D1 sink.Close(); } - var (baselineOriginX, baselineOriginY) = glyphRun.PlatformImpl.Item.BaselineOrigin; + var (baselineOriginX, baselineOriginY) = glyphRun.BaselineOrigin; var transformedGeometry = new SharpDX.Direct2D1.TransformedGeometry( Direct2D1Factory, diff --git a/tests/Avalonia.RenderTests/Controls/TextBlockTests.cs b/tests/Avalonia.RenderTests/Controls/TextBlockTests.cs index c11bd2b816..4210ee8238 100644 --- a/tests/Avalonia.RenderTests/Controls/TextBlockTests.cs +++ b/tests/Avalonia.RenderTests/Controls/TextBlockTests.cs @@ -1,3 +1,4 @@ +using System.Net; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Layout; @@ -17,6 +18,56 @@ namespace Avalonia.Direct2D1.RenderTests.Controls { } + [Win32Fact("Has text")] + public async Task Should_Draw_TextDecorations() + { + Border target = new Border + { + Padding = new Thickness(8), + Width = 200, + Height = 30, + Background = Brushes.White, + Child = new TextBlock + { + FontFamily = TestFontFamily, + FontSize = 12, + Foreground = Brushes.Black, + Text = "Neque porro quisquam est qui dolorem", + VerticalAlignment = VerticalAlignment.Top, + TextWrapping = TextWrapping.NoWrap, + TextDecorations = new TextDecorationCollection + { + new TextDecoration + { + Location = TextDecorationLocation.Overline, + StrokeThickness= 1.5, + StrokeThicknessUnit = TextDecorationUnit.Pixel, + Stroke = new SolidColorBrush(Colors.Red) + }, + new TextDecoration + { + Location = TextDecorationLocation.Baseline, + StrokeThickness= 1.5, + StrokeThicknessUnit = TextDecorationUnit.Pixel, + Stroke = new SolidColorBrush(Colors.Green) + }, + new TextDecoration + { + Location = TextDecorationLocation.Underline, + StrokeThickness= 1.5, + StrokeThicknessUnit = TextDecorationUnit.Pixel, + Stroke = new SolidColorBrush(Colors.Blue), + StrokeOffset = 2, + StrokeOffsetUnit = TextDecorationUnit.Pixel + } + } + } + }; + + await RenderToFile(target); + CompareImages(); + } + [Win32Fact("Has text")] public async Task Wrapping_NoWrap() { diff --git a/tests/TestFiles/Direct2D1/Controls/TextBlock/Should_Draw_TextDecorations.expected.png b/tests/TestFiles/Direct2D1/Controls/TextBlock/Should_Draw_TextDecorations.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..494c8a900252641099b21c89d680145adde9046f GIT binary patch literal 1279 zcmVPx#1ZP1_K>z@;j|==^1poj9sYygZRCr$Pnma6QK@^6ELPRvT5|Kza3WZ8UL_9*` z*;1fVNoa0IqtqPH+=fJ@aunp2UWIr>M3f2=uSTJ|2_h=4f9|Yp8T-thvmN(!)|X87 znpt~ZYu1{X?Z|t6K?#BoO1^mtf)IiPh#)wS01*TS5+H)$KmtS%97uo&f&&QMmwmhfq^qk-KRd$d&`nQI>xc)C`MbC#;9Y{Qc9eXt5oiN#Q$6aBh#D*Jm+K*O9Um(; z4k|~=CYe!LzMZJ2lmb*Vj?lkbK~b&>d-0oNhX zT+t`;=jZ1tkw|FSUh8!5b&SF*K+ew2)WE=inwy(5C9hym2p$~uNR^e9fsvu>?CcD5 z^Yin%JPH_=w+SOOH#Zw?G*?$wL0#-HpH> zd3o7r)8q0V3I^UUT_mHz`vd`)w7omR7;p%RNCXia6u?PZ zT3WOWMn*=AP3)5cAXvB!FjimS%(NzUV3n7b2R7igh{EMR2nLK7<@PBV7xE$B z@3xErq`$vkO-@dl+$$7~lzb!E1-^TDn@E`$K9|Ab!(MPcIG25Gi39EGv=(8TMO<9| zg8)GpHZIJ?cazK(83hO&6jy@v^>tICbbfJh(d6EO_i6VAe7DAq!;gO6Mtyy~?jx=- z>^^Ci##s0F_q7brC(1qooDruTA1ii2zDuVgu6=w#`*1G4UN}{_4fwSg@6+xLTvtAq zE)!h-!@+eIryXt=9|3a&z9&-T4~4z+y}Gf!zCNA5xw%oRtE;NDwN+JAROqtJ%}sTE zeXV8C)6=6$N=mf6va+J??(Vc)U0to84Gj(Y$#r#gb?Wr=^j$whSy`Ft=;$!H^q@_= zdvbE3^XQZM1AMG@o2Xk`TN})agL?%?1+_lrL84!$XlkB{|xVfL~~=RcR1muh=^ z+mwd;0QxyPI?^)c!q++-+_#>;yT3ys1LRB3HzR$V$HzzY^z@{Pii%80_PM>iRfUCx zs-U32l>9sV4*S?VJUpoU{CsV*T;#{h#;gzsh^u4Ek3jSWm^CM002ovPDHLkV1l~1QeprA literal 0 HcmV?d00001 diff --git a/tests/TestFiles/Skia/Controls/TextBlock/Should_Draw_TextDecorations.expected.png b/tests/TestFiles/Skia/Controls/TextBlock/Should_Draw_TextDecorations.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..297bd592ff27631f9c0c5738da922f49e7f3dd25 GIT binary patch literal 1516 zcmVR9JKak1zRd8c^6@WOQo{X5km70J(RM=3R0%aLc%IoM2ZM99+FHZ z$z(FwCf#j$AM9cB&-?%X{qOz%dv9ho<@>&mLZOIc$5uO_P;6&Zf+!R*s02|cVo(X9 zP{g1TM4^a5C5S>1gGvyEB8F!M-f`r}V{k*E*dnf9j}@oXi$aB>KZCbebwNIr!jZub zhx74N9bG!AQYp3_ z(zV8_KVgDs%oTNn8=WE2Ns&r5|MG%*ADJTr`KG)s^5nxAC!Nl=&8=O~Her>JP97ng zME-QjBu>gf%23;oQR$>`q{In)a%k^5H|Oi4#HTLu&2|x6tS)~#``Oqc2W^ED^fT6G zx6V-irIW{T8vf*kXvQe{)-UP&>4R+T2p3t1`bE7%vz2u*0-uWLfv*Cy7WOsSa!ulntNESa=+ zVbaM^&|5LM2qIwxPMa+GZJkxy!U;NAMrs%FYb7Xn7`{)fMBjoMCT{*v7dWB#_`Xjt zBzh6wuTjc@UG42>ekP-OVr2qU$V=gsUS9iE=+o%}wO}dVu7bpbPqfvpZK2Nwb zEk{|nK^5yhPz9BuTc#V>WkG@>97$NO=atE6TBNUaS5UT%-c?~3dVH0*Ru0n`oXwk# zPg2q*OMVqQSS8bFI-wXeD){g=C8IeT$IqWIdNKL7E#9Ptjz!XkvIAcf&Wj_g+<@t$ z2iXnq@E$9|W#92ryuPn-&&KV`EZ=OmITR)P5$um_EU8gjGWo)t=jw&aw3;0<3^wld z+F5HUois38vIwSMm}dUXd8CuV#m4&7g4ckVc@_@u5AIv9EOP0m=EcjioHM7GXh^6m zR@igSWNM;U|9WMS%ARwWQNq7h5ubg>ug%Mu`2|8IV+s6`U@x@e^X1PUu>AQ~ed4ph z?aRyD?9eocVIJk+?uXo45x8q@Nq9VzSqjyW~KdSww$Tj22ibt>WkZdT+t--f# S6vVXv0000 Date: Thu, 2 Feb 2023 10:06:57 +0000 Subject: [PATCH 04/29] fix scroll snap points attached properties --- src/Avalonia.Controls/ScrollViewer.cs | 88 +++++++++++++++++++++++++-- 1 file changed, 84 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs index 1c23919d0e..ab114da933 100644 --- a/src/Avalonia.Controls/ScrollViewer.cs +++ b/src/Avalonia.Controls/ScrollViewer.cs @@ -154,15 +154,15 @@ namespace Avalonia.Controls /// /// Defines the property. /// - public static readonly StyledProperty HorizontalSnapPointsTypeProperty = - AvaloniaProperty.Register( + public static readonly AttachedProperty HorizontalSnapPointsTypeProperty = + AvaloniaProperty.RegisterAttached( nameof(HorizontalSnapPointsType)); /// /// Defines the property. /// - public static readonly StyledProperty VerticalSnapPointsTypeProperty = - AvaloniaProperty.Register( + public static readonly AttachedProperty VerticalSnapPointsTypeProperty = + AvaloniaProperty.RegisterAttached( nameof(VerticalSnapPointsType)); /// @@ -625,6 +625,86 @@ namespace Avalonia.Controls control.SetValue(HorizontalScrollBarVisibilityProperty, value); } + /// + /// Gets the value of the HorizontalSnapPointsType attached property. + /// + /// The control to read the value from. + /// The value of the property. + public static SnapPointsType GetHorizontalSnapPointsType(Control control) + { + return control.GetValue(HorizontalSnapPointsTypeProperty); + } + + /// + /// Gets the value of the HorizontalSnapPointsType attached property. + /// + /// The control to set the value on. + /// The value of the property. + public static void SetHorizontalSnapPointsType(Control control, SnapPointsType value) + { + control.SetValue(HorizontalSnapPointsTypeProperty, value); + } + + /// + /// Gets the value of the VerticalSnapPointsType attached property. + /// + /// The control to read the value from. + /// The value of the property. + public static SnapPointsType GetVerticalSnapPointsType(Control control) + { + return control.GetValue(VerticalSnapPointsTypeProperty); + } + + /// + /// Gets the value of the VerticalSnapPointsType attached property. + /// + /// The control to set the value on. + /// The value of the property. + public static void SetVerticalSnapPointsType(Control control, SnapPointsType value) + { + control.SetValue(VerticalSnapPointsTypeProperty, value); + } + + /// + /// Gets the value of the HorizontalSnapPointsAlignment attached property. + /// + /// The control to read the value from. + /// The value of the property. + public static SnapPointsAlignment GetHorizontalSnapPointsAlignment(Control control) + { + return control.GetValue(HorizontalSnapPointsAlignmentProperty); + } + + /// + /// Gets the value of the HorizontalSnapPointsAlignment attached property. + /// + /// The control to set the value on. + /// The value of the property. + public static void SetHorizontalSnapPointsAlignment(Control control, SnapPointsAlignment value) + { + control.SetValue(HorizontalSnapPointsAlignmentProperty, value); + } + + /// + /// Gets the value of the VerticalSnapPointsAlignment attached property. + /// + /// The control to read the value from. + /// The value of the property. + public static SnapPointsAlignment GetVerticalSnapPointsAlignment(Control control) + { + return control.GetValue(VerticalSnapPointsAlignmentProperty); + } + + /// + /// Gets the value of the VerticalSnapPointsAlignment attached property. + /// + /// The control to set the value on. + /// The value of the property. + public static void SetVerticalSnapPointsAlignment(Control control, SnapPointsAlignment value) + { + control.SetValue(VerticalSnapPointsAlignmentProperty, value); + } + /// /// Gets the value of the VerticalScrollBarVisibility attached property. /// From cd90a9c0c239c4a67ae8dd348358f52b56658084 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Thu, 2 Feb 2023 14:21:22 +0100 Subject: [PATCH 05/29] feat(HamburgerMenu): Auto close on SelectedItem Changed when HamburgerMenu is display as Overlay --- samples/ControlCatalog/MainView.xaml | 4 ++-- .../SampleControls/HamburgerMenu/HamburgerMenu.cs | 12 ++++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 0695d9d17a..3681298a72 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -14,8 +14,8 @@ - - + + diff --git a/samples/SampleControls/HamburgerMenu/HamburgerMenu.cs b/samples/SampleControls/HamburgerMenu/HamburgerMenu.cs index ab61dcde91..7ff8160720 100644 --- a/samples/SampleControls/HamburgerMenu/HamburgerMenu.cs +++ b/samples/SampleControls/HamburgerMenu/HamburgerMenu.cs @@ -52,6 +52,14 @@ namespace ControlSamples var (oldBounds, newBounds) = change.GetOldAndNewValue(); EnsureSplitViewMode(oldBounds, newBounds); } + + if (change.Property == SelectedItemProperty) + { + if (_splitView is not null && _splitView.DisplayMode == SplitViewDisplayMode.Overlay) + { + _splitView.SetValue(SplitView.IsPaneOpenProperty, false, Avalonia.Data.BindingPriority.Animation); + } + } } private void EnsureSplitViewMode(Rect oldBounds, Rect newBounds) @@ -60,12 +68,12 @@ namespace ControlSamples { var threshold = ExpandedModeThresholdWidth; - if (newBounds.Width >= threshold && oldBounds.Width < threshold) + if (newBounds.Width >= threshold) { _splitView.DisplayMode = SplitViewDisplayMode.Inline; _splitView.IsPaneOpen = true; } - else if (newBounds.Width < threshold && oldBounds.Width >= threshold) + else if (newBounds.Width < threshold) { _splitView.DisplayMode = SplitViewDisplayMode.Overlay; _splitView.IsPaneOpen = false; From 69b3db42a2f25e0dd878918911ec136565bbd7e2 Mon Sep 17 00:00:00 2001 From: Dmitry Zhelnin Date: Thu, 2 Feb 2023 13:20:17 +0300 Subject: [PATCH 06/29] Cleanup attributes Enable rules CA1018: Mark attributes with AttributeUsageAttribute; CA1813: Avoid unsealed attributes --- .editorconfig | 4 ++++ src/Avalonia.Base/Metadata/AmbientAttribute.cs | 4 ++-- src/Avalonia.Base/Metadata/ContentAttribute.cs | 2 +- src/Avalonia.Base/Metadata/DataTypeAttribute.cs | 4 ++-- src/Avalonia.Base/Metadata/DependsOnAttribute.cs | 2 +- .../Metadata/NotClientImplementableAttribute.cs | 2 +- src/Avalonia.Base/Metadata/TemplateContent.cs | 2 +- .../Metadata/TrimSurroundingWhitespaceAttribute.cs | 2 +- src/Avalonia.Base/Metadata/UnstableAttribute.cs | 3 ++- .../Metadata/UsableDuringInitializationAttribute.cs | 4 ++-- .../WhitespaceSignificantCollectionAttribute.cs | 2 +- .../Metadata/XmlnsDefinitionAttribute.cs | 2 +- .../Rendering/Composition/Expressions/Expression.cs | 3 ++- .../Platform/ExportAvaloniaModuleAttribute.cs | 2 +- src/Avalonia.Controls/ResolveByNameAttribute.cs | 3 ++- src/Avalonia.OpenGL/GlEntryPointAttribute.cs | 4 ++-- .../AvaloniaRemoteMessageGuidAttribute.cs | 2 +- src/Markup/Avalonia.Markup.Xaml/XamlTypes.cs | 3 ++- src/Shared/ModuleInitializer.cs | 3 ++- src/Shared/SourceGeneratorAttributes.cs | 11 ++++++++--- 20 files changed, 39 insertions(+), 25 deletions(-) diff --git a/.editorconfig b/.editorconfig index eac5870f96..e6ae266849 100644 --- a/.editorconfig +++ b/.editorconfig @@ -145,10 +145,14 @@ dotnet_diagnostic.CS1591.severity = suggestion # CS0162: Remove unreachable code dotnet_diagnostic.CS0162.severity = error +# CA1018: Mark attributes with AttributeUsageAttribute +dotnet_diagnostic.CA1018.severity = error # CA1304: Specify CultureInfo dotnet_diagnostic.CA1304.severity = warning # CA1802: Use literals where appropriate dotnet_diagnostic.CA1802.severity = warning +# CA1813: Avoid unsealed attributes +dotnet_diagnostic.CA1813.severity = error # CA1815: Override equals and operator equals on value types dotnet_diagnostic.CA1815.severity = warning # CA1820: Test for empty strings using string length diff --git a/src/Avalonia.Base/Metadata/AmbientAttribute.cs b/src/Avalonia.Base/Metadata/AmbientAttribute.cs index 85ca6c4ec9..1c85a67641 100644 --- a/src/Avalonia.Base/Metadata/AmbientAttribute.cs +++ b/src/Avalonia.Base/Metadata/AmbientAttribute.cs @@ -3,10 +3,10 @@ using System; namespace Avalonia.Metadata { /// - /// Defines the ambient class/property + /// Defines the ambient class/property /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, Inherited = true)] - public class AmbientAttribute : Attribute + public sealed class AmbientAttribute : Attribute { } } diff --git a/src/Avalonia.Base/Metadata/ContentAttribute.cs b/src/Avalonia.Base/Metadata/ContentAttribute.cs index a0b2fa0e1d..f32c8e78f6 100644 --- a/src/Avalonia.Base/Metadata/ContentAttribute.cs +++ b/src/Avalonia.Base/Metadata/ContentAttribute.cs @@ -6,7 +6,7 @@ namespace Avalonia.Metadata /// Defines the property that contains the object's content in markup. /// [AttributeUsage(AttributeTargets.Property)] - public class ContentAttribute : Attribute + public sealed class ContentAttribute : Attribute { } } diff --git a/src/Avalonia.Base/Metadata/DataTypeAttribute.cs b/src/Avalonia.Base/Metadata/DataTypeAttribute.cs index ac46a0d30a..dd9603b4a9 100644 --- a/src/Avalonia.Base/Metadata/DataTypeAttribute.cs +++ b/src/Avalonia.Base/Metadata/DataTypeAttribute.cs @@ -9,7 +9,7 @@ namespace Avalonia.Metadata; /// Used on DataTemplate.DataType property so it can be inherited in compiled bindings inside of the template. /// [AttributeUsage(AttributeTargets.Property)] -public class DataTypeAttribute : Attribute +public sealed class DataTypeAttribute : Attribute { - + } diff --git a/src/Avalonia.Base/Metadata/DependsOnAttribute.cs b/src/Avalonia.Base/Metadata/DependsOnAttribute.cs index caee71ebfd..ca58a91eb9 100644 --- a/src/Avalonia.Base/Metadata/DependsOnAttribute.cs +++ b/src/Avalonia.Base/Metadata/DependsOnAttribute.cs @@ -6,7 +6,7 @@ namespace Avalonia.Metadata /// Indicates that the property depends on the value of another property in markup. /// [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method, Inherited = true, AllowMultiple = true)] - public class DependsOnAttribute : Attribute + public sealed class DependsOnAttribute : Attribute { /// /// Initializes a new instance of the class. diff --git a/src/Avalonia.Base/Metadata/NotClientImplementableAttribute.cs b/src/Avalonia.Base/Metadata/NotClientImplementableAttribute.cs index 348c983c03..75fe7b8031 100644 --- a/src/Avalonia.Base/Metadata/NotClientImplementableAttribute.cs +++ b/src/Avalonia.Base/Metadata/NotClientImplementableAttribute.cs @@ -11,7 +11,7 @@ namespace Avalonia.Metadata /// may be added to its API. /// [AttributeUsage(AttributeTargets.Interface)] - public class NotClientImplementableAttribute : Attribute + public sealed class NotClientImplementableAttribute : Attribute { } } diff --git a/src/Avalonia.Base/Metadata/TemplateContent.cs b/src/Avalonia.Base/Metadata/TemplateContent.cs index 258154aba4..78bcc2ff29 100644 --- a/src/Avalonia.Base/Metadata/TemplateContent.cs +++ b/src/Avalonia.Base/Metadata/TemplateContent.cs @@ -6,7 +6,7 @@ namespace Avalonia.Metadata /// Defines the property that contains the object's content in markup. /// [AttributeUsage(AttributeTargets.Property)] - public class TemplateContentAttribute : Attribute + public sealed class TemplateContentAttribute : Attribute { public Type? TemplateResultType { get; set; } } diff --git a/src/Avalonia.Base/Metadata/TrimSurroundingWhitespaceAttribute.cs b/src/Avalonia.Base/Metadata/TrimSurroundingWhitespaceAttribute.cs index c46891b3ad..a644c9afe6 100644 --- a/src/Avalonia.Base/Metadata/TrimSurroundingWhitespaceAttribute.cs +++ b/src/Avalonia.Base/Metadata/TrimSurroundingWhitespaceAttribute.cs @@ -3,7 +3,7 @@ namespace Avalonia.Metadata { [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] - public class TrimSurroundingWhitespaceAttribute : Attribute + public sealed class TrimSurroundingWhitespaceAttribute : Attribute { } diff --git a/src/Avalonia.Base/Metadata/UnstableAttribute.cs b/src/Avalonia.Base/Metadata/UnstableAttribute.cs index 3b6fa5168a..361f6d30fd 100644 --- a/src/Avalonia.Base/Metadata/UnstableAttribute.cs +++ b/src/Avalonia.Base/Metadata/UnstableAttribute.cs @@ -6,7 +6,8 @@ namespace Avalonia.Metadata /// This API is unstable and is not covered by API compatibility guarantees between minor and /// patch releases. /// - public class UnstableAttribute : Attribute + [AttributeUsage(AttributeTargets.All)] + public sealed class UnstableAttribute : Attribute { } } diff --git a/src/Avalonia.Base/Metadata/UsableDuringInitializationAttribute.cs b/src/Avalonia.Base/Metadata/UsableDuringInitializationAttribute.cs index 753a96b9ce..d2d163b368 100644 --- a/src/Avalonia.Base/Metadata/UsableDuringInitializationAttribute.cs +++ b/src/Avalonia.Base/Metadata/UsableDuringInitializationAttribute.cs @@ -3,8 +3,8 @@ using System; namespace Avalonia.Metadata { [AttributeUsage(AttributeTargets.Class)] - public class UsableDuringInitializationAttribute : Attribute + public sealed class UsableDuringInitializationAttribute : Attribute { - + } } diff --git a/src/Avalonia.Base/Metadata/WhitespaceSignificantCollectionAttribute.cs b/src/Avalonia.Base/Metadata/WhitespaceSignificantCollectionAttribute.cs index aeaa38dad9..2fd2b1da3b 100644 --- a/src/Avalonia.Base/Metadata/WhitespaceSignificantCollectionAttribute.cs +++ b/src/Avalonia.Base/Metadata/WhitespaceSignificantCollectionAttribute.cs @@ -6,7 +6,7 @@ namespace Avalonia.Metadata /// Indicates that a collection type should be processed as being whitespace significant by a XAML processor. /// [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] - public class WhitespaceSignificantCollectionAttribute : Attribute + public sealed class WhitespaceSignificantCollectionAttribute : Attribute { } } diff --git a/src/Avalonia.Base/Metadata/XmlnsDefinitionAttribute.cs b/src/Avalonia.Base/Metadata/XmlnsDefinitionAttribute.cs index d43fa55f5c..c6b79ba987 100644 --- a/src/Avalonia.Base/Metadata/XmlnsDefinitionAttribute.cs +++ b/src/Avalonia.Base/Metadata/XmlnsDefinitionAttribute.cs @@ -6,7 +6,7 @@ namespace Avalonia.Metadata /// Maps an XML namespace to a CLR namespace for use in XAML. /// [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - public class XmlnsDefinitionAttribute : Attribute + public sealed class XmlnsDefinitionAttribute : Attribute { /// /// Initializes a new instance of the class. diff --git a/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs b/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs index ff2069e71e..560ee05c10 100644 --- a/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs +++ b/src/Avalonia.Base/Rendering/Composition/Expressions/Expression.cs @@ -39,7 +39,8 @@ namespace Avalonia.Rendering.Composition.Expressions } } - internal class PrettyPrintStringAttribute : Attribute + [AttributeUsage(AttributeTargets.Field)] + internal sealed class PrettyPrintStringAttribute : Attribute { public string Name { get; } diff --git a/src/Avalonia.Controls/Platform/ExportAvaloniaModuleAttribute.cs b/src/Avalonia.Controls/Platform/ExportAvaloniaModuleAttribute.cs index 5a34c5c0e1..f271abb59a 100644 --- a/src/Avalonia.Controls/Platform/ExportAvaloniaModuleAttribute.cs +++ b/src/Avalonia.Controls/Platform/ExportAvaloniaModuleAttribute.cs @@ -41,7 +41,7 @@ namespace Avalonia.Platform /// The fallback module will only be initialized if the Skia-specific module is not applicable. /// [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] - public class ExportAvaloniaModuleAttribute : Attribute + public sealed class ExportAvaloniaModuleAttribute : Attribute { public ExportAvaloniaModuleAttribute(string name, Type moduleType) { diff --git a/src/Avalonia.Controls/ResolveByNameAttribute.cs b/src/Avalonia.Controls/ResolveByNameAttribute.cs index a13b10d630..3c56c20db0 100644 --- a/src/Avalonia.Controls/ResolveByNameAttribute.cs +++ b/src/Avalonia.Controls/ResolveByNameAttribute.cs @@ -7,7 +7,8 @@ namespace Avalonia.Controls /// When applying this to attached properties, ensure to put on both /// the Getter and Setter methods. /// - public class ResolveByNameAttribute : Attribute + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Method)] + public sealed class ResolveByNameAttribute : Attribute { } } diff --git a/src/Avalonia.OpenGL/GlEntryPointAttribute.cs b/src/Avalonia.OpenGL/GlEntryPointAttribute.cs index 3e31de6995..386db30f92 100644 --- a/src/Avalonia.OpenGL/GlEntryPointAttribute.cs +++ b/src/Avalonia.OpenGL/GlEntryPointAttribute.cs @@ -3,7 +3,7 @@ using System; namespace Avalonia.OpenGL { [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] - class GlMinVersionEntryPoint : Attribute + sealed class GlMinVersionEntryPoint : Attribute { public GlMinVersionEntryPoint(string entry, int minVersionMajor, int minVersionMinor) { @@ -28,7 +28,7 @@ namespace Avalonia.OpenGL } [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] - class GlExtensionEntryPoint : Attribute + sealed class GlExtensionEntryPoint : Attribute { public GlExtensionEntryPoint(string entry, string extension) { diff --git a/src/Avalonia.Remote.Protocol/AvaloniaRemoteMessageGuidAttribute.cs b/src/Avalonia.Remote.Protocol/AvaloniaRemoteMessageGuidAttribute.cs index 98a843bad1..44605a2ffb 100644 --- a/src/Avalonia.Remote.Protocol/AvaloniaRemoteMessageGuidAttribute.cs +++ b/src/Avalonia.Remote.Protocol/AvaloniaRemoteMessageGuidAttribute.cs @@ -3,7 +3,7 @@ namespace Avalonia.Remote.Protocol { [AttributeUsage(AttributeTargets.Class)] - public class AvaloniaRemoteMessageGuidAttribute : Attribute + public sealed class AvaloniaRemoteMessageGuidAttribute : Attribute { public Guid Guid { get; } diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlTypes.cs b/src/Markup/Avalonia.Markup.Xaml/XamlTypes.cs index 8d6f8cdf3a..da4d7374d4 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlTypes.cs @@ -34,7 +34,8 @@ namespace Avalonia.Markup.Xaml } - public class ConstructorArgumentAttribute : Attribute + [AttributeUsage(AttributeTargets.Property)] + public sealed class ConstructorArgumentAttribute : Attribute { public ConstructorArgumentAttribute(string name) { diff --git a/src/Shared/ModuleInitializer.cs b/src/Shared/ModuleInitializer.cs index a72929e06f..e58b296474 100644 --- a/src/Shared/ModuleInitializer.cs +++ b/src/Shared/ModuleInitializer.cs @@ -1,7 +1,8 @@ namespace System.Runtime.CompilerServices { #if NETSTANDARD2_0 - internal class ModuleInitializerAttribute : Attribute + [AttributeUsage(AttributeTargets.Method)] + internal sealed class ModuleInitializerAttribute : Attribute { } diff --git a/src/Shared/SourceGeneratorAttributes.cs b/src/Shared/SourceGeneratorAttributes.cs index 3f00fbef57..bdd21d0426 100644 --- a/src/Shared/SourceGeneratorAttributes.cs +++ b/src/Shared/SourceGeneratorAttributes.cs @@ -16,7 +16,9 @@ namespace Avalonia.SourceGenerator } - internal class GetProcAddressAttribute : Attribute + + [AttributeUsage(AttributeTargets.Method)] + internal sealed class GetProcAddressAttribute : Attribute { public GetProcAddressAttribute(string proc) { @@ -39,11 +41,14 @@ namespace Avalonia.SourceGenerator } } - internal class GenerateEnumValueDictionaryAttribute : Attribute + [AttributeUsage(AttributeTargets.Method)] + internal sealed class GenerateEnumValueDictionaryAttribute : Attribute { } - internal class GenerateEnumValueListAttribute : Attribute + + [AttributeUsage(AttributeTargets.Method)] + internal sealed class GenerateEnumValueListAttribute : Attribute { } } From 936d4c3448108f793002adb87f09f1eede21dbb1 Mon Sep 17 00:00:00 2001 From: Dmitry Zhelnin Date: Thu, 2 Feb 2023 18:15:35 +0300 Subject: [PATCH 07/29] Animatable: handle transitions subscription when contol is added or removed from visual tree --- src/Avalonia.Base/Animation/Animatable.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Animation/Animatable.cs b/src/Avalonia.Base/Animation/Animatable.cs index edaa76233e..eddb89c3e8 100644 --- a/src/Avalonia.Base/Animation/Animatable.cs +++ b/src/Avalonia.Base/Animation/Animatable.cs @@ -27,6 +27,7 @@ namespace Avalonia.Animation AvaloniaProperty.Register(nameof(Transitions)); private bool _transitionsEnabled = true; + private bool _isSubscribedToTransitionsCollection = false; private Dictionary? _transitionState; /// @@ -62,6 +63,11 @@ namespace Avalonia.Animation if (Transitions is object) { + if (!_isSubscribedToTransitionsCollection) + { + _isSubscribedToTransitionsCollection = true; + Transitions.CollectionChanged += TransitionsCollectionChanged; + } AddTransitions(Transitions); } } @@ -72,7 +78,7 @@ namespace Avalonia.Animation /// /// /// This method should not be called from user code, it will be called automatically by the framework - /// when a control is added to the visual tree. + /// when a control is removed from the visual tree. /// protected void DisableTransitions() { @@ -82,6 +88,11 @@ namespace Avalonia.Animation if (Transitions is object) { + if (_isSubscribedToTransitionsCollection) + { + _isSubscribedToTransitionsCollection = false; + Transitions.CollectionChanged -= TransitionsCollectionChanged; + } RemoveTransitions(Transitions); } } @@ -110,6 +121,7 @@ namespace Avalonia.Animation } newTransitions.CollectionChanged += TransitionsCollectionChanged; + _isSubscribedToTransitionsCollection = true; AddTransitions(toAdd); } From 96bda931196bab8368969d0a1e1151a9427d618d Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Sat, 21 Jan 2023 11:43:14 +0100 Subject: [PATCH 08/29] fix: DevGenerator Nullable --- .../DevGenerators/CompositionGenerator/Generator.ListProxy.cs | 2 +- src/tools/DevGenerators/CompositionGenerator/Generator.cs | 4 ++-- src/tools/DevGenerators/EnumMemberDictionaryGenerator.cs | 2 +- src/tools/DevGenerators/GetProcAddressInitialization.cs | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/tools/DevGenerators/CompositionGenerator/Generator.ListProxy.cs b/src/tools/DevGenerators/CompositionGenerator/Generator.ListProxy.cs index 135ab0426e..c293a9101d 100644 --- a/src/tools/DevGenerators/CompositionGenerator/Generator.ListProxy.cs +++ b/src/tools/DevGenerators/CompositionGenerator/Generator.ListProxy.cs @@ -112,7 +112,7 @@ class Template var defs = cl.Members.OfType().First(m => m.Identifier.Text == "InitializeDefaults"); - cl = cl.ReplaceNode(defs.Body, defs.Body.AddStatements( + cl = cl.ReplaceNode(defs.Body!, defs.Body!.AddStatements( ParseStatement($"_list = new ServerListProxyHelper<{itemType}, {serverItemType}>(this);"))); diff --git a/src/tools/DevGenerators/CompositionGenerator/Generator.cs b/src/tools/DevGenerators/CompositionGenerator/Generator.cs index 3b5d3d8c3f..dfc8b45579 100644 --- a/src/tools/DevGenerators/CompositionGenerator/Generator.cs +++ b/src/tools/DevGenerators/CompositionGenerator/Generator.cs @@ -297,8 +297,8 @@ namespace Avalonia.SourceGenerator.CompositionGenerator server = server.WithBaseList( server.BaseList?.AddTypes(SimpleBaseType(ParseTypeName(impl.ServerName)))); - client = client.AddMembers( - ParseMemberDeclaration($"{impl.ServerName} {impl.Name}.Server => Server;")); + if(ParseMemberDeclaration($"{impl.ServerName} {impl.Name}.Server => Server;") is { } member) + client = client.AddMembers(member); } diff --git a/src/tools/DevGenerators/EnumMemberDictionaryGenerator.cs b/src/tools/DevGenerators/EnumMemberDictionaryGenerator.cs index 86dbb3a452..c975bb8444 100644 --- a/src/tools/DevGenerators/EnumMemberDictionaryGenerator.cs +++ b/src/tools/DevGenerators/EnumMemberDictionaryGenerator.cs @@ -32,7 +32,7 @@ public class EnumMemberDictionaryGenerator : IIncrementalGenerator ).Collect(); context.RegisterSourceOutput(all, static (context, methods) => { - foreach (var typeGroup in methods.GroupBy(f => f.ContainingType, SymbolEqualityComparer.Default)) + foreach (var typeGroup in methods.GroupBy(f => f.ContainingType, SymbolEqualityComparer.Default)) { var classBuilder = new StringBuilder(); if (typeGroup.Key.ContainingNamespace != null) diff --git a/src/tools/DevGenerators/GetProcAddressInitialization.cs b/src/tools/DevGenerators/GetProcAddressInitialization.cs index aedc13e7f6..e8d7c251fa 100644 --- a/src/tools/DevGenerators/GetProcAddressInitialization.cs +++ b/src/tools/DevGenerators/GetProcAddressInitialization.cs @@ -34,7 +34,7 @@ public class GetProcAddressInitializationGenerator : IIncrementalGenerator var all = fieldsWithAttribute.Collect(); context.RegisterSourceOutput(all, static (context, methods) => { - foreach (var typeGroup in methods.GroupBy(f => f.ContainingType, SymbolEqualityComparer.Default)) + foreach (var typeGroup in methods.GroupBy(f => f.ContainingType, SymbolEqualityComparer.Default)) { var nextContext = 0; var contexts = new Dictionary(); From 14fba2e53ac5348b1a86a5b28980ecd18bddc9ee Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Thu, 2 Feb 2023 17:39:36 +0100 Subject: [PATCH 09/29] Rework HitTestTextRange --- src/Avalonia.Base/Media/FormattedText.cs | 32 +- .../Media/TextFormatting/ITextSource.cs | 4 +- .../Media/TextFormatting/TextFormatter.cs | 2 +- .../Media/TextFormatting/TextFormatterImpl.cs | 18 +- .../Media/TextFormatting/TextLayout.cs | 37 +- .../Media/TextFormatting/TextLineImpl.cs | 684 ++++++++++-------- src/Avalonia.Controls/TextBlock.cs | 51 +- .../TextFormatting/TextFormatterTests.cs | 84 +++ .../Media/TextFormatting/TextLayoutTests.cs | 60 +- .../Media/TextFormatting/TextLineTests.cs | 13 +- 10 files changed, 633 insertions(+), 352 deletions(-) diff --git a/src/Avalonia.Base/Media/FormattedText.cs b/src/Avalonia.Base/Media/FormattedText.cs index 0bab473442..28757b1a1d 100644 --- a/src/Avalonia.Base/Media/FormattedText.cs +++ b/src/Avalonia.Base/Media/FormattedText.cs @@ -741,6 +741,11 @@ namespace Avalonia.Media null // no previous line break ); + if(Current is null) + { + return false; + } + // check if this line fits the text height if (_totalHeight + Current.Height > _that._maxTextHeight) { @@ -779,7 +784,7 @@ namespace Avalonia.Media // maybe there is no next line at all if (Position + Current.Length < _that._text.Length) { - bool nextLineFits; + bool nextLineFits = false; if (_lineCount + 1 >= _that._maxLineCount) { @@ -795,7 +800,10 @@ namespace Avalonia.Media currentLineBreak ); - nextLineFits = (_totalHeight + Current.Height + _nextLine.Height <= _that._maxTextHeight); + if(_nextLine != null) + { + nextLineFits = (_totalHeight + Current.Height + _nextLine.Height <= _that._maxTextHeight); + } } if (!nextLineFits) @@ -819,16 +827,22 @@ namespace Avalonia.Media _previousLineBreak ); - currentLineBreak = Current.TextLineBreak; + if(Current != null) + { + currentLineBreak = Current.TextLineBreak; + } _that._defaultParaProps.SetTextWrapping(currentWrap); } } } - _previousHeight = Current.Height; + if(Current != null) + { + _previousHeight = Current.Height; - Length = Current.Length; + Length = Current.Length; + } _previousLineBreak = currentLineBreak; @@ -838,7 +852,7 @@ namespace Avalonia.Media /// /// Wrapper of TextFormatter.FormatLine that auto-collapses the line if needed. /// - private TextLine FormatLine(ITextSource textSource, int textSourcePosition, double maxLineLength, TextParagraphProperties paraProps, TextLineBreak? lineBreak) + private TextLine? FormatLine(ITextSource textSource, int textSourcePosition, double maxLineLength, TextParagraphProperties paraProps, TextLineBreak? lineBreak) { var line = _formatter.FormatLine( textSource, @@ -848,7 +862,7 @@ namespace Avalonia.Media lineBreak ); - if (_that._trimming != TextTrimming.None && line.HasOverflowed && line.Length > 0) + if (line != null && _that._trimming != TextTrimming.None && line.HasOverflowed && line.Length > 0) { // what I really need here is the last displayed text run of the line // textSourcePosition + line.Length - 1 works except the end of paragraph case, @@ -1601,11 +1615,11 @@ namespace Avalonia.Media } /// - public TextRun? GetTextRun(int textSourceCharacterIndex) + public TextRun GetTextRun(int textSourceCharacterIndex) { if (textSourceCharacterIndex >= _that._text.Length) { - return null; + return new TextEndOfParagraph(); } var thatFormatRider = new SpanRider(_that._formatRuns, _that._latestPosition, textSourceCharacterIndex); diff --git a/src/Avalonia.Base/Media/TextFormatting/ITextSource.cs b/src/Avalonia.Base/Media/TextFormatting/ITextSource.cs index 26966b37bc..32012ab8e9 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ITextSource.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ITextSource.cs @@ -1,6 +1,4 @@ -using Avalonia.Metadata; - -namespace Avalonia.Media.TextFormatting +namespace Avalonia.Media.TextFormatting { /// /// Produces objects that are used by the . diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatter.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatter.cs index 0b5d7649d7..ff8c1c4860 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatter.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatter.cs @@ -38,7 +38,7 @@ /// A value that specifies the text formatter state, /// in terms of where the previous line in the paragraph was broken by the text formatting process. /// The formatted line. - public abstract TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, + public abstract TextLine? FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties, TextLineBreak? previousLineBreak = null); } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index 812c4e9eb8..7f74f49982 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -18,7 +18,7 @@ namespace Avalonia.Media.TextFormatting [ThreadStatic] private static BidiAlgorithm? t_bidiAlgorithm; /// - public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, + public override TextLine? FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties, TextLineBreak? previousLineBreak = null) { TextLineBreak? nextLineBreak = null; @@ -41,6 +41,11 @@ namespace Avalonia.Media.TextFormatting fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, objectPool, out var textEndOfLine, out var textSourceLength); + if (fetchedRuns.Count == 0) + { + return null; + } + shapedTextRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, objectPool, fontManager, out var resolvedFlowDirection); @@ -491,16 +496,7 @@ namespace Avalonia.Media.TextFormatting while (textRunEnumerator.MoveNext()) { - var textRun = textRunEnumerator.Current; - - if (textRun == null) - { - textRuns.Add(new TextEndOfParagraph()); - - textSourceLength += TextRun.DefaultTextSourceLength; - - break; - } + TextRun textRun = textRunEnumerator.Current!; if (textRun is TextEndOfLine textEndOfLine) { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index 8e85c10bba..4dbc472133 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -238,7 +238,7 @@ namespace Avalonia.Media.TextFormatting foreach (var textLine in _textLines) { //Current line isn't covered. - if (textLine.FirstTextSourceIndex + textLine.Length < start) + if (textLine.FirstTextSourceIndex + textLine.Length <= start) { currentY += textLine.Height; @@ -348,14 +348,36 @@ namespace Avalonia.Media.TextFormatting { var (x, y) = point; - var lastTrailingIndex = textLine.FirstTextSourceIndex + textLine.Length; - var isInside = x >= 0 && x <= textLine.Width && y >= 0 && y <= textLine.Height; - if (x >= textLine.Width && textLine.Length > 0 && textLine.NewLineLength > 0) + var lastTrailingIndex = 0; + + if(_paragraphProperties.FlowDirection== FlowDirection.LeftToRight) { - lastTrailingIndex -= textLine.NewLineLength; + lastTrailingIndex = textLine.FirstTextSourceIndex + textLine.Length; + + if (x >= textLine.Width && textLine.Length > 0 && textLine.NewLineLength > 0) + { + lastTrailingIndex -= textLine.NewLineLength; + } + + if (textLine.TextLineBreak?.TextEndOfLine is TextEndOfLine textEndOfLine) + { + lastTrailingIndex -= textEndOfLine.Length; + } } + else + { + if (x <= textLine.WidthIncludingTrailingWhitespace - textLine.Width && textLine.Length > 0 && textLine.NewLineLength > 0) + { + lastTrailingIndex += textLine.NewLineLength; + } + + if (textLine.TextLineBreak?.TextEndOfLine is TextEndOfLine textEndOfLine) + { + lastTrailingIndex += textEndOfLine.Length; + } + } var textPosition = characterHit.FirstCharacterIndex + characterHit.TrailingLength; @@ -391,7 +413,7 @@ namespace Avalonia.Media.TextFormatting /// private static TextParagraphProperties CreateTextParagraphProperties(Typeface typeface, double fontSize, IBrush? foreground, TextAlignment textAlignment, TextWrapping textWrapping, - TextDecorationCollection? textDecorations, FlowDirection flowDirection, double lineHeight, + TextDecorationCollection? textDecorations, FlowDirection flowDirection, double lineHeight, double letterSpacing) { var textRunStyle = new GenericTextRunProperties(typeface, fontSize, textDecorations, foreground); @@ -456,7 +478,7 @@ namespace Avalonia.Media.TextFormatting var textLine = textFormatter.FormatLine(_textSource, _textSourceLength, MaxWidth, _paragraphProperties, previousLine?.TextLineBreak); - if (textLine.Length == 0) + if (textLine is null) { if (previousLine != null && previousLine.NewLineLength > 0) { @@ -518,7 +540,6 @@ namespace Avalonia.Media.TextFormatting } } - //Make sure the TextLayout always contains at least on empty line if (textLines.Count == 0) { var textLine = TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties); diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index d29063e07d..187b3154ad 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -10,6 +10,7 @@ namespace Avalonia.Media.TextFormatting private readonly double _paragraphWidth; private readonly TextParagraphProperties _paragraphProperties; private TextLineMetrics _textLineMetrics; + private TextLineBreak? _textLineBreak; private readonly FlowDirection _resolvedFlowDirection; public TextLineImpl(TextRun[] textRuns, int firstTextSourceIndex, int length, double paragraphWidth, @@ -18,7 +19,7 @@ namespace Avalonia.Media.TextFormatting { FirstTextSourceIndex = firstTextSourceIndex; Length = length; - TextLineBreak = lineBreak; + _textLineBreak = lineBreak; HasCollapsed = hasCollapsed; _textRuns = textRuns; @@ -38,7 +39,7 @@ namespace Avalonia.Media.TextFormatting public override int Length { get; } /// - public override TextLineBreak? TextLineBreak { get; } + public override TextLineBreak? TextLineBreak => _textLineBreak; /// public override bool HasCollapsed { get; } @@ -167,50 +168,54 @@ namespace Avalonia.Media.TextFormatting { if (_textRuns.Length == 0) { - return new CharacterHit(); + return new CharacterHit(FirstTextSourceIndex); } distance -= Start; - var firstRunIndex = 0; + var lastIndex = _textRuns.Length - 1; - if (_textRuns[firstRunIndex] is TextEndOfLine) + if (_textRuns[lastIndex] is TextEndOfLine) { - firstRunIndex++; + lastIndex--; } - if(firstRunIndex >= _textRuns.Length) + var currentPosition = FirstTextSourceIndex; + + if (lastIndex < 0) { - return new CharacterHit(FirstTextSourceIndex); + return new CharacterHit(currentPosition); } if (distance <= 0) { - var firstRun = _textRuns[firstRunIndex]; + var firstRun = _textRuns[0]; - return GetRunCharacterHit(firstRun, FirstTextSourceIndex, 0); + if (_paragraphProperties.FlowDirection == FlowDirection.RightToLeft) + { + currentPosition = Length - firstRun.Length; + } + + return GetRunCharacterHit(firstRun, currentPosition, 0); } if (distance >= WidthIncludingTrailingWhitespace) { - var lastRun = _textRuns[_textRuns.Length - 1]; - - var size = 0.0; + var lastRun = _textRuns[lastIndex]; - if (lastRun is DrawableTextRun drawableTextRun) + if (_paragraphProperties.FlowDirection == FlowDirection.LeftToRight) { - size = drawableTextRun.Size.Width; + currentPosition = Length - lastRun.Length; } - return GetRunCharacterHit(lastRun, FirstTextSourceIndex + Length - lastRun.Length, size); + return GetRunCharacterHit(lastRun, currentPosition, distance); } // process hit that happens within the line var characterHit = new CharacterHit(); - var currentPosition = FirstTextSourceIndex; var currentDistance = 0.0; - for (var i = 0; i < _textRuns.Length; i++) + for (var i = 0; i <= lastIndex; i++) { var currentRun = _textRuns[i]; @@ -242,7 +247,7 @@ namespace Avalonia.Media.TextFormatting currentRun = _textRuns[j]; - if(currentRun is not ShapedTextRun) + if (currentRun is not ShapedTextRun) { continue; } @@ -274,10 +279,6 @@ namespace Avalonia.Media.TextFormatting continue; } } - else - { - continue; - } break; } @@ -422,10 +423,10 @@ namespace Avalonia.Media.TextFormatting { if (currentGlyphRun != null) { - distance = currentGlyphRun.Size.Width - distance; + currentDistance -= currentGlyphRun.Size.Width; } - return Math.Max(0, currentDistance - distance); + return currentDistance + distance; } if (currentRun is DrawableTextRun drawableTextRun) @@ -575,386 +576,505 @@ namespace Avalonia.Media.TextFormatting return GetPreviousCaretCharacterHit(characterHit); } - private IReadOnlyList GetTextBoundsLeftToRight(int firstTextSourceIndex, int textLength) + public override IReadOnlyList GetTextBounds(int firstTextSourceIndex, int textLength) { - var characterIndex = firstTextSourceIndex + textLength; + if (_textRuns.Length == 0) + { + return Array.Empty(); + } - var result = new List(_textRuns.Length); - var lastDirection = FlowDirection.LeftToRight; - var currentDirection = lastDirection; + var result = new List(); var currentPosition = FirstTextSourceIndex; var remainingLength = textLength; - var startX = Start; - double currentWidth = 0; - var currentRect = default(Rect); - - TextRunBounds lastRunBounds = default; - - for (var index = 0; index < _textRuns.Length; index++) + static FlowDirection GetDirection(TextRun textRun, FlowDirection currentDirection) { - if (_textRuns[index] is not DrawableTextRun currentRun) + if (textRun is ShapedTextRun shapedTextRun) { - continue; + return shapedTextRun.ShapedBuffer.IsLeftToRight ? + FlowDirection.LeftToRight : + FlowDirection.RightToLeft; } - var characterLength = 0; - var endX = startX; - - TextRunBounds currentRunBounds; + return currentDirection; + } - double combinedWidth; + if (_paragraphProperties.FlowDirection == FlowDirection.LeftToRight) + { + var currentX = Start; - if (currentRun is ShapedTextRun currentShapedRun) + for (int i = 0; i < _textRuns.Length; i++) { - var firstCluster = currentShapedRun.GlyphRun.Metrics.FirstCluster; + var currentRun = _textRuns[i]; + + var firstRunIndex = i; + var lastRunIndex = firstRunIndex; + var currentDirection = GetDirection(currentRun, FlowDirection.LeftToRight); + var directionalWidth = 0.0; - if (currentPosition + currentRun.Length <= firstTextSourceIndex) + if (currentRun is DrawableTextRun currentDrawable) { - startX += currentRun.Size.Width; + directionalWidth = currentDrawable.Size.Width; + } - currentPosition += currentRun.Length; + // Find consecutive runs of same direction + for (; lastRunIndex + 1 < _textRuns.Length; lastRunIndex++) + { + var nextRun = _textRuns[lastRunIndex + 1]; - continue; + var nextDirection = GetDirection(nextRun, currentDirection); + + if (currentDirection != nextDirection) + { + break; + } + + if (nextRun is DrawableTextRun nextDrawable) + { + directionalWidth += nextDrawable.Size.Width; + } } - if (currentShapedRun.ShapedBuffer.IsLeftToRight) + //Skip runs that are not part of the hit test range + switch (currentDirection) { - var startIndex = firstCluster + Math.Max(0, firstTextSourceIndex - currentPosition); + case FlowDirection.RightToLeft: + { + for (; lastRunIndex >= firstRunIndex; lastRunIndex--) + { + currentRun = _textRuns[lastRunIndex]; + + if (currentPosition + currentRun.Length > firstTextSourceIndex) + { + break; + } + + currentPosition += currentRun.Length; - double startOffset; + if (currentRun is DrawableTextRun drawableTextRun) + { + directionalWidth -= drawableTextRun.Size.Width; + currentX += drawableTextRun.Size.Width; + } - double endOffset; + if(lastRunIndex - 1 < 0) + { + break; + } + } - startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); + break; + } + default: + { + for (; firstRunIndex <= lastRunIndex; firstRunIndex++) + { + currentRun = _textRuns[firstRunIndex]; - endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); + if (currentPosition + currentRun.Length > firstTextSourceIndex) + { + break; + } - startX += startOffset; + currentPosition += currentRun.Length; - endX += endOffset; + if (currentRun is DrawableTextRun drawableTextRun) + { + currentX += drawableTextRun.Size.Width; + directionalWidth -= drawableTextRun.Size.Width; + } - var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); + if(firstRunIndex + 1 == _textRuns.Length) + { + break; + } + } - var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); + break; + } + } - characterLength = Math.Abs(endHit.FirstCharacterIndex + endHit.TrailingLength - startHit.FirstCharacterIndex - startHit.TrailingLength); + i = lastRunIndex; - currentDirection = FlowDirection.LeftToRight; + if (directionalWidth == 0) + { + continue; } - else + + var coveredLength = 0; + TextBounds? textBounds = null; + + switch (currentDirection) { - var rightToLeftIndex = index; - var rightToLeftWidth = currentShapedRun.Size.Width; - while (rightToLeftIndex + 1 <= _textRuns.Length - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextRun nextShapedRun) - { - if (nextShapedRun == null || nextShapedRun.ShapedBuffer.IsLeftToRight) + case FlowDirection.RightToLeft: { + textBounds = GetTextRunBoundsRightToLeft(firstRunIndex, lastRunIndex, currentX + directionalWidth, firstTextSourceIndex, + currentPosition, remainingLength, out coveredLength, out currentPosition); + + currentX += directionalWidth; + break; } + default: + { + textBounds = GetTextBoundsLeftToRight(firstRunIndex, lastRunIndex, currentX, firstTextSourceIndex, + currentPosition, remainingLength, out coveredLength, out currentPosition); - rightToLeftIndex++; - - rightToLeftWidth += nextShapedRun.Size.Width; + currentX = textBounds.Rectangle.Right; - if (currentPosition + nextShapedRun.Length > firstTextSourceIndex + textLength) - { break; } + } - currentShapedRun = nextShapedRun; - } + if (coveredLength > 0) + { + result.Add(textBounds); + + remainingLength -= coveredLength; + } + + if (remainingLength <= 0) + { + break; + } + } + } + else + { + var currentX = Start + WidthIncludingTrailingWhitespace; - startX += rightToLeftWidth; + for (int i = _textRuns.Length - 1; i >= 0; i--) + { + var currentRun = _textRuns[i]; + var firstRunIndex = i; + var lastRunIndex = firstRunIndex; + var currentDirection = GetDirection(currentRun, FlowDirection.RightToLeft); + var directionalWidth = 0.0; - currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength); + if (currentRun is DrawableTextRun currentDrawable) + { + directionalWidth = currentDrawable.Size.Width; + } - remainingLength -= currentRunBounds.Length; - currentPosition = currentRunBounds.TextSourceCharacterIndex + currentRunBounds.Length; - endX = currentRunBounds.Rectangle.Right; - startX = currentRunBounds.Rectangle.Left; + // Find consecutive runs of same direction + for (; firstRunIndex - 1 > 0; firstRunIndex--) + { + var previousRun = _textRuns[firstRunIndex - 1]; - var rightToLeftRunBounds = new List { currentRunBounds }; + var previousDirection = GetDirection(previousRun, currentDirection); - for (int i = rightToLeftIndex - 1; i >= index; i--) + if (currentDirection != previousDirection) { - if (_textRuns[i] is not ShapedTextRun shapedRun) + break; + } + + if (currentRun is DrawableTextRun previousDrawable) + { + directionalWidth += previousDrawable.Size.Width; + } + } + + //Skip runs that are not part of the hit test range + switch (currentDirection) + { + case FlowDirection.RightToLeft: { - continue; - } + for (; lastRunIndex >= firstRunIndex; lastRunIndex--) + { + currentRun = _textRuns[lastRunIndex]; - currentShapedRun = shapedRun; + if (currentPosition + currentRun.Length <= firstTextSourceIndex) + { + currentPosition += currentRun.Length; - currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength); + if (currentRun is DrawableTextRun drawableTextRun) + { + currentX -= drawableTextRun.Size.Width; + directionalWidth -= drawableTextRun.Size.Width; + } - rightToLeftRunBounds.Insert(0, currentRunBounds); + continue; + } - remainingLength -= currentRunBounds.Length; - startX = currentRunBounds.Rectangle.Left; + break; + } - currentPosition += currentRunBounds.Length; - } + break; + } + default: + { + for (; firstRunIndex <= lastRunIndex; firstRunIndex++) + { + currentRun = _textRuns[firstRunIndex]; - combinedWidth = endX - startX; + if (currentPosition + currentRun.Length <= firstTextSourceIndex) + { + currentPosition += currentRun.Length; - currentRect = new Rect(startX, 0, combinedWidth, Height); + if (currentRun is DrawableTextRun drawableTextRun) + { + currentX += drawableTextRun.Size.Width; + directionalWidth -= drawableTextRun.Size.Width; + } - currentDirection = FlowDirection.RightToLeft; + continue; + } - if (!MathUtilities.IsZero(combinedWidth)) - { - result.Add(new TextBounds(currentRect, currentDirection, rightToLeftRunBounds)); - } + break; + } - startX = endX; + break; + } } - } - else - { - if (currentPosition + currentRun.Length <= firstTextSourceIndex) - { - startX += currentRun.Size.Width; - currentPosition += currentRun.Length; + i = firstRunIndex; + if (directionalWidth == 0) + { continue; } - if (currentPosition < firstTextSourceIndex) - { - startX += currentRun.Size.Width; - } + var coveredLength = 0; - if (currentPosition + currentRun.Length <= characterIndex) + TextBounds? textBounds = null; + + switch (currentDirection) { - endX += currentRun.Size.Width; + case FlowDirection.LeftToRight: + { + textBounds = GetTextBoundsLeftToRight(firstRunIndex, lastRunIndex, currentX - directionalWidth, firstTextSourceIndex, + currentPosition, remainingLength, out coveredLength, out currentPosition); + + currentX -= directionalWidth; - characterLength = currentRun.Length; + break; + } + default: + { + textBounds = GetTextRunBoundsRightToLeft(firstRunIndex, lastRunIndex, currentX, firstTextSourceIndex, + currentPosition, remainingLength, out coveredLength, out currentPosition); + + currentX = textBounds.Rectangle.Left; + + break; + } } - } - if (endX < startX) - { - (endX, startX) = (startX, endX); - } + //Visual order is always left to right so we need to insert + result.Insert(0, textBounds); - //Lines that only contain a linebreak need to be covered here - if (characterLength == 0) - { - characterLength = NewLineLength; + remainingLength -= coveredLength; + + if (remainingLength <= 0) + { + break; + } } + } - combinedWidth = endX - startX; + return result; + } - currentRunBounds = new TextRunBounds(new Rect(startX, 0, combinedWidth, Height), currentPosition, characterLength, currentRun); + private TextBounds GetTextRunBoundsRightToLeft(int firstRunIndex, int lastRunIndex, double endX, + int firstTextSourceIndex, int currentPosition, int remainingLength, out int coveredLength, out int newPosition) + { + coveredLength = 0; + var textRunBounds = new List(); + var startX = endX; - currentPosition += characterLength; + for (int i = lastRunIndex; i >= firstRunIndex; i--) + { + var currentRun = _textRuns[i]; - remainingLength -= characterLength; + if (currentRun is ShapedTextRun shapedTextRun) + { + var runBounds = GetRunBoundsRightToLeft(shapedTextRun, startX, firstTextSourceIndex, remainingLength, currentPosition, out var offset); - startX = endX; + textRunBounds.Insert(0, runBounds); - if (currentRunBounds.TextRun != null && !MathUtilities.IsZero(combinedWidth) || NewLineLength > 0) - { - if (result.Count > 0 && lastDirection == currentDirection && MathUtilities.AreClose(currentRect.Left, lastRunBounds.Rectangle.Right)) + if (offset > 0) { - currentRect = currentRect.WithWidth(currentWidth + combinedWidth); + endX = runBounds.Rectangle.Right; - var textBounds = result[result.Count - 1]; + startX = endX; + } - textBounds.Rectangle = currentRect; + startX -= runBounds.Rectangle.Width; - textBounds.TextRunBounds.Add(currentRunBounds); - } - else + currentPosition += runBounds.Length + offset; + + coveredLength += runBounds.Length; + + remainingLength -= runBounds.Length; + } + else + { + if (currentRun is DrawableTextRun drawableTextRun) { - currentRect = currentRunBounds.Rectangle; + startX -= drawableTextRun.Size.Width; - result.Add(new TextBounds(currentRect, currentDirection, new List { currentRunBounds })); + textRunBounds.Insert(0, + new TextRunBounds( + new Rect(startX, 0, drawableTextRun.Size.Width, Height), currentPosition, currentRun.Length, currentRun)); } - } - lastRunBounds = currentRunBounds; + currentPosition += currentRun.Length; + + coveredLength += currentRun.Length; - currentWidth += combinedWidth; + remainingLength -= currentRun.Length; + } - if (remainingLength <= 0 || currentPosition >= characterIndex) + if (remainingLength <= 0) { break; } - - lastDirection = currentDirection; } - return result; - } + newPosition = currentPosition; - private IReadOnlyList GetTextBoundsRightToLeft(int firstTextSourceIndex, int textLength) - { - var characterIndex = firstTextSourceIndex + textLength; + var runWidth = endX - startX; - var result = new List(_textRuns.Length); - var lastDirection = FlowDirection.LeftToRight; - var currentDirection = lastDirection; + var bounds = new Rect(startX, 0, runWidth, Height); - var currentPosition = FirstTextSourceIndex; - var remainingLength = textLength; + return new TextBounds(bounds, FlowDirection.RightToLeft, textRunBounds); + } - var startX = WidthIncludingTrailingWhitespace; - double currentWidth = 0; - var currentRect = default(Rect); + private TextBounds GetTextBoundsLeftToRight(int firstRunIndex, int lastRunIndex, double startX, + int firstTextSourceIndex, int currentPosition, int remainingLength, out int coveredLength, out int newPosition) + { + coveredLength = 0; + var textRunBounds = new List(); + var endX = startX; - for (var index = _textRuns.Length - 1; index >= 0; index--) + for (int i = firstRunIndex; i <= lastRunIndex; i++) { - if (_textRuns[index] is not DrawableTextRun currentRun) - { - continue; - } - - if (currentPosition + currentRun.Length < firstTextSourceIndex) - { - startX -= currentRun.Size.Width; - - currentPosition += currentRun.Length; - - continue; - } - - var characterLength = 0; - var endX = startX; + var currentRun = _textRuns[i]; - if (currentRun is ShapedTextRun currentShapedRun) + if (currentRun is ShapedTextRun shapedTextRun) { - var offset = Math.Max(0, firstTextSourceIndex - currentPosition); - - currentPosition += offset; - - var startIndex = currentPosition; - double startOffset; - double endOffset; + var runBounds = GetRunBoundsLeftToRight(shapedTextRun, endX, firstTextSourceIndex, remainingLength, currentPosition, out var offset); - if (currentShapedRun.ShapedBuffer.IsLeftToRight) - { - if (currentPosition < startIndex) - { - startOffset = endOffset = 0; - } - else - { - endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); + textRunBounds.Add(runBounds); - startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); - } - } - else + if (offset > 0) { - endOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); + startX = runBounds.Rectangle.Left; - startOffset = currentShapedRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); + endX = startX; } - startX -= currentRun.Size.Width - startOffset; - endX -= currentRun.Size.Width - endOffset; + currentPosition += runBounds.Length + offset; - var endHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); - var startHit = currentShapedRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); + endX += runBounds.Rectangle.Width; - characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength); + coveredLength += runBounds.Length; - currentDirection = currentShapedRun.ShapedBuffer.IsLeftToRight ? - FlowDirection.LeftToRight : - FlowDirection.RightToLeft; + remainingLength -= runBounds.Length; } else { - if (currentPosition + currentRun.Length <= characterIndex) + if (currentRun is DrawableTextRun drawableTextRun) { - endX -= currentRun.Size.Width; + textRunBounds.Add( + new TextRunBounds( + new Rect(endX, 0, drawableTextRun.Size.Width, Height), currentPosition, currentRun.Length, currentRun)); + + endX += drawableTextRun.Size.Width; } - if (currentPosition < firstTextSourceIndex) - { - startX -= currentRun.Size.Width; + currentPosition += currentRun.Length; - characterLength = currentRun.Length; - } - } + coveredLength += currentRun.Length; - if (endX < startX) - { - (endX, startX) = (startX, endX); + remainingLength -= currentRun.Length; } - //Lines that only contain a linebreak need to be covered here - if (characterLength == 0) + if (remainingLength <= 0) { - characterLength = NewLineLength; + break; } + } - var runWidth = endX - startX; + newPosition = currentPosition; - var currentRunBounds = new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); + var runWidth = endX - startX; - if (!MathUtilities.IsZero(runWidth) || NewLineLength > 0) - { - if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, Start + startX)) - { - currentRect = currentRect.WithWidth(currentWidth + runWidth); + var bounds = new Rect(startX, 0, runWidth, Height); - var textBounds = result[result.Count - 1]; + return new TextBounds(bounds, FlowDirection.LeftToRight, textRunBounds); + } - textBounds.Rectangle = currentRect; + private TextRunBounds GetRunBoundsLeftToRight(ShapedTextRun currentRun, double startX, + int firstTextSourceIndex, int remainingLength, int currentPosition, out int offset) + { + var startIndex = currentPosition; - textBounds.TextRunBounds.Add(currentRunBounds); - } - else - { - currentRect = currentRunBounds.Rectangle; + offset = Math.Max(0, firstTextSourceIndex - currentPosition); - result.Add(new TextBounds(currentRect, currentDirection, new List { currentRunBounds })); - } - } + var firstCluster = currentRun.GlyphRun.Metrics.FirstCluster; - currentWidth += runWidth; - currentPosition += characterLength; + if (currentPosition != firstCluster) + { + startIndex = firstCluster + offset; + } + else + { + startIndex += offset; + } - if (currentPosition > characterIndex) - { - break; - } + var startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); + var endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); - lastDirection = currentDirection; - remainingLength -= characterLength; + var endX = startX + endOffset; + startX += startOffset; - if (remainingLength <= 0) - { - break; - } + var startHit = currentRun.GlyphRun.GetCharacterHitFromDistance(startOffset, out _); + var endHit = currentRun.GlyphRun.GetCharacterHitFromDistance(endOffset, out _); + + var characterLength = Math.Abs(startHit.FirstCharacterIndex + startHit.TrailingLength - endHit.FirstCharacterIndex - endHit.TrailingLength); + + if (endX < startX) + { + (endX, startX) = (startX, endX); + } + + //Lines that only contain a linebreak need to be covered here + if (characterLength == 0) + { + characterLength = NewLineLength; } - result.Reverse(); + var runWidth = endX - startX; - return result; + return new TextRunBounds(new Rect(startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); } - private TextRunBounds GetRightToLeftTextRunBounds(ShapedTextRun currentRun, double endX, int firstTextSourceIndex, int characterIndex, int currentPosition, int remainingLength) + private TextRunBounds GetRunBoundsRightToLeft(ShapedTextRun currentRun, double endX, + int firstTextSourceIndex, int remainingLength, int currentPosition, out int offset) { var startX = endX; - var offset = Math.Max(0, firstTextSourceIndex - currentPosition); + var startIndex = currentPosition; - currentPosition += offset; + offset = Math.Max(0, firstTextSourceIndex - currentPosition); - var startIndex = currentPosition; + var firstCluster = currentRun.GlyphRun.Metrics.FirstCluster; - double startOffset; - double endOffset; + if (currentPosition != firstCluster) + { + startIndex = firstCluster + offset; + } + else + { + startIndex += offset; + } - endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); + var endOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex)); - startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); + var startOffset = currentRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(startIndex + remainingLength)); startX -= currentRun.Size.Width - startOffset; endX -= currentRun.Size.Width - endOffset; @@ -980,16 +1100,6 @@ namespace Avalonia.Media.TextFormatting return new TextRunBounds(new Rect(Start + startX, 0, runWidth, Height), currentPosition, characterLength, currentRun); } - public override IReadOnlyList GetTextBounds(int firstTextSourceIndex, int textLength) - { - if (_paragraphProperties.FlowDirection == FlowDirection.LeftToRight) - { - return GetTextBoundsLeftToRight(firstTextSourceIndex, textLength); - } - - return GetTextBoundsRightToLeft(firstTextSourceIndex, textLength); - } - public override void Dispose() { for (int i = 0; i < _textRuns.Length; i++) @@ -1005,6 +1115,11 @@ namespace Avalonia.Media.TextFormatting { _textLineMetrics = CreateLineMetrics(); + if (_textLineBreak is null && _textRuns.Length > 1 && _textRuns[_textRuns.Length - 1] is TextEndOfLine textEndOfLine) + { + _textLineBreak = new TextLineBreak(textEndOfLine); + } + BidiReorderer.Instance.BidiReorder(_textRuns, _resolvedFlowDirection); } @@ -1328,7 +1443,7 @@ namespace Avalonia.Media.TextFormatting { width = widthIncludingWhitespace + textRun.GlyphRun.Metrics.Width; trailingWhitespaceLength = textRun.GlyphRun.Metrics.TrailingWhitespaceLength; - newLineLength = textRun.GlyphRun.Metrics.NewLineLength; + newLineLength += textRun.GlyphRun.Metrics.NewLineLength; } widthIncludingWhitespace += textRun.Size.Width; @@ -1340,31 +1455,10 @@ namespace Avalonia.Media.TextFormatting { widthIncludingWhitespace += drawableTextRun.Size.Width; - switch (_paragraphProperties.FlowDirection) + if (index == lastRunIndex) { - case FlowDirection.LeftToRight: - { - if (index == lastRunIndex) - { - width = widthIncludingWhitespace; - trailingWhitespaceLength = 0; - newLineLength = 0; - } - - break; - } - - case FlowDirection.RightToLeft: - { - if (index == lastRunIndex) - { - width = widthIncludingWhitespace; - trailingWhitespaceLength = 0; - newLineLength = 0; - } - - break; - } + width = widthIncludingWhitespace; + trailingWhitespaceLength = 0; } if (drawableTextRun.Size.Height > height) diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 9bd1dc95f9..df9a3eb8f3 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -720,6 +720,16 @@ namespace Avalonia.Controls var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale); + if (HasComplexContent) + { + ArrangeComplexContent(TextLayout, padding); + } + + if (MathUtilities.AreClose(_constraint.Inflate(padding).Width, finalSize.Width)) + { + return finalSize; + } + _constraint = new Size(Math.Ceiling(finalSize.Deflate(padding).Width), double.PositiveInfinity); _textLayout?.Dispose(); @@ -727,31 +737,36 @@ namespace Avalonia.Controls if (HasComplexContent) { - var currentY = padding.Top; + ArrangeComplexContent(TextLayout, padding); + } - foreach (var textLine in TextLayout.TextLines) - { - var currentX = padding.Left + textLine.Start; + return finalSize; + } - foreach (var run in textLine.TextRuns) + private static void ArrangeComplexContent(TextLayout textLayout, Thickness padding) + { + var currentY = padding.Top; + + foreach (var textLine in textLayout.TextLines) + { + var currentX = padding.Left + textLine.Start; + + foreach (var run in textLine.TextRuns) + { + if (run is DrawableTextRun drawable) { - if (run is DrawableTextRun drawable) + if (drawable is EmbeddedControlRun controlRun + && controlRun.Control is Control control) { - if (drawable is EmbeddedControlRun controlRun - && controlRun.Control is Control control) - { - control.Arrange(new Rect(new Point(currentX, currentY), control.DesiredSize)); - } - - currentX += drawable.Size.Width; + control.Arrange(new Rect(new Point(currentX, currentY), control.DesiredSize)); } - } - currentY += textLine.Height; + currentX += drawable.Size.Width; + } } - } - return finalSize; + currentY += textLine.Height; + } } protected override AutomationPeer OnCreateAutomationPeer() @@ -892,7 +907,7 @@ namespace Avalonia.Controls return textRun; } - return null; + return new TextEndOfParagraph(); } } } diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs index 954169f975..8a2d4ecc6b 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs @@ -660,6 +660,90 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } + [Fact] + public void Should_Return_Null_For_Empty_TextSource() + { + using (Start()) + { + var defaultRunProperties = new GenericTextRunProperties(Typeface.Default); + var paragraphProperties = new GenericTextParagraphProperties(defaultRunProperties); + var textSource = new EmptyTextSource(); + + var textLine = TextFormatter.Current.FormatLine(textSource, 0, double.PositiveInfinity, paragraphProperties); + + Assert.Null(textLine); + } + } + + [Fact] + public void Should_Retain_TextEndOfParagraph_With_TextWrapping() + { + using (Start()) + { + var defaultRunProperties = new GenericTextRunProperties(Typeface.Default); + var paragraphProperties = new GenericTextParagraphProperties(defaultRunProperties, textWrap: TextWrapping.Wrap); + + var text = "Hello World"; + + var textSource = new SimpleTextSource(text, defaultRunProperties); + + var pos = 0; + + TextLineBreak previousLineBreak = null; + TextLine textLine = null; + + while (pos < text.Length) + { + textLine = TextFormatter.Current.FormatLine(textSource, pos, 30, paragraphProperties, previousLineBreak); + + pos += textLine.Length; + + previousLineBreak = textLine.TextLineBreak; + } + + Assert.NotNull(textLine); + + Assert.NotNull(textLine.TextLineBreak.TextEndOfLine); + } + } + + protected readonly record struct SimpleTextSource : ITextSource + { + private readonly string _text; + private readonly TextRunProperties _defaultProperties; + + public SimpleTextSource(string text, TextRunProperties defaultProperties) + { + _text = text; + _defaultProperties = defaultProperties; + } + + public TextRun? GetTextRun(int textSourceIndex) + { + if (textSourceIndex > _text.Length) + { + return new TextEndOfParagraph(); + } + + var runText = _text.AsMemory(textSourceIndex); + + if (runText.IsEmpty) + { + return new TextEndOfParagraph(); + } + + return new TextCharacters(runText, _defaultProperties); + } + } + + private class EmptyTextSource : ITextSource + { + public TextRun GetTextRun(int textSourceIndex) + { + return null; + } + } + private class EndOfLineTextSource : ITextSource { public TextRun GetTextRun(int textSourceIndex) diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index 2b63f24cf6..3735e9f6d7 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -9,7 +9,6 @@ using Avalonia.Media.TextFormatting.Unicode; using Avalonia.UnitTests; using Avalonia.Utilities; using Xunit; - namespace Avalonia.Skia.UnitTests.Media.TextFormatting { public class TextLayoutTests @@ -1028,6 +1027,65 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } + [InlineData("mgfg🧐df f sdf", "g🧐d", 20, 40)] + [InlineData("وه. وقد تعرض لانتقادات", "دات", 5, 30)] + [InlineData("وه. وقد تعرض لانتقادات", "تعرض", 20, 50)] + [InlineData(" علمية 😱ومضللة ،", " علمية 😱ومضللة ،", 40, 100)] + [InlineData("في عام 2018 ، رفعت ل", "في عام 2018 ، رفعت ل", 100, 120)] + [Theory] + public void HitTestTextRange_Range_ValidLength(string text, string textToSelect, double minWidth, double maxWidth) + { + using (Start()) + { + var layout = new TextLayout(text, Typeface.Default, 12, Brushes.Black); + var start = text.IndexOf(textToSelect); + var selectionRectangles = layout.HitTestTextRange(start, textToSelect.Length); + Assert.Equal(1, selectionRectangles.Count()); + var rect = selectionRectangles.First(); + Assert.InRange(rect.Width, minWidth, maxWidth); + } + } + + [InlineData("012🧐210", 2, 4, FlowDirection.LeftToRight, "14.40234375,40.8046875")] + [InlineData("210🧐012", 2, 4, FlowDirection.RightToLeft, "0,7.201171875;21.603515625,33.603515625;48.005859375,55.20703125")] + [InlineData("שנב🧐שנב", 2, 4, FlowDirection.LeftToRight, "11.63671875,39.779296875")] + [InlineData("שנב🧐שנב", 2, 4, FlowDirection.RightToLeft, "11.63671875,39.779296875")] + [Theory] + public void Should_HitTextTextRangeBetweenRuns(string text, int start, int length, + FlowDirection flowDirection, string expected) + { + using (Start()) + { + var expectedRects = expected.Split(';').Select(x => + { + var startEnd = x.Split(','); + + var start = double.Parse(startEnd[0], CultureInfo.InvariantCulture); + + var end = double.Parse(startEnd[1], CultureInfo.InvariantCulture); + + return new Rect(start, 0, end - start, 0); + }).ToArray(); + + var textLayout = new TextLayout(text, Typeface.Default, 12, Brushes.Black, flowDirection: flowDirection); + + var rects = textLayout.HitTestTextRange(start, length).ToArray(); + + Assert.Equal(expectedRects.Length, rects.Length); + + var endX = textLayout.TextLines[0].GetDistanceFromCharacterHit(new CharacterHit(2)); + var startX = textLayout.TextLines[0].GetDistanceFromCharacterHit(new CharacterHit(5, 1)); + + for (int i = 0; i < expectedRects.Length; i++) + { + var expectedRect = expectedRects[i]; + + Assert.Equal(expectedRect.Left, rects[i].Left); + + Assert.Equal(expectedRect.Right, rects[i].Right); + } + } + } private static IDisposable Start() diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index 544b84912e..e47542af7a 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -604,19 +604,19 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting textBounds = textLine.GetTextBounds(0, 20); - Assert.Equal(2, textBounds.Count); + Assert.Equal(1, textBounds.Count); Assert.Equal(144.0234375, textBounds.Sum(x => x.Rectangle.Width)); textBounds = textLine.GetTextBounds(0, 30); - Assert.Equal(3, textBounds.Count); + Assert.Equal(1, textBounds.Count); Assert.Equal(216.03515625, textBounds.Sum(x => x.Rectangle.Width)); textBounds = textLine.GetTextBounds(0, 40); - Assert.Equal(4, textBounds.Count); + Assert.Equal(1, textBounds.Count); Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width)); } @@ -847,7 +847,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var textBounds = textLine.GetTextBounds(0, textLine.Length); - Assert.Equal(6, textBounds.Count); + Assert.Equal(1, textBounds.Count); Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width)); textBounds = textLine.GetTextBounds(0, 1); @@ -857,7 +857,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting textBounds = textLine.GetTextBounds(0, firstRun.Length + 1); - Assert.Equal(2, textBounds.Count); + Assert.Equal(1, textBounds.Count); Assert.Equal(firstRun.Size.Width + 14, textBounds.Sum(x => x.Rectangle.Width)); textBounds = textLine.GetTextBounds(1, firstRun.Length); @@ -867,7 +867,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting textBounds = textLine.GetTextBounds(0, 1 + firstRun.Length); - Assert.Equal(2, textBounds.Count); + Assert.Equal(1, textBounds.Count); Assert.Equal(firstRun.Size.Width + 14, textBounds.Sum(x => x.Rectangle.Width)); } } @@ -958,6 +958,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(secondRun.Size.Width, textBounds[1].Rectangle.Width); Assert.Equal(7.201171875, textBounds[0].Rectangle.Width); + Assert.Equal(textLine.Start + 7.201171875, textBounds[0].Rectangle.Right); Assert.Equal(textLine.Start + firstRun.Size.Width, textBounds[1].Rectangle.Left); From 84cd896cd80a39668fee46c16285f3c0f102efa4 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Thu, 2 Feb 2023 17:49:14 +0100 Subject: [PATCH 10/29] fix: Waring CS0618 ListItemAutomationPeer --- .../Automation/Peers/ListItemAutomationPeer.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs index 85f139a6a3..aea91b5e26 100644 --- a/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs +++ b/src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs @@ -1,5 +1,4 @@ -using System; -using Avalonia.Automation.Provider; +using Avalonia.Automation.Provider; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Controls.Selection; @@ -64,7 +63,7 @@ namespace Avalonia.Automation.Peers if (Owner.Parent is ItemsControl parent && parent.GetValue(ListBox.SelectionProperty) is ISelectionModel selectionModel) { - var index = parent.ItemContainerGenerator.IndexFromContainer(Owner); + var index = parent.IndexFromContainer(Owner); if (index != -1) selectionModel.Deselect(index); From 5ff8dc97fd8949fdb43dcb2a9dfbfa8163a5c28f Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Sat, 21 Jan 2023 11:42:52 +0100 Subject: [PATCH 11/29] fix: Linux Framebuffer nullable --- .../Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs | 2 +- src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs index 2dcce12df9..c3e90f5fd7 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs @@ -154,7 +154,7 @@ public static class LinuxFramebufferPlatformExtensions var lifetime = LinuxFramebufferPlatform.Initialize(builder, outputBackend, inputBackend); builder.SetupWithLifetime(lifetime); lifetime.Start(args); - builder.Instance.Run(lifetime.Token); + builder.Instance!.Run(lifetime.Token); return lifetime.ExitCode; } } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs index d61dcd4f91..0135cb3d1f 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs @@ -43,13 +43,13 @@ namespace Avalonia.LinuxFramebuffer.Output public IPlatformGraphics PlatformGraphics { get; private set; } public DrmOutput(DrmCard card, DrmResources resources, DrmConnector connector, DrmModeInfo modeInfo, - DrmOutputOptions? options = null) + DrmOutputOptions options = null) { if(options != null) _outputOptions = options; Init(card, resources, connector, modeInfo); } - public DrmOutput(string path = null, bool connectorsForceProbe = false, DrmOutputOptions? options = null) + public DrmOutput(string path = null, bool connectorsForceProbe = false, DrmOutputOptions options = null) { if(options != null) _outputOptions = options; @@ -63,7 +63,7 @@ namespace Avalonia.LinuxFramebuffer.Output if(connector == null) throw new InvalidOperationException("Unable to find connected DRM connector"); - DrmModeInfo? mode = null; + DrmModeInfo mode = null; if (options?.VideoMode != null) { From 07adf1d6dc82bf776b2adef730797fd585662def Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 3 Feb 2023 09:12:18 +0100 Subject: [PATCH 12/29] Try a different font --- .../Assets/NotoSansHebrew-Regular.ttf | Bin 0 -> 42284 bytes .../Avalonia.Skia.RenderTests.csproj | 2 +- .../Avalonia.Skia.UnitTests.csproj | 2 +- .../Media/CustomFontManagerImpl.cs | 4 +++- 4 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 tests/Avalonia.RenderTests/Assets/NotoSansHebrew-Regular.ttf diff --git a/tests/Avalonia.RenderTests/Assets/NotoSansHebrew-Regular.ttf b/tests/Avalonia.RenderTests/Assets/NotoSansHebrew-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..703cfa472d19b1ee2b371da8b88e69caf2ba9c26 GIT binary patch literal 42284 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 literal 0 HcmV?d00001 diff --git a/tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj b/tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj index ba45bbbc2e..0d182678ef 100644 --- a/tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj +++ b/tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj @@ -8,7 +8,7 @@ - + diff --git a/tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj b/tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj index ea91b8c196..86a680fac5 100644 --- a/tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj +++ b/tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj @@ -9,7 +9,7 @@ - + diff --git a/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs index a748f6cf00..24ecb21d18 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs @@ -17,6 +17,8 @@ namespace Avalonia.Skia.UnitTests.Media new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono"); private readonly Typeface _arabicTypeface = new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Sans Arabic"); + private readonly Typeface _hebrewTypeface = + new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Sans Hebrew"); private readonly Typeface _italicTypeface = new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Sans", FontStyle.Italic); private readonly Typeface _emojiTypeface = @@ -24,7 +26,7 @@ namespace Avalonia.Skia.UnitTests.Media public CustomFontManagerImpl() { - _customTypefaces = new[] { _emojiTypeface, _italicTypeface, _arabicTypeface, _defaultTypeface }; + _customTypefaces = new[] { _emojiTypeface, _italicTypeface, _arabicTypeface, _hebrewTypeface, _defaultTypeface }; _defaultFamilyName = _defaultTypeface.FontFamily.FamilyNames.PrimaryFamilyName; } From 42f8c58aef5930cb5ec3def93c981c39beab5143 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 3 Feb 2023 10:45:53 +0100 Subject: [PATCH 13/29] Update teste for the new typeface --- .../Media/TextFormatting/TextCharacters.cs | 21 +++++-------------- .../Media/CustomFontManagerImpl.cs | 6 ++++++ .../Media/TextFormatting/TextLayoutTests.cs | 14 ++++++------- .../Media/TextFormatting/TextLineTests.cs | 14 ++++++------- 4 files changed, 25 insertions(+), 30 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs index 82cf3297fd..c9dafaced7 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs @@ -82,24 +82,15 @@ namespace Avalonia.Media.TextFormatting var previousGlyphTypeface = previousProperties?.CachedGlyphTypeface; var textSpan = text.Span; - if (TryGetShapeableLength(textSpan, defaultGlyphTypeface, null, out var count, out var script)) + if (TryGetShapeableLength(textSpan, defaultGlyphTypeface, null, out var count)) { - if (script == Script.Common && previousGlyphTypeface is not null) - { - if (TryGetShapeableLength(textSpan, previousGlyphTypeface, null, out var fallbackCount, out _)) - { - return new UnshapedTextRun(text.Slice(0, fallbackCount), - defaultProperties.WithTypeface(previousTypeface!.Value), biDiLevel); - } - } - return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(defaultTypeface), biDiLevel); } if (previousGlyphTypeface is not null) { - if (TryGetShapeableLength(textSpan, previousGlyphTypeface, defaultGlyphTypeface, out count, out _)) + if (TryGetShapeableLength(textSpan, previousGlyphTypeface, defaultGlyphTypeface, out count)) { return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(previousTypeface!.Value), biDiLevel); @@ -130,7 +121,7 @@ namespace Avalonia.Media.TextFormatting var fallbackGlyphTypeface = fontManager.GetOrAddGlyphTypeface(fallbackTypeface); - if (matchFound && TryGetShapeableLength(textSpan, fallbackGlyphTypeface, defaultGlyphTypeface, out count, out _)) + if (matchFound && TryGetShapeableLength(textSpan, fallbackGlyphTypeface, defaultGlyphTypeface, out count)) { //Fallback found return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(fallbackTypeface), @@ -160,17 +151,15 @@ namespace Avalonia.Media.TextFormatting /// The typeface that is used to find matching characters. /// The default typeface. /// The shapeable length. - /// /// internal static bool TryGetShapeableLength( ReadOnlySpan text, IGlyphTypeface glyphTypeface, IGlyphTypeface? defaultGlyphTypeface, - out int length, - out Script script) + out int length) { length = 0; - script = Script.Unknown; + var script = Script.Unknown; if (text.IsEmpty) { diff --git a/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs index 24ecb21d18..5a6d7f2cdf 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs @@ -90,6 +90,12 @@ namespace Avalonia.Skia.UnitTests.Media skTypeface = typefaceCollection.Get(typeface); break; } + case "Noto Sans Hebrew": + { + var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_hebrewTypeface.FontFamily); + skTypeface = typefaceCollection.Get(typeface); + break; + } case FontFamily.DefaultFontFamilyName: case "Noto Mono": { diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index 3735e9f6d7..9a7460c218 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -724,7 +724,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var selectedRect = rects[0]; - Assert.Equal(selectedText.Bounds.Width, selectedRect.Width); + Assert.Equal(selectedText.Bounds.Width, selectedRect.Width, 2); } } @@ -885,7 +885,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var distance = hitRange.First().Left; - Assert.Equal(currentX, distance); + Assert.Equal(currentX, distance, 2); currentX += advance; } @@ -915,7 +915,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var distance = hitRange.First().Left + 0.5; - Assert.Equal(currentX, distance); + Assert.Equal(currentX, distance, 2); currentX += advance; } @@ -1048,8 +1048,8 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting [InlineData("012🧐210", 2, 4, FlowDirection.LeftToRight, "14.40234375,40.8046875")] [InlineData("210🧐012", 2, 4, FlowDirection.RightToLeft, "0,7.201171875;21.603515625,33.603515625;48.005859375,55.20703125")] - [InlineData("שנב🧐שנב", 2, 4, FlowDirection.LeftToRight, "11.63671875,39.779296875")] - [InlineData("שנב🧐שנב", 2, 4, FlowDirection.RightToLeft, "11.63671875,39.779296875")] + [InlineData("שנב🧐שנב", 2, 4, FlowDirection.LeftToRight, "11.268,38.208")] + [InlineData("שנב🧐שנב", 2, 4, FlowDirection.RightToLeft, "11.268,38.208")] [Theory] public void Should_HitTextTextRangeBetweenRuns(string text, int start, int length, FlowDirection flowDirection, string expected) @@ -1080,9 +1080,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { var expectedRect = expectedRects[i]; - Assert.Equal(expectedRect.Left, rects[i].Left); + Assert.Equal(expectedRect.Left, rects[i].Left, 2); - Assert.Equal(expectedRect.Right, rects[i].Right); + Assert.Equal(expectedRect.Right, rects[i].Right, 2); } } } diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index e47542af7a..70e74cdf83 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -658,7 +658,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(TextTestHelper.GetStartCharIndex(run.Text), bounds.TextSourceCharacterIndex); Assert.Equal(run, bounds.TextRun); - Assert.Equal(run.Size.Width, bounds.Rectangle.Width); + Assert.Equal(run.Size.Width, bounds.Rectangle.Width, 2); } for (var i = 0; i < textBounds.Count; i++) @@ -667,19 +667,19 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting if (lastBounds != null) { - Assert.Equal(lastBounds.Rectangle.Right, currentBounds.Rectangle.Left); + Assert.Equal(lastBounds.Rectangle.Right, currentBounds.Rectangle.Left, 2); } var sumOfRunWidth = currentBounds.TextRunBounds.Sum(x => x.Rectangle.Width); - Assert.Equal(sumOfRunWidth, currentBounds.Rectangle.Width); + Assert.Equal(sumOfRunWidth, currentBounds.Rectangle.Width, 2); lastBounds = currentBounds; } var sumOfBoundsWidth = textBounds.Sum(x => x.Rectangle.Width); - Assert.Equal(lineWidth, sumOfBoundsWidth); + Assert.Equal(lineWidth, sumOfBoundsWidth, 2); } } @@ -959,14 +959,14 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(secondRun.Size.Width, textBounds[1].Rectangle.Width); Assert.Equal(7.201171875, textBounds[0].Rectangle.Width); - Assert.Equal(textLine.Start + 7.201171875, textBounds[0].Rectangle.Right); - Assert.Equal(textLine.Start + firstRun.Size.Width, textBounds[1].Rectangle.Left); + Assert.Equal(textLine.Start + 7.201171875, textBounds[0].Rectangle.Right, 2); + Assert.Equal(textLine.Start + firstRun.Size.Width, textBounds[1].Rectangle.Left, 2); textBounds = textLine.GetTextBounds(0, text.Length); Assert.Equal(2, textBounds.Count); Assert.Equal(7, textBounds.Sum(x => x.TextRunBounds.Sum(x => x.Length))); - Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width)); + Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width), 2); } } From 73cede0bf5f36c72ae918e758858b5b815c11bc3 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 3 Feb 2023 13:24:12 +0100 Subject: [PATCH 14/29] Added integration tests for window transparency. Failing on macOS, passing on win32. --- samples/IntegrationTestApp/MainWindow.axaml | 54 ++++++----- .../IntegrationTestApp/MainWindow.axaml.cs | 91 +++++++++++++++++++ .../Avalonia.IntegrationTests.Appium.csproj | 1 + .../WindowTests.cs | 45 ++++++++- 4 files changed, 166 insertions(+), 25 deletions(-) diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml index 54c0cb0655..b116e4c789 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml +++ b/samples/IntegrationTestApp/MainWindow.axaml @@ -120,30 +120,36 @@ - - - - NonOwned - Owned - Modal - - - Manual - CenterScreen - CenterOwner - - - Normal - Minimized - Maximized - FullScreen - - - - - - - + + + + + NonOwned + Owned + Modal + + + Manual + CenterScreen + CenterOwner + + + Normal + Minimized + Maximized + FullScreen + + + + + + + + + + + + diff --git a/samples/IntegrationTestApp/MainWindow.axaml.cs b/samples/IntegrationTestApp/MainWindow.axaml.cs index 841947673a..3cd5350cce 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml.cs +++ b/samples/IntegrationTestApp/MainWindow.axaml.cs @@ -7,9 +7,13 @@ using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.Media; using Avalonia.Markup.Xaml; using Avalonia.VisualTree; using Microsoft.CodeAnalysis; +using Avalonia.Controls.Primitives; +using Avalonia.Threading; +using Avalonia.Controls.Primitives.PopupPositioning; namespace IntegrationTestApp { @@ -103,6 +107,89 @@ namespace IntegrationTestApp } } + private void ShowTransparentWindow() + { + // Show a background window to make sure the color behind the transparent window is + // a known color (green). + var backgroundWindow = new Window + { + Title = "Transparent Window Background", + Name = "TransparentWindowBackground", + Width = 300, + Height = 300, + Background = Brushes.Green, + WindowStartupLocation = WindowStartupLocation.CenterOwner, + }; + + // This is the transparent window with a red circle. + var window = new Window + { + Title = "Transparent Window", + Name = "TransparentWindow", + SystemDecorations = SystemDecorations.None, + Background = Brushes.Transparent, + TransparencyLevelHint = WindowTransparencyLevel.Transparent, + WindowStartupLocation = WindowStartupLocation.CenterOwner, + Width = 200, + Height = 200, + Content = new Border + { + Background = Brushes.Red, + CornerRadius = new CornerRadius(100), + } + }; + + window.PointerPressed += (_, _) => + { + window.Close(); + backgroundWindow.Close(); + }; + + backgroundWindow.Show(this); + window.Show(backgroundWindow); + } + + private void ShowTransparentPopup() + { + var popup = new Popup + { + WindowManagerAddShadowHint = false, + PlacementMode = PlacementMode.AnchorAndGravity, + PlacementAnchor = PopupAnchor.Top, + PlacementGravity = PopupGravity.Bottom, + Width= 200, + Height= 200, + Child = new Border + { + Background = Brushes.Red, + CornerRadius = new CornerRadius(100), + } + }; + + // Show a background window to make sure the color behind the transparent window is + // a known color (green). + var backgroundWindow = new Window + { + Title = "Transparent Popup Background", + Name = "TransparentPopupBackground", + Width = 200, + Height = 200, + Background = Brushes.Green, + WindowStartupLocation = WindowStartupLocation.CenterOwner, + Content = new Border + { + Name = "PopupContainer", + Child = popup, + [AutomationProperties.AccessibilityViewProperty] = AccessibilityView.Content, + } + }; + + backgroundWindow.PointerPressed += (_, _) => backgroundWindow.Close(); + backgroundWindow.Show(this); + + popup.Open(); + } + private void SendToBack() { var lifetime = (ClassicDesktopStyleApplicationLifetime)Application.Current!.ApplicationLifetime!; @@ -175,6 +262,10 @@ namespace IntegrationTestApp this.Get("BasicListBox").SelectedIndex = -1; if (source?.Name == "MenuClickedMenuItemReset") this.Get("ClickedMenuItem").Text = "None"; + if (source?.Name == "ShowTransparentWindow") + ShowTransparentWindow(); + if (source?.Name == "ShowTransparentPopup") + ShowTransparentPopup(); if (source?.Name == "ShowWindow") ShowWindow(); if (source?.Name == "SendToBack") diff --git a/tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj b/tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj index 57338a1e08..3ff91139f1 100644 --- a/tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj +++ b/tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj @@ -16,4 +16,5 @@ + diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs index 4d833cdb1f..3b74ed314b 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs @@ -1,11 +1,14 @@ using System; +using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Threading; using Avalonia.Controls; +using Avalonia.Media.Imaging; using OpenQA.Selenium; using OpenQA.Selenium.Appium; using OpenQA.Selenium.Interactions; +using SixLabors.ImageSharp.PixelFormats; using Xunit; using Xunit.Sdk; @@ -141,7 +144,6 @@ namespace Avalonia.IntegrationTests.Appium } } - [Theory] [InlineData(ShowWindowMode.NonOwned)] [InlineData(ShowWindowMode.Owned)] @@ -187,6 +189,47 @@ namespace Avalonia.IntegrationTests.Appium } } + [Fact] + public void TransparentWindow() + { + var showTransparentWindow = _session.FindElementByAccessibilityId("ShowTransparentWindow"); + showTransparentWindow.Click(); + Thread.Sleep(1000); + + var window = _session.FindElementByAccessibilityId("TransparentWindow"); + var screenshot = window.GetScreenshot(); + + window.Click(); + + var img = SixLabors.ImageSharp.Image.Load(screenshot.AsByteArray); + var topLeftColor = img[1, 1]; + var centerColor = img[img.Width / 2, img.Height / 2]; + + Assert.Equal(new Rgba32(0, 128, 0), topLeftColor); + Assert.Equal(new Rgba32(255, 0, 0), centerColor); + } + + [Fact] + public void TransparentPopup() + { + var showTransparentWindow = _session.FindElementByAccessibilityId("ShowTransparentPopup"); + showTransparentWindow.Click(); + Thread.Sleep(1000); + + var window = _session.FindElementByAccessibilityId("TransparentPopupBackground"); + var container = window.FindElementByAccessibilityId("PopupContainer"); + var screenshot = container.GetScreenshot(); + + window.Click(); + + var img = SixLabors.ImageSharp.Image.Load(screenshot.AsByteArray); + var topLeftColor = img[1, 1]; + var centerColor = img[img.Width / 2, img.Height / 2]; + + Assert.Equal(new Rgba32(0, 128, 0), topLeftColor); + Assert.Equal(new Rgba32(255, 0, 0), centerColor); + } + public static TheoryData StartupLocationData() { var sizes = new Size?[] { null, new Size(400, 300) }; From 93396ea6d870aa7eb58e525aaf9b4f127d04507f Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Fri, 3 Feb 2023 15:20:51 +0100 Subject: [PATCH 15/29] perf: Animatable small adjust - delegate caching - try to partially avoid changing the Transitions property during subscribe/unsubscribe operations --- src/Avalonia.Base/Animation/Animatable.cs | 32 ++++++++++++++--------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/Avalonia.Base/Animation/Animatable.cs b/src/Avalonia.Base/Animation/Animatable.cs index eddb89c3e8..50d9a48e9e 100644 --- a/src/Avalonia.Base/Animation/Animatable.cs +++ b/src/Avalonia.Base/Animation/Animatable.cs @@ -29,6 +29,12 @@ namespace Avalonia.Animation private bool _transitionsEnabled = true; private bool _isSubscribedToTransitionsCollection = false; private Dictionary? _transitionState; + readonly NotifyCollectionChangedEventHandler _collectionChanged; + + public Animatable() + { + _collectionChanged = TransitionsCollectionChanged; + } /// /// Gets or sets the clock which controls the animations on the control. @@ -61,14 +67,14 @@ namespace Avalonia.Animation { _transitionsEnabled = true; - if (Transitions is object) + if (Transitions is Transitions transitions) { if (!_isSubscribedToTransitionsCollection) { _isSubscribedToTransitionsCollection = true; - Transitions.CollectionChanged += TransitionsCollectionChanged; + transitions.CollectionChanged += _collectionChanged; } - AddTransitions(Transitions); + AddTransitions(transitions); } } } @@ -86,14 +92,14 @@ namespace Avalonia.Animation { _transitionsEnabled = false; - if (Transitions is object) + if (Transitions is Transitions transitions) { if (_isSubscribedToTransitionsCollection) { _isSubscribedToTransitionsCollection = false; - Transitions.CollectionChanged -= TransitionsCollectionChanged; + transitions.CollectionChanged -= _collectionChanged; } - RemoveTransitions(Transitions); + RemoveTransitions(transitions); } } } @@ -120,7 +126,7 @@ namespace Avalonia.Animation toAdd = newTransitions.Except(oldTransitions).ToList(); } - newTransitions.CollectionChanged += TransitionsCollectionChanged; + newTransitions.CollectionChanged += _collectionChanged; _isSubscribedToTransitionsCollection = true; AddTransitions(toAdd); } @@ -134,19 +140,19 @@ namespace Avalonia.Animation toRemove = oldTransitions.Except(newTransitions).ToList(); } - oldTransitions.CollectionChanged -= TransitionsCollectionChanged; + oldTransitions.CollectionChanged -= _collectionChanged; RemoveTransitions(toRemove); } } else if (_transitionsEnabled && - Transitions is object && + Transitions is Transitions transitions && _transitionState is object && !change.Property.IsDirect && change.Priority > BindingPriority.Animation) { - for (var i = Transitions.Count -1; i >= 0; --i) + for (var i = transitions.Count - 1; i >= 0; --i) { - var transition = Transitions[i]; + var transition = transitions[i]; if (transition.Property == change.Property && _transitionState.TryGetValue(transition, out var state)) @@ -166,11 +172,11 @@ namespace Avalonia.Animation { oldValue = animatedValue; } - + var clock = Clock ?? AvaloniaLocator.Current.GetRequiredService(); state.Instance?.Dispose(); state.Instance = transition.Apply( this, - Clock ?? AvaloniaLocator.Current.GetRequiredService(), + clock, oldValue, newValue); return; From eb1aa547a88a25813ec6dfa68cb336dff9d973b2 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 3 Feb 2023 15:22:57 +0100 Subject: [PATCH 16/29] Fix default transparency level. The macOS backend sets up a window with a transparency level of `None` but the managed code thinks it's `Transparent`, meaning that making the window transparent did nothing as it thinks it's already set. Fixes #8419 --- src/Avalonia.Native/WindowImplBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index 1f290acd86..50bee0d395 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -501,7 +501,7 @@ namespace Avalonia.Native } } - public WindowTransparencyLevel TransparencyLevel { get; private set; } = WindowTransparencyLevel.Transparent; + public WindowTransparencyLevel TransparencyLevel { get; private set; } = WindowTransparencyLevel.None; public void SetFrameThemeVariant(PlatformThemeVariant themeVariant) { From b1ca75a30b9cda3f91dbd9af4c2d89246d52b66f Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Fri, 3 Feb 2023 16:13:12 +0100 Subject: [PATCH 17/29] fix: Address review --- src/Avalonia.Base/Animation/Animatable.cs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Base/Animation/Animatable.cs b/src/Avalonia.Base/Animation/Animatable.cs index 50d9a48e9e..6a05dfbe6d 100644 --- a/src/Avalonia.Base/Animation/Animatable.cs +++ b/src/Avalonia.Base/Animation/Animatable.cs @@ -29,12 +29,9 @@ namespace Avalonia.Animation private bool _transitionsEnabled = true; private bool _isSubscribedToTransitionsCollection = false; private Dictionary? _transitionState; - readonly NotifyCollectionChangedEventHandler _collectionChanged; - - public Animatable() - { - _collectionChanged = TransitionsCollectionChanged; - } + private NotifyCollectionChangedEventHandler _collectionChanged; + private NotifyCollectionChangedEventHandler TransitionsCollectionChangedHandler => + _collectionChanged ??= TransitionsCollectionChanged; /// /// Gets or sets the clock which controls the animations on the control. @@ -72,7 +69,7 @@ namespace Avalonia.Animation if (!_isSubscribedToTransitionsCollection) { _isSubscribedToTransitionsCollection = true; - transitions.CollectionChanged += _collectionChanged; + transitions.CollectionChanged += TransitionsCollectionChangedHandler; } AddTransitions(transitions); } @@ -97,7 +94,7 @@ namespace Avalonia.Animation if (_isSubscribedToTransitionsCollection) { _isSubscribedToTransitionsCollection = false; - transitions.CollectionChanged -= _collectionChanged; + transitions.CollectionChanged -= TransitionsCollectionChangedHandler; } RemoveTransitions(transitions); } @@ -126,7 +123,7 @@ namespace Avalonia.Animation toAdd = newTransitions.Except(oldTransitions).ToList(); } - newTransitions.CollectionChanged += _collectionChanged; + newTransitions.CollectionChanged += TransitionsCollectionChangedHandler; _isSubscribedToTransitionsCollection = true; AddTransitions(toAdd); } @@ -140,7 +137,7 @@ namespace Avalonia.Animation toRemove = oldTransitions.Except(newTransitions).ToList(); } - oldTransitions.CollectionChanged -= _collectionChanged; + oldTransitions.CollectionChanged -= TransitionsCollectionChangedHandler; RemoveTransitions(toRemove); } } From 41966d314fe8c0e7d8e82b513e285adfb3379c48 Mon Sep 17 00:00:00 2001 From: workgroupengineering Date: Fri, 3 Feb 2023 16:40:53 +0100 Subject: [PATCH 18/29] fix: Builld --- src/Avalonia.Base/Animation/Animatable.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Animation/Animatable.cs b/src/Avalonia.Base/Animation/Animatable.cs index 6a05dfbe6d..5208c8b218 100644 --- a/src/Avalonia.Base/Animation/Animatable.cs +++ b/src/Avalonia.Base/Animation/Animatable.cs @@ -29,7 +29,7 @@ namespace Avalonia.Animation private bool _transitionsEnabled = true; private bool _isSubscribedToTransitionsCollection = false; private Dictionary? _transitionState; - private NotifyCollectionChangedEventHandler _collectionChanged; + private NotifyCollectionChangedEventHandler? _collectionChanged; private NotifyCollectionChangedEventHandler TransitionsCollectionChangedHandler => _collectionChanged ??= TransitionsCollectionChanged; From 2bbaf74d8f05184b4d7873e526dc97935efd6096 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 4 Feb 2023 00:13:25 +0100 Subject: [PATCH 19/29] Fix integration tests on macOS. Seems that the version of macOS on the test runner applies some shading to the top of the window. Check the color of a pixel lower down. --- tests/Avalonia.IntegrationTests.Appium/WindowTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs index 3b74ed314b..7bb991aae6 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs @@ -202,7 +202,7 @@ namespace Avalonia.IntegrationTests.Appium window.Click(); var img = SixLabors.ImageSharp.Image.Load(screenshot.AsByteArray); - var topLeftColor = img[1, 1]; + var topLeftColor = img[10, 10]; var centerColor = img[img.Width / 2, img.Height / 2]; Assert.Equal(new Rgba32(0, 128, 0), topLeftColor); @@ -223,7 +223,7 @@ namespace Avalonia.IntegrationTests.Appium window.Click(); var img = SixLabors.ImageSharp.Image.Load(screenshot.AsByteArray); - var topLeftColor = img[1, 1]; + var topLeftColor = img[10, 10]; var centerColor = img[img.Width / 2, img.Height / 2]; Assert.Equal(new Rgba32(0, 128, 0), topLeftColor); From db78f7c8701c94f13794d6e9e5f7c872f89f4387 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 3 Feb 2023 21:18:55 -0500 Subject: [PATCH 20/29] Update angle to 2.1.0.2023020321 --- samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj | 1 - samples/MobileSandbox.Desktop/MobileSandbox.Desktop.csproj | 1 - src/Windows/Avalonia.Win32/Avalonia.Win32.csproj | 2 +- .../Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj | 2 +- 4 files changed, 2 insertions(+), 4 deletions(-) diff --git a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj index e4c83dca49..e465e9caf3 100644 --- a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj +++ b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj @@ -31,7 +31,6 @@ - diff --git a/samples/MobileSandbox.Desktop/MobileSandbox.Desktop.csproj b/samples/MobileSandbox.Desktop/MobileSandbox.Desktop.csproj index 1b83a3e567..a24e55de81 100644 --- a/samples/MobileSandbox.Desktop/MobileSandbox.Desktop.csproj +++ b/samples/MobileSandbox.Desktop/MobileSandbox.Desktop.csproj @@ -24,7 +24,6 @@ - diff --git a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj index b7dca78845..a24fe31df8 100644 --- a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj +++ b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj b/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj index 0c0fe5b921..f3af312d1a 100644 --- a/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj +++ b/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj @@ -23,7 +23,7 @@ - + From 757f82c385c7f23b790b3d20e428c55b044c1317 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Sat, 4 Feb 2023 17:44:14 +0000 Subject: [PATCH 21/29] Updated browser packages to stable. --- .../ControlCatalog.Browser.Blazor.csproj | 4 ++-- .../Avalonia.Browser.Blazor/Avalonia.Browser.Blazor.csproj | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/samples/ControlCatalog.Browser.Blazor/ControlCatalog.Browser.Blazor.csproj b/samples/ControlCatalog.Browser.Blazor/ControlCatalog.Browser.Blazor.csproj index d0fb614840..733a4b7194 100644 --- a/samples/ControlCatalog.Browser.Blazor/ControlCatalog.Browser.Blazor.csproj +++ b/samples/ControlCatalog.Browser.Blazor/ControlCatalog.Browser.Blazor.csproj @@ -9,8 +9,8 @@ - - + + diff --git a/src/Browser/Avalonia.Browser.Blazor/Avalonia.Browser.Blazor.csproj b/src/Browser/Avalonia.Browser.Blazor/Avalonia.Browser.Blazor.csproj index a9cad0538f..9017ce1546 100644 --- a/src/Browser/Avalonia.Browser.Blazor/Avalonia.Browser.Blazor.csproj +++ b/src/Browser/Avalonia.Browser.Blazor/Avalonia.Browser.Blazor.csproj @@ -15,7 +15,7 @@ - + From d0fe9e0e14f3dbe72ece505ddb2d2ea88606d371 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Sat, 4 Feb 2023 18:06:42 +0000 Subject: [PATCH 22/29] Updated Skia and HarfBuzz to latest stable. --- build/HarfBuzzSharp.props | 6 +++--- build/SkiaSharp.props | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/build/HarfBuzzSharp.props b/build/HarfBuzzSharp.props index 620ec58ff3..75d317be1a 100644 --- a/build/HarfBuzzSharp.props +++ b/build/HarfBuzzSharp.props @@ -1,7 +1,7 @@  - - - + + + diff --git a/build/SkiaSharp.props b/build/SkiaSharp.props index 31619399f9..f45addaa2a 100644 --- a/build/SkiaSharp.props +++ b/build/SkiaSharp.props @@ -1,7 +1,7 @@  - - - + + + From 9fdf98dde6e4d466e9ff72bf9635f8fdad1455f6 Mon Sep 17 00:00:00 2001 From: amwx <40413319+amwx@users.noreply.github.com> Date: Sat, 4 Feb 2023 14:18:35 -0500 Subject: [PATCH 23/29] Only use font fallback if match found --- .../Media/TextFormatting/TextCharacters.cs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs index c9dafaced7..8480be3882 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs @@ -118,14 +118,18 @@ namespace Avalonia.Media.TextFormatting fontManager.TryMatchCharacter(codepoint, defaultTypeface.Style, defaultTypeface.Weight, defaultTypeface.Stretch, defaultTypeface.FontFamily, defaultProperties.CultureInfo, out var fallbackTypeface); - - var fallbackGlyphTypeface = fontManager.GetOrAddGlyphTypeface(fallbackTypeface); - - if (matchFound && TryGetShapeableLength(textSpan, fallbackGlyphTypeface, defaultGlyphTypeface, out count)) + + if (matchFound) { + var fallbackGlyphTypeface = fontManager.GetOrAddGlyphTypeface(fallbackTypeface); + //Fallback found - return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(fallbackTypeface), - biDiLevel); + if (TryGetShapeableLength(textSpan, fallbackGlyphTypeface, defaultGlyphTypeface, out count)) + { + + return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(fallbackTypeface), + biDiLevel); + } } // no fallback found From 1cfdae9448d9eac12bba5ac02ac859ab7e459134 Mon Sep 17 00:00:00 2001 From: amwx <40413319+amwx@users.noreply.github.com> Date: Sat, 4 Feb 2023 14:23:03 -0500 Subject: [PATCH 24/29] Cleanup --- src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs index 8480be3882..b4734d702b 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs @@ -121,12 +121,11 @@ namespace Avalonia.Media.TextFormatting if (matchFound) { + // Fallback found var fallbackGlyphTypeface = fontManager.GetOrAddGlyphTypeface(fallbackTypeface); - - //Fallback found + if (TryGetShapeableLength(textSpan, fallbackGlyphTypeface, defaultGlyphTypeface, out count)) - { - + { return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(fallbackTypeface), biDiLevel); } From fe2233d25a53e654ff82705cba3069319538743c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Sat, 4 Feb 2023 22:03:50 +0100 Subject: [PATCH 25/29] Update ItemsPresenter.cs --- src/Avalonia.Controls/Presenters/ItemsPresenter.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index 8594b584fa..e8eaac7d17 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -28,19 +28,19 @@ namespace Avalonia.Controls.Presenters /// Defines the property. /// public static readonly StyledProperty AreHorizontalSnapPointsRegularProperty = - AvaloniaProperty.Register(nameof(AreHorizontalSnapPointsRegular)); + AvaloniaProperty.Register(nameof(AreHorizontalSnapPointsRegular)); /// /// Defines the property. /// public static readonly StyledProperty AreVerticalSnapPointsRegularProperty = - AvaloniaProperty.Register(nameof(AreVerticalSnapPointsRegular)); + AvaloniaProperty.Register(nameof(AreVerticalSnapPointsRegular)); /// /// Defines the event. /// public static readonly RoutedEvent HorizontalSnapPointsChangedEvent = - RoutedEvent.Register( + RoutedEvent.Register( nameof(HorizontalSnapPointsChanged), RoutingStrategies.Bubble); @@ -48,7 +48,7 @@ namespace Avalonia.Controls.Presenters /// Defines the event. /// public static readonly RoutedEvent VerticalSnapPointsChangedEvent = - RoutedEvent.Register( + RoutedEvent.Register( nameof(VerticalSnapPointsChanged), RoutingStrategies.Bubble); @@ -139,7 +139,7 @@ namespace Avalonia.Controls.Presenters Size IScrollable.Viewport => _logicalScrollable?.Viewport ?? default; /// - /// Gets or sets whether the horizontal snap points for the are equidistant from each other. + /// Gets or sets whether the horizontal snap points for the are equidistant from each other. /// public bool AreHorizontalSnapPointsRegular { @@ -148,7 +148,7 @@ namespace Avalonia.Controls.Presenters } /// - /// Gets or sets whether the vertical snap points for the are equidistant from each other. + /// Gets or sets whether the vertical snap points for the are equidistant from each other. /// public bool AreVerticalSnapPointsRegular { From 465d72f2903572c66a444f9f2b31392b7ad6cd69 Mon Sep 17 00:00:00 2001 From: Andrew Arnott Date: Sat, 4 Feb 2023 16:46:13 -0700 Subject: [PATCH 26/29] Add nullable ref annotation to Windows.Close --- src/Avalonia.Controls/Window.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index a20b4eee58..b1110ece55 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -450,7 +450,7 @@ namespace Avalonia.Controls /// resulting task will produce the value when the window /// is closed. /// - public void Close(object dialogResult) + public void Close(object? dialogResult) { _dialogResult = dialogResult; CloseCore(WindowCloseReason.WindowClosing, true); From 8c07476a1a61ff0d70af12672697b301958394b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Sun, 5 Feb 2023 09:01:44 +0000 Subject: [PATCH 27/29] Fixed NuGet package authors. --- build/SharedVersion.props | 1 + 1 file changed, 1 insertion(+) diff --git a/build/SharedVersion.props b/build/SharedVersion.props index eca3ba37b0..2849262591 100644 --- a/build/SharedVersion.props +++ b/build/SharedVersion.props @@ -3,6 +3,7 @@ Avalonia 11.0.999 + Avalonia Team Copyright 2022 © The AvaloniaUI Project https://avaloniaui.net https://github.com/AvaloniaUI/Avalonia/ From cc850694cdf17aaca4fc48571090620371fe4e1f Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Sun, 5 Feb 2023 12:00:38 +0100 Subject: [PATCH 28/29] editorconfig: don't apply s_ prefix to public fields --- .editorconfig | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/.editorconfig b/.editorconfig index e6ae266849..a144ec8843 100644 --- a/.editorconfig +++ b/.editorconfig @@ -55,16 +55,17 @@ dotnet_naming_symbols.constant_fields.required_modifiers = const dotnet_naming_style.pascal_case_style.capitalization = pascal_case -# static fields should have s_ prefix -dotnet_naming_rule.static_fields_should_have_prefix.severity = suggestion -dotnet_naming_rule.static_fields_should_have_prefix.symbols = static_fields -dotnet_naming_rule.static_fields_should_have_prefix.style = static_prefix_style +# private static fields should have s_ prefix +dotnet_naming_rule.private_static_fields_should_have_prefix.severity = suggestion +dotnet_naming_rule.private_static_fields_should_have_prefix.symbols = private_static_fields +dotnet_naming_rule.private_static_fields_should_have_prefix.style = private_static_prefix_style -dotnet_naming_symbols.static_fields.applicable_kinds = field -dotnet_naming_symbols.static_fields.required_modifiers = static +dotnet_naming_symbols.private_static_fields.applicable_kinds = field +dotnet_naming_symbols.private_static_fields.required_modifiers = static +dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private -dotnet_naming_style.static_prefix_style.required_prefix = s_ -dotnet_naming_style.static_prefix_style.capitalization = camel_case +dotnet_naming_style.private_static_prefix_style.required_prefix = s_ +dotnet_naming_style.private_static_prefix_style.capitalization = camel_case # internal and private fields should be _camelCase dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion @@ -117,7 +118,7 @@ csharp_space_after_dot = false csharp_space_after_keywords_in_control_flow_statements = true csharp_space_after_semicolon_in_for_statement = true csharp_space_around_binary_operators = before_and_after -csharp_space_around_declaration_statements = do_not_ignore +csharp_space_around_declaration_statements = false csharp_space_before_colon_in_inheritance_clause = true csharp_space_before_comma = false csharp_space_before_dot = false @@ -211,5 +212,5 @@ indent_size = 2 # Shell scripts [*.sh] end_of_line = lf -[*.{cmd, bat}] +[*.{cmd,bat}] end_of_line = crlf From 1642129d0f7477ce0b02c38d8482f253525f2d41 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 6 Feb 2023 04:07:02 -0500 Subject: [PATCH 29/29] Implement simple PreloadDepsAssemblies --- .../DesignXamlLoader.cs | 71 +++++++++++++++++-- 1 file changed, 67 insertions(+), 4 deletions(-) diff --git a/src/tools/Avalonia.Designer.HostApp/DesignXamlLoader.cs b/src/tools/Avalonia.Designer.HostApp/DesignXamlLoader.cs index 181883656c..690926a193 100644 --- a/src/tools/Avalonia.Designer.HostApp/DesignXamlLoader.cs +++ b/src/tools/Avalonia.Designer.HostApp/DesignXamlLoader.cs @@ -1,16 +1,79 @@ using System; +using System.Collections.Generic; using System.IO; +using System.Linq; using System.Reflection; +using System.Text.RegularExpressions; using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml.XamlIl; -namespace Avalonia.Designer.HostApp +namespace Avalonia.Designer.HostApp; + +class DesignXamlLoader : AvaloniaXamlLoader.IRuntimeXamlLoader { - class DesignXamlLoader : AvaloniaXamlLoader.IRuntimeXamlLoader + public object Load(RuntimeXamlLoaderDocument document, RuntimeXamlLoaderConfiguration configuration) + { + PreloadDepsAssemblies(configuration.LocalAssembly ?? Assembly.GetEntryAssembly()); + + return AvaloniaXamlIlRuntimeCompiler.Load(document, configuration); + } + + private void PreloadDepsAssemblies(Assembly targetAssembly) { - public object Load(RuntimeXamlLoaderDocument document, RuntimeXamlLoaderConfiguration configuration) + // Assemblies loaded in memory (e.g. single file) return empty string from Location. + // In these cases, don't try probing next to the assembly. + var assemblyLocation = targetAssembly.Location; + if (string.IsNullOrEmpty(assemblyLocation)) + { + return; + } + + var depsJsonFile = Path.ChangeExtension(assemblyLocation, ".deps.json"); + if (!File.Exists(depsJsonFile)) + { + return; + } + + using var stream = File.OpenRead(depsJsonFile); + + /* + We can't use any references in the Avalonia.Designer.HostApp. Including even json. + Ideally we would prefer Microsoft.Extensions.DependencyModel package, but can't use it here. + So, instead we need to fallback to some JSON parsing using pretty easy regex. + + Json part example: +"Avalonia.Xaml.Interactions/11.0.0-preview5": { + "dependencies": { + "Avalonia": "11.0.999", + "Avalonia.Xaml.Interactivity": "11.0.0-preview5" + }, + "runtime": { + "lib/net6.0/Avalonia.Xaml.Interactions.dll": { + "assemblyVersion": "11.0.0.0", + "fileVersion": "11.0.0.0" + } + } +}, + We want to extract "lib/net6.0/Avalonia.Xaml.Interactions.dll" from here. + No need to resolve real path of ref assemblies. + No need to handle special cases with .NET Framework and GAC. + */ + var text = new StreamReader(stream).ReadToEnd(); + var matches = Regex.Matches( text, """runtime"\s*:\s*{\s*"([^"]+)"""); + + foreach (Match match in matches) { - return AvaloniaXamlIlRuntimeCompiler.Load(document, configuration); + if (match.Groups[1] is { Success: true } g) + { + var assemblyName = Path.GetFileNameWithoutExtension(g.Value); + try + { + _ = Assembly.Load(new AssemblyName(assemblyName)); + } + catch + { + } + } } } }