diff --git a/Avalonia.Desktop.slnf b/Avalonia.Desktop.slnf index 4a7a329fc6..477aaec6a8 100644 --- a/Avalonia.Desktop.slnf +++ b/Avalonia.Desktop.slnf @@ -21,6 +21,7 @@ "src\\Avalonia.Desktop\\Avalonia.Desktop.csproj", "src\\Avalonia.Diagnostics\\Avalonia.Diagnostics.csproj", "src\\Avalonia.Dialogs\\Avalonia.Dialogs.csproj", + "src\\Avalonia.Fonts.Inter\\Avalonia.Fonts.Inter.csproj", "src\\Avalonia.FreeDesktop\\Avalonia.FreeDesktop.csproj", "src\\Avalonia.Headless.Vnc\\Avalonia.Headless.Vnc.csproj", "src\\Avalonia.Headless\\Avalonia.Headless.csproj", diff --git a/Avalonia.sln b/Avalonia.sln index 56847bae31..e66b73de0e 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -244,6 +244,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepe EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepeater.UnitTests", "tests\Avalonia.Controls.ItemsRepeater.UnitTests\Avalonia.Controls.ItemsRepeater.UnitTests.csproj", "{F4E36AA8-814E-4704-BC07-291F70F45193}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Fonts.Inter", "src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj", "{13F1135D-BA1A-435C-9C5B-A368D1D63DE4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -555,9 +557,14 @@ Global {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Debug|Any CPU.Build.0 = Debug|Any CPU {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Release|Any CPU.ActiveCfg = Release|Any CPU {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Release|Any CPU.Build.0 = Release|Any CPU + {C810060E-3809-4B74-A125-F11533AF9C1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C810060E-3809-4B74-A125-F11533AF9C1B}.Debug|Any CPU.Build.0 = Debug|Any CPU {C810060E-3809-4B74-A125-F11533AF9C1B}.Release|Any CPU.ActiveCfg = Release|Any CPU {C810060E-3809-4B74-A125-F11533AF9C1B}.Release|Any CPU.Build.0 = Release|Any CPU + {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Debug|Any CPU.ActiveCfg = Release|Any CPU + {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Debug|Any CPU.Build.0 = Release|Any CPU + {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Release|Any CPU.Build.0 = Release|Any CPU {EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}.Debug|Any CPU.Build.0 = Debug|Any CPU {EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -566,10 +573,10 @@ Global {F4E36AA8-814E-4704-BC07-291F70F45193}.Debug|Any CPU.Build.0 = Debug|Any CPU {F4E36AA8-814E-4704-BC07-291F70F45193}.Release|Any CPU.ActiveCfg = Release|Any CPU {F4E36AA8-814E-4704-BC07-291F70F45193}.Release|Any CPU.Build.0 = Release|Any CPU - {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Debug|Any CPU.ActiveCfg = Release|Any CPU - {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Debug|Any CPU.Build.0 = Release|Any CPU - {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C692FE73-43DB-49CE-87FC-F03ED61F25C9}.Release|Any CPU.Build.0 = Release|Any CPU + {13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {13F1135D-BA1A-435C-9C5B-A368D1D63DE4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -635,8 +642,8 @@ Global {90B08091-9BBD-4362-B712-E9F2CC62B218} = {9B9E3891-2366-4253-A952-D08BCEB71098} {75C47156-C5D8-44BC-A5A7-E8657C2248D6} = {9B9E3891-2366-4253-A952-D08BCEB71098} {C810060E-3809-4B74-A125-F11533AF9C1B} = {9B9E3891-2366-4253-A952-D08BCEB71098} - {F4E36AA8-814E-4704-BC07-291F70F45193} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {C692FE73-43DB-49CE-87FC-F03ED61F25C9} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} + {F4E36AA8-814E-4704-BC07-291F70F45193} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/azure-pipelines-integrationtests.yml b/azure-pipelines-integrationtests.yml index 5735da19ab..7221fe4657 100644 --- a/azure-pipelines-integrationtests.yml +++ b/azure-pipelines-integrationtests.yml @@ -1,11 +1,3 @@ -# Starter pipeline -# Start with a minimal pipeline that you can customize to build and deploy your code. -# Add steps that build, run tests, deploy, and more: -# https://aka.ms/yaml - -trigger: -- master - jobs: - job: Mac pool: @@ -25,6 +17,7 @@ jobs: - script: system_profiler SPDisplaysDataType |grep Resolution - script: | + sudo xcode-select -s /Applications/Xcode.app/Contents/Developer pkill node appium & pkill IntegrationTestApp diff --git a/packages/Avalonia/Avalonia.props b/packages/Avalonia/Avalonia.props index 6f21971d3d..26908b1081 100644 --- a/packages/Avalonia/Avalonia.props +++ b/packages/Avalonia/Avalonia.props @@ -6,4 +6,9 @@ false + + + + + diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index c223bfe1a9..0e84b3d182 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -29,6 +29,7 @@ + diff --git a/samples/ControlCatalog/Pages/CustomDrawingExampleControl.cs b/samples/ControlCatalog/Pages/CustomDrawingExampleControl.cs index 938c45a4e2..db74be7c08 100644 --- a/samples/ControlCatalog/Pages/CustomDrawingExampleControl.cs +++ b/samples/ControlCatalog/Pages/CustomDrawingExampleControl.cs @@ -16,10 +16,10 @@ namespace ControlCatalog.Pages private Point _cursorPoint; - public StyledProperty ScaleProperty = AvaloniaProperty.Register(nameof(Scale), 1.0d); + public static readonly StyledProperty ScaleProperty = AvaloniaProperty.Register(nameof(Scale), 1.0d); public double Scale { get => GetValue(ScaleProperty); set => SetValue(ScaleProperty, value); } - public StyledProperty RotationProperty = AvaloniaProperty.Register(nameof(Rotation)); + public static readonly StyledProperty RotationProperty = AvaloniaProperty.Register(nameof(Rotation)); /// /// Rotation, measured in Radians! /// @@ -33,10 +33,10 @@ namespace ControlCatalog.Pages } } - public StyledProperty ViewportCenterYProperty = AvaloniaProperty.Register(nameof(ViewportCenterY), 0.0d); + public static readonly StyledProperty ViewportCenterYProperty = AvaloniaProperty.Register(nameof(ViewportCenterY), 0.0d); public double ViewportCenterY { get => GetValue(ViewportCenterYProperty); set => SetValue(ViewportCenterYProperty, value); } - public StyledProperty ViewportCenterXProperty = AvaloniaProperty.Register(nameof(ViewportCenterX), 0.0d); + public static readonly StyledProperty ViewportCenterXProperty = AvaloniaProperty.Register(nameof(ViewportCenterX), 0.0d); public double ViewportCenterX { get => GetValue(ViewportCenterXProperty); set => SetValue(ViewportCenterXProperty, value); } private IPen _pen; diff --git a/samples/ControlCatalog/Pages/PointerCanvas.cs b/samples/ControlCatalog/Pages/PointerCanvas.cs index da1ff5442d..32e46af9dd 100644 --- a/samples/ControlCatalog/Pages/PointerCanvas.cs +++ b/samples/ControlCatalog/Pages/PointerCanvas.cs @@ -93,7 +93,7 @@ public class PointerCanvas : Control } private int _threadSleep; - public static DirectProperty ThreadSleepProperty = + public static readonly DirectProperty ThreadSleepProperty = AvaloniaProperty.RegisterDirect(nameof(ThreadSleep), c => c.ThreadSleep, (c, v) => c.ThreadSleep = v); public int ThreadSleep @@ -103,7 +103,7 @@ public class PointerCanvas : Control } private bool _drawOnlyPoints; - public static DirectProperty DrawOnlyPointsProperty = + public static readonly DirectProperty DrawOnlyPointsProperty = AvaloniaProperty.RegisterDirect(nameof(DrawOnlyPoints), c => c.DrawOnlyPoints, (c, v) => c.DrawOnlyPoints = v); public bool DrawOnlyPoints @@ -113,8 +113,8 @@ public class PointerCanvas : Control } private string? _status; - public static DirectProperty StatusProperty = - AvaloniaProperty.RegisterDirect(nameof(DrawOnlyPoints), c => c.Status, (c, v) => c.Status = v, + public static readonly DirectProperty StatusProperty = + AvaloniaProperty.RegisterDirect(nameof(Status), c => c.Status, (c, v) => c.Status = v, defaultBindingMode: Avalonia.Data.BindingMode.TwoWay); public string? Status diff --git a/samples/IntegrationTestApp/IntegrationTestApp.csproj b/samples/IntegrationTestApp/IntegrationTestApp.csproj index 0a761d70ba..1356eeb526 100644 --- a/samples/IntegrationTestApp/IntegrationTestApp.csproj +++ b/samples/IntegrationTestApp/IntegrationTestApp.csproj @@ -19,6 +19,7 @@ + diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml index 58e8aef1e9..090cf23b33 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml +++ b/samples/IntegrationTestApp/MainWindow.axaml @@ -25,6 +25,7 @@ WindowState: + diff --git a/samples/IntegrationTestApp/MainWindow.axaml.cs b/samples/IntegrationTestApp/MainWindow.axaml.cs index 087f25666b..19eb1d64b0 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml.cs +++ b/samples/IntegrationTestApp/MainWindow.axaml.cs @@ -1,19 +1,17 @@ -using System; using System.Collections.Generic; using System.Linq; using Avalonia; using Avalonia.Automation; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Input; using Avalonia.Interactivity; -using Avalonia.Media; using Avalonia.Markup.Xaml; +using Avalonia.Media; using Avalonia.VisualTree; using Microsoft.CodeAnalysis; -using Avalonia.Controls.Primitives; -using Avalonia.Threading; -using Avalonia.Controls.Primitives.PopupPositioning; namespace IntegrationTestApp { @@ -25,6 +23,10 @@ namespace IntegrationTestApp InitializeViewMenu(); InitializeGesturesTab(); this.AttachDevTools(); + + var overlayPopups = this.Get("AppOverlayPopups"); + overlayPopups.Text = Program.OverlayPopups ? "Overlay Popups" : "Native Popups"; + AddHandler(Button.ClickEvent, OnButtonClick); ListBoxItems = Enumerable.Range(0, 100).Select(x => "Item " + x).ToList(); DataContext = this; diff --git a/samples/IntegrationTestApp/Program.cs b/samples/IntegrationTestApp/Program.cs index c09b249cfa..6603450b85 100644 --- a/samples/IntegrationTestApp/Program.cs +++ b/samples/IntegrationTestApp/Program.cs @@ -1,17 +1,31 @@ using System; +using System.Linq; using Avalonia; -using Avalonia.Controls; -using Avalonia.Controls.ApplicationLifetimes; namespace IntegrationTestApp { class Program { + public static bool OverlayPopups { get; private set; } + // Initialization code. Don't use any Avalonia, third-party APIs or any // SynchronizationContext-reliant code before AppMain is called: things aren't initialized // yet and stuff might break. - public static void Main(string[] args) => BuildAvaloniaApp() - .StartWithClassicDesktopLifetime(args); + public static void Main(string[] args) + { + OverlayPopups = args.Contains("--overlayPopups"); + + BuildAvaloniaApp() + .With(new Win32PlatformOptions + { + OverlayPopups = OverlayPopups, + }) + .With(new AvaloniaNativePlatformOptions + { + OverlayPopups = OverlayPopups, + }) + .StartWithClassicDesktopLifetime(args); + } // Avalonia configuration, don't remove; also used by visual designer. public static AppBuilder BuildAvaloniaApp() diff --git a/src/Avalonia.Base/Input/AccessKeyHandler.cs b/src/Avalonia.Base/Input/AccessKeyHandler.cs index 59c66ed505..13ca140565 100644 --- a/src/Avalonia.Base/Input/AccessKeyHandler.cs +++ b/src/Avalonia.Base/Input/AccessKeyHandler.cs @@ -1,9 +1,8 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using Avalonia.Interactivity; -using Avalonia.VisualTree; +using Avalonia.LogicalTree; namespace Avalonia.Input { @@ -190,7 +189,7 @@ namespace Avalonia.Input // If the menu is open, only match controls in the menu's visual tree. if (menuIsOpen) { - matches = matches.Where(x => x is not null && ((Visual)MainMenu!).IsVisualAncestorOf((Visual)x)); + matches = matches.Where(x => x is not null && ((Visual)MainMenu!).IsLogicalAncestorOf((Visual)x)); } var match = matches.FirstOrDefault(); diff --git a/src/Avalonia.Base/Media/DrawingContext.cs b/src/Avalonia.Base/Media/DrawingContext.cs index 31a16dc69c..f2106f2f86 100644 --- a/src/Avalonia.Base/Media/DrawingContext.cs +++ b/src/Avalonia.Base/Media/DrawingContext.cs @@ -277,7 +277,6 @@ namespace Avalonia.Media private readonly record struct RestoreState : IDisposable { private readonly DrawingContext _context; - private readonly Matrix _matrix; private readonly PushedStateType _type; public enum PushedStateType diff --git a/src/Avalonia.Base/Media/DrawingGroup.cs b/src/Avalonia.Base/Media/DrawingGroup.cs index 812d315912..a41054202e 100644 --- a/src/Avalonia.Base/Media/DrawingGroup.cs +++ b/src/Avalonia.Base/Media/DrawingGroup.cs @@ -107,8 +107,6 @@ namespace Avalonia.Media private readonly DrawingGroup _drawingGroup; private readonly IPlatformRenderInterface _platformRenderInterface = AvaloniaLocator.Current.GetRequiredService(); - private Matrix _transform; - private bool _disposed; // Root drawing created by this DrawingContext. diff --git a/src/Avalonia.Base/Media/GeometryDrawing.cs b/src/Avalonia.Base/Media/GeometryDrawing.cs index ac2dce1e42..abfd2e33ac 100644 --- a/src/Avalonia.Base/Media/GeometryDrawing.cs +++ b/src/Avalonia.Base/Media/GeometryDrawing.cs @@ -27,8 +27,8 @@ namespace Avalonia.Media /// /// Defines the property. /// - public static readonly StyledProperty PenProperty = - AvaloniaProperty.Register(nameof(Pen)); + public static readonly StyledProperty PenProperty = + AvaloniaProperty.Register(nameof(Pen)); /// /// Gets or sets the that describes the shape of this . diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index 2966ceee8d..d795cca894 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -151,9 +151,9 @@ namespace Avalonia.Media } /// - /// Gets or sets the conservative bounding box of the . + /// Gets the conservative bounding box of the . /// - public Size Size => PlatformImpl.Item.Size; + public Rect Bounds => PlatformImpl.Item.Bounds; /// /// @@ -252,7 +252,7 @@ namespace Avalonia.Media if (characterIndex > Metrics.LastCluster) { - return Size.Width; + return Bounds.Width; } var glyphIndex = FindGlyphIndex(characterIndex); @@ -287,7 +287,7 @@ namespace Avalonia.Media if (characterIndex <= Metrics.FirstCluster) { - return Size.Width; + return Bounds.Width; } for (var i = glyphIndex + 1; i < _glyphInfos.Count; i++) @@ -295,7 +295,7 @@ namespace Avalonia.Media distance += _glyphInfos[i].GlyphAdvance; } - return Size.Width - distance; + return Bounds.Width - distance; } } @@ -321,7 +321,7 @@ namespace Avalonia.Media } //After - if (distance >= Size.Width) + if (distance >= Bounds.Width) { isInside = false; @@ -354,7 +354,7 @@ namespace Avalonia.Media } else { - currentX = Size.Width; + currentX = Bounds.Width; for (var index = _glyphInfos.Count - 1; index >= 0; index--) { diff --git a/src/Avalonia.Base/Media/GlyphRunDrawing.cs b/src/Avalonia.Base/Media/GlyphRunDrawing.cs index 06d92fd81c..961203e30e 100644 --- a/src/Avalonia.Base/Media/GlyphRunDrawing.cs +++ b/src/Avalonia.Base/Media/GlyphRunDrawing.cs @@ -32,7 +32,7 @@ public override Rect GetBounds() { - return GlyphRun != null ? new Rect(GlyphRun.Size) : default; + return GlyphRun != null ? GlyphRun.Bounds : default; } } } diff --git a/src/Avalonia.Base/Media/PolyLineSegment.cs b/src/Avalonia.Base/Media/PolyLineSegment.cs index 55bfb33041..5c48c11e19 100644 --- a/src/Avalonia.Base/Media/PolyLineSegment.cs +++ b/src/Avalonia.Base/Media/PolyLineSegment.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using Avalonia.Collections; namespace Avalonia.Media { @@ -20,7 +19,7 @@ namespace Avalonia.Media /// /// The points. /// - public AvaloniaList Points + public Points Points { get => GetValue(PointsProperty); set => SetValue(PointsProperty, value); diff --git a/src/Avalonia.Base/Media/TextDecoration.cs b/src/Avalonia.Base/Media/TextDecoration.cs index b74b7df9c5..e89a7d8826 100644 --- a/src/Avalonia.Base/Media/TextDecoration.cs +++ b/src/Avalonia.Base/Media/TextDecoration.cs @@ -223,7 +223,7 @@ namespace Avalonia.Media if (intersections.Count > 0) { var last = baselineOrigin.X; - var finalPos = last + glyphRun.Size.Width; + var finalPos = last + glyphRun.Bounds.Width; var end = last; var points = new List(); diff --git a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs index c1b9b77401..75249ff7e7 100644 --- a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs +++ b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs @@ -89,13 +89,13 @@ namespace Avalonia.Media.TextFormatting var offset = Math.Max(0, currentPosition - glyphRun.Metrics.FirstCluster); var glyphIndex = glyphRun.FindGlyphIndex(characterIndex - offset); - var glyphInfo = shapedBuffer.GlyphInfos[glyphIndex]; + var glyphInfo = shapedBuffer[glyphIndex]; - shapedBuffer.GlyphInfos[glyphIndex] = new GlyphInfo(glyphInfo.GlyphIndex, + shapedBuffer[glyphIndex] = new GlyphInfo(glyphInfo.GlyphIndex, glyphInfo.GlyphCluster, glyphInfo.GlyphAdvance + spacing); } - glyphRun.GlyphInfos = shapedBuffer.GlyphInfos; + glyphRun.GlyphInfos = shapedBuffer; } currentPosition += textRun.Length; diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs index f29bdd4459..3f26d081b0 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs @@ -2,6 +2,7 @@ using System.Buffers; using System.Collections; using System.Collections.Generic; +using System.Runtime.CompilerServices; using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting @@ -9,12 +10,13 @@ namespace Avalonia.Media.TextFormatting public sealed class ShapedBuffer : IReadOnlyList, IDisposable { private GlyphInfo[]? _rentedBuffer; + private ArraySlice _glyphInfos; public ShapedBuffer(ReadOnlyMemory text, int bufferLength, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) { - _rentedBuffer = ArrayPool.Shared.Rent(bufferLength); Text = text; - GlyphInfos = new ArraySlice(_rentedBuffer, 0, bufferLength); + _rentedBuffer = ArrayPool.Shared.Rent(bufferLength); + _glyphInfos = new ArraySlice(_rentedBuffer, 0, bufferLength); GlyphTypeface = glyphTypeface; FontRenderingEmSize = fontRenderingEmSize; BidiLevel = bidiLevel; @@ -23,27 +25,70 @@ namespace Avalonia.Media.TextFormatting internal ShapedBuffer(ReadOnlyMemory text, ArraySlice glyphInfos, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) { Text = text; - GlyphInfos = glyphInfos; + _glyphInfos = glyphInfos; GlyphTypeface = glyphTypeface; FontRenderingEmSize = fontRenderingEmSize; BidiLevel = bidiLevel; } - internal ArraySlice GlyphInfos { get; private set; } - - public int Length - => GlyphInfos.Length; + /// + /// The buffer's length. + /// + public int Length => _glyphInfos.Length; + /// + /// The buffer's glyph typeface. + /// public IGlyphTypeface GlyphTypeface { get; } + /// + /// The buffers font rendering em size. + /// public double FontRenderingEmSize { get; } + /// + /// The buffer's bidi level. + /// public sbyte BidiLevel { get; } + /// + /// The buffer's reading direction. + /// public bool IsLeftToRight => (BidiLevel & 1) == 0; + /// + /// The text that is represended by this buffer. + /// public ReadOnlyMemory Text { get; } + /// + /// Reverses the buffer. + /// + public void Reverse() + { + _glyphInfos.Span.Reverse(); + } + + public void Dispose() + { + if (_rentedBuffer is not null) + { + ArrayPool.Shared.Return(_rentedBuffer); + _rentedBuffer = null; + _glyphInfos = ArraySlice.Empty; // ensure we don't misuse the returned array + } + } + + public GlyphInfo this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _glyphInfos[index]; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + set => _glyphInfos[index] = value; + } + + public IEnumerator GetEnumerator() => _glyphInfos.GetEnumerator(); + /// /// Finds a glyph index for given character index. /// @@ -53,20 +98,19 @@ namespace Avalonia.Media.TextFormatting /// private int FindGlyphIndex(int characterIndex) { - if (characterIndex < GlyphInfos[0].GlyphCluster) + if (characterIndex < _glyphInfos[0].GlyphCluster) { return 0; } - if (characterIndex > GlyphInfos[GlyphInfos.Length - 1].GlyphCluster) + if (characterIndex > _glyphInfos[_glyphInfos.Length - 1].GlyphCluster) { - return GlyphInfos.Length - 1; + return _glyphInfos.Length - 1; } - var comparer = GlyphInfo.ClusterAscendingComparer; - var glyphInfos = GlyphInfos.Span; + var glyphInfos = _glyphInfos.Span; var searchValue = new GlyphInfo(default, characterIndex, default); @@ -109,42 +153,24 @@ namespace Avalonia.Media.TextFormatting return new SplitResult(this, null); } - var firstCluster = GlyphInfos[0].GlyphCluster; - var lastCluster = GlyphInfos[GlyphInfos.Length - 1].GlyphCluster; + var firstCluster = _glyphInfos[0].GlyphCluster; + var lastCluster = _glyphInfos[_glyphInfos.Length - 1].GlyphCluster; var start = firstCluster < lastCluster ? firstCluster : lastCluster; var glyphCount = FindGlyphIndex(start + length); var first = new ShapedBuffer(Text.Slice(0, length), - GlyphInfos.Take(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel); + _glyphInfos.Take(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel); var second = new ShapedBuffer(Text.Slice(length), - GlyphInfos.Skip(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel); + _glyphInfos.Skip(glyphCount), GlyphTypeface, FontRenderingEmSize, BidiLevel); return new SplitResult(first, second); } - int IReadOnlyCollection.Count => GlyphInfos.Length; - - public GlyphInfo this[int index] - { - get => GlyphInfos[index]; - set => GlyphInfos[index] = value; - } - - public IEnumerator GetEnumerator() => GlyphInfos.GetEnumerator(); + int IReadOnlyCollection.Count => _glyphInfos.Length; - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - public void Dispose() - { - if (_rentedBuffer is not null) - { - ArrayPool.Shared.Return(_rentedBuffer); - _rentedBuffer = null; - GlyphInfos = ArraySlice.Empty; // ensure we don't misuse the returned array - } - } + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } } diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs index 568148e15c..2f28c3f954 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs @@ -38,14 +38,14 @@ namespace Avalonia.Media.TextFormatting public override double Baseline => -TextMetrics.Ascent; - public override Size Size => GlyphRun.Size; + public override Size Size => GlyphRun.Bounds.Size; public GlyphRun GlyphRun => _glyphRun ??= CreateGlyphRun(); /// public override void Draw(DrawingContext drawingContext, Point origin) { - using (drawingContext.PushPreTransform(Matrix.CreateTranslation(origin))) + using (drawingContext.PushTransform(Matrix.CreateTranslation(origin))) { if (GlyphRun.GlyphInfos.Count == 0) { @@ -85,7 +85,7 @@ namespace Avalonia.Media.TextFormatting { _glyphRun = null; - ShapedBuffer.GlyphInfos.Span.Reverse(); + ShapedBuffer.Reverse(); IsReversed = !IsReversed; } @@ -106,7 +106,7 @@ namespace Avalonia.Media.TextFormatting for (var i = 0; i < ShapedBuffer.Length; i++) { - var advance = ShapedBuffer.GlyphInfos[i].GlyphAdvance; + var advance = ShapedBuffer[i].GlyphAdvance; if (currentWidth + advance > availableWidth) { @@ -130,7 +130,7 @@ namespace Avalonia.Media.TextFormatting for (var i = ShapedBuffer.Length - 1; i >= 0; i--) { - var advance = ShapedBuffer.GlyphInfos[i].GlyphAdvance; + var advance = ShapedBuffer[i].GlyphAdvance; if (width + advance > availableWidth) { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs index 6422f23dcd..8b6d576c6e 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs @@ -19,7 +19,7 @@ namespace Avalonia.Media.TextFormatting var collapsedLength = 0; var shapedSymbol = TextFormatterImpl.CreateSymbol(properties.Symbol, FlowDirection.LeftToRight); - if (properties.Width < shapedSymbol.GlyphRun.Size.Width) + if (properties.Width < shapedSymbol.GlyphRun.Bounds.Width) { //Not enough space to fit in the symbol return Array.Empty(); diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index 7f74f49982..12efb3c383 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -583,7 +583,7 @@ namespace Avalonia.Media.TextFormatting { if (shapedTextCharacters.ShapedBuffer.Length > 0) { - var firstCluster = shapedTextCharacters.ShapedBuffer.GlyphInfos[0].GlyphCluster; + var firstCluster = shapedTextCharacters.ShapedBuffer[0].GlyphCluster; var lastCluster = firstCluster; for (var j = 0; j < shapedTextCharacters.ShapedBuffer.Length; j++) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs index a21a5d45e9..41d451b9e3 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs @@ -60,7 +60,7 @@ namespace Avalonia.Media.TextFormatting var currentWidth = 0.0; var shapedSymbol = TextFormatterImpl.CreateSymbol(Symbol, FlowDirection.LeftToRight); - if (Width < shapedSymbol.GlyphRun.Size.Width) + if (Width < shapedSymbol.GlyphRun.Bounds.Width) { return Array.Empty(); } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index b3321d4d9f..f426a20b2c 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -423,7 +423,7 @@ namespace Avalonia.Media.TextFormatting { if (currentGlyphRun != null) { - currentDistance -= currentGlyphRun.Size.Width; + currentDistance -= currentGlyphRun.Bounds.Width; } return currentDistance + distance; @@ -477,7 +477,7 @@ namespace Avalonia.Media.TextFormatting { if (currentGlyphRun.IsLeftToRight || flowDirection == FlowDirection.RightToLeft) { - distance = currentGlyphRun.Size.Width; + distance = currentGlyphRun.Bounds.Width; } return true; @@ -1483,7 +1483,7 @@ namespace Avalonia.Media.TextFormatting trailingWhitespaceLength += glyphRunMetrics.TrailingWhitespaceLength; - var whitespaceWidth = glyphRun.Size.Width - glyphRunMetrics.Width; + var whitespaceWidth = glyphRun.Bounds.Width - glyphRunMetrics.Width; width -= whitespaceWidth; } diff --git a/src/Avalonia.Base/Platform/AssetLoader.cs b/src/Avalonia.Base/Platform/AssetLoader.cs index 659cfb75df..7df446e854 100644 --- a/src/Avalonia.Base/Platform/AssetLoader.cs +++ b/src/Avalonia.Base/Platform/AssetLoader.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection; @@ -62,7 +63,7 @@ namespace Avalonia.Platform /// True if the asset could be found; otherwise false. public bool Exists(Uri uri, Uri? baseUri = null) { - return GetAsset(uri, baseUri) != null; + return TryGetAsset(uri, baseUri, out _); } /// @@ -94,21 +95,27 @@ namespace Avalonia.Platform /// public (Stream stream, Assembly assembly) OpenAndGetAssembly(Uri uri, Uri? baseUri = null) { - var asset = GetAsset(uri, baseUri); - - if (asset == null) + if (TryGetAsset(uri, baseUri, out var assetDescriptor)) { - throw new FileNotFoundException($"The resource {uri} could not be found."); + return (assetDescriptor.GetStream(), assetDescriptor.Assembly); } - return (asset.GetStream(), asset.Assembly); + throw new FileNotFoundException($"The resource {uri} could not be found."); } public Assembly? GetAssembly(Uri uri, Uri? baseUri) { if (!uri.IsAbsoluteUri && baseUri != null) + { uri = new Uri(baseUri, uri); - return GetAssembly(uri)?.Assembly; + } + + if (TryGetAssembly(uri, out var assemblyDescriptor)) + { + return assemblyDescriptor.Assembly; + } + + return null; } /// @@ -121,99 +128,145 @@ namespace Avalonia.Platform { if (uri.IsAbsoluteResm()) { - var assembly = GetAssembly(uri); + if (!TryGetAssembly(uri, out var assembly)) + { + assembly = _defaultResmAssembly; + } return assembly?.Resources? - .Where(x => x.Key.IndexOf(uri.GetUnescapeAbsolutePath(), StringComparison.Ordinal) >= 0) - .Select(x =>new Uri($"resm:{x.Key}?assembly={assembly.Name}")) ?? - Enumerable.Empty(); + .Where(x => x.Key.Contains(uri.GetUnescapeAbsolutePath())) + .Select(x => new Uri($"resm:{x.Key}?assembly={assembly.Name}")) ?? + Enumerable.Empty(); } uri = uri.EnsureAbsolute(baseUri); + if (uri.IsAvares()) { - var (asm, path) = GetResAsmAndPath(uri); - if (asm == null) + if (!TryGetResAsmAndPath(uri, out var assembly, out var path)) { - throw new ArgumentException( - "No default assembly, entry assembly or explicit assembly specified; " + - "don't know where to look up for the resource, try specifying assembly explicitly."); + return Enumerable.Empty(); } - if (asm.AvaloniaResources == null) + if (assembly?.AvaloniaResources == null) + { return Enumerable.Empty(); + } - if (path[path.Length - 1] != '/') + if (path.Length > 0 && path[path.Length - 1] != '/') + { path += '/'; + } - return asm.AvaloniaResources + return assembly.AvaloniaResources .Where(r => r.Key.StartsWith(path, StringComparison.Ordinal)) - .Select(x => new Uri($"avares://{asm.Name}{x.Key}")); + .Select(x => new Uri($"avares://{assembly.Name}{x.Key}")); } return Enumerable.Empty(); } - - private IAssetDescriptor? GetAsset(Uri uri, Uri? baseUri) - { + + private bool TryGetAsset(Uri uri, Uri? baseUri, [NotNullWhen(true)] out IAssetDescriptor? assetDescriptor) + { + assetDescriptor = null; + if (uri.IsAbsoluteResm()) { - var asm = GetAssembly(uri) ?? GetAssembly(baseUri) ?? _defaultResmAssembly; - - if (asm == null) + if (!TryGetAssembly(uri, out var assembly) && !TryGetAssembly(baseUri, out assembly)) { - throw new ArgumentException( - "No default assembly, entry assembly or explicit assembly specified; " + - "don't know where to look up for the resource, try specifying assembly explicitly."); + assembly = _defaultResmAssembly; } - var resourceKey = uri.AbsolutePath; - IAssetDescriptor? rv = null; - asm.Resources?.TryGetValue(resourceKey, out rv); - return rv; + if (assembly?.Resources != null) + { + var resourceKey = uri.AbsolutePath; + + if (assembly.Resources.TryGetValue(resourceKey, out assetDescriptor)) + { + return true; + } + } } uri = uri.EnsureAbsolute(baseUri); if (uri.IsAvares()) { - var (asm, path) = GetResAsmAndPath(uri); - if (asm.AvaloniaResources == null) - return null; - asm.AvaloniaResources.TryGetValue(path, out var desc); - return desc; + if (TryGetResAsmAndPath(uri, out var assembly, out var path)) + { + if (assembly.AvaloniaResources == null) + { + return false; + } + + if (assembly.AvaloniaResources.TryGetValue(path, out assetDescriptor)) + { + return true; + } + } } - throw new ArgumentException($"Unsupported url type: " + uri.Scheme, nameof(uri)); + return false; } - private static (IAssemblyDescriptor asm, string path) GetResAsmAndPath(Uri uri) + private static bool TryGetResAsmAndPath(Uri uri, [NotNullWhen(true)] out IAssemblyDescriptor? assembly, out string path) { - var asm = s_assemblyDescriptorResolver.GetAssembly(uri.Authority); - return (asm, uri.GetUnescapeAbsolutePath()); + path = uri.GetUnescapeAbsolutePath(); + + if (TryLoadAssembly(uri.Authority, out assembly)) + { + return true; + } + + return false; } - - private static IAssemblyDescriptor? GetAssembly(Uri? uri) + + private static bool TryGetAssembly(Uri? uri, [NotNullWhen(true)] out IAssemblyDescriptor? assembly) { + assembly = null; + if (uri != null) { if (!uri.IsAbsoluteUri) - return null; - if (uri.IsAvares()) - return GetResAsmAndPath(uri).asm; + { + return false; + } + + if (uri.IsAvares() && TryGetResAsmAndPath(uri, out assembly, out _)) + { + return true; + } if (uri.IsResm()) { var assemblyName = uri.GetAssemblyNameFromQuery(); - if (assemblyName.Length > 0) - return s_assemblyDescriptorResolver.GetAssembly(assemblyName); + + if (assemblyName.Length > 0 && TryLoadAssembly(assemblyName, out assembly)) + { + return true; + } } } - return null; + return false; + } + + private static bool TryLoadAssembly(string assemblyName, [NotNullWhen(true)] out IAssemblyDescriptor? assembly) + { + assembly = null; + + try + { + assembly = s_assemblyDescriptorResolver.GetAssembly(assemblyName); + + return true; + } + catch (Exception) { } + + return false; } #endif - + public static void RegisterResUriParsers() { if (!UriParser.IsKnownScheme("avares")) diff --git a/src/Avalonia.Base/Platform/IGlyphRunImpl.cs b/src/Avalonia.Base/Platform/IGlyphRunImpl.cs index 46b065b04e..fccea27c43 100644 --- a/src/Avalonia.Base/Platform/IGlyphRunImpl.cs +++ b/src/Avalonia.Base/Platform/IGlyphRunImpl.cs @@ -10,11 +10,10 @@ namespace Avalonia.Platform [Unstable] public interface IGlyphRunImpl : IDisposable { - /// /// Gets the conservative bounding box of the glyph run./>. /// - Size Size { get; } + Rect Bounds { get; } /// /// Gets the baseline origin of the glyph run./>. diff --git a/src/Avalonia.Base/PropertyStore/ValueStore.cs b/src/Avalonia.Base/PropertyStore/ValueStore.cs index 0887f11ec9..53cd3ff307 100644 --- a/src/Avalonia.Base/PropertyStore/ValueStore.cs +++ b/src/Avalonia.Base/PropertyStore/ValueStore.cs @@ -924,7 +924,7 @@ namespace Avalonia.PropertyStore { _effectiveValues.GetKeyValue(i, out var key, out var e); - if (e.Priority == BindingPriority.Unset) + if (e.Priority == BindingPriority.Unset && !e.IsOverridenCurrentValue) { RemoveEffectiveValue(key, i); e.DisposeAndRaiseUnset(this, key); diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index 01299e4ffa..df3a70b3e6 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -225,7 +225,7 @@ public class CompositingRenderer : IRendererWithCompositor sortedChildren.Dispose(); } else - foreach (var ch in v.GetVisualChildren()) + foreach (var ch in visualChildren) { var compositionChild = ch.CompositionVisual; if (compositionChild != null) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs b/src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs index b01fb46aa3..04e40e8744 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs @@ -20,7 +20,7 @@ namespace Avalonia.Rendering.Composition.Server for (var c = FirstChar; c <= LastChar; c++) { - var height = _runs[c - FirstChar].Size.Height; + var height = _runs[c - FirstChar].Bounds.Height; if (height > maxHeight) { maxHeight = height; @@ -51,8 +51,8 @@ namespace Avalonia.Rendering.Composition.Server { var effectiveChar = c is >= FirstChar and <= LastChar ? c : ' '; var run = _runs[effectiveChar - FirstChar]; - width += run.Size.Width; - height = Math.Max(height, run.Size.Height); + width += run.Bounds.Width; + height = Math.Max(height, run.Bounds.Height); } return new Size(width, height); @@ -69,7 +69,7 @@ namespace Avalonia.Rendering.Composition.Server var run = _runs[effectiveChar - FirstChar]; context.Transform = originalTransform * Matrix.CreateTranslation(offset, 0.0); context.DrawGlyphRun(foreground, run.PlatformImpl); - offset += run.Size.Width; + offset += run.Bounds.Width; } context.Transform = originalTransform; diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs index 4d8759f545..381c63f430 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs @@ -16,12 +16,11 @@ namespace Avalonia.Rendering.SceneGraph /// The transform. /// The foreground brush. /// The glyph run to draw. - /// Auxiliary data required to draw the brush. public GlyphRunNode( Matrix transform, IImmutableBrush foreground, IRef glyphRun) - : base(new Rect(glyphRun.Item.Size), transform, foreground) + : base(glyphRun.Item.Bounds, transform, foreground) { GlyphRun = glyphRun.Clone(); } @@ -54,7 +53,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public override bool HitTest(Point p) => new Rect(GlyphRun.Item.Size).ContainsExclusive(p); + public override bool HitTest(Point p) => GlyphRun.Item.Bounds.ContainsExclusive(p); public override void Dispose() { diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs index 82e948eea8..cbdf3c3c1e 100644 --- a/src/Avalonia.Base/StyledElement.cs +++ b/src/Avalonia.Base/StyledElement.cs @@ -803,8 +803,11 @@ namespace Avalonia if (theme.HasChildren) { - foreach (var child in theme.Children) - ApplyStyle(child, null, type); + var children = theme.Children; + for (var i = 0; i < children.Count; i++) + { + ApplyStyle(children[i], null, type); + } } } @@ -816,8 +819,11 @@ namespace Avalonia if (host.IsStylesInitialized) { - foreach (var style in host.Styles) - ApplyStyle(style, host, FrameType.Style); + var styles = host.Styles; + for (var i = 0; i < styles.Count; ++i) + { + ApplyStyle(styles[i], host, FrameType.Style); + } } } @@ -826,8 +832,11 @@ namespace Avalonia if (style is Style s) s.TryAttach(this, host, type); - foreach (var child in style.Children) - ApplyStyle(child, host, type); + var children = style.Children; + for (var i = 0; i < children.Count; i++) + { + ApplyStyle(children[i], host, type); + } } private void ReevaluateImplicitTheme() diff --git a/src/Avalonia.Base/Styling/OrSelector.cs b/src/Avalonia.Base/Styling/OrSelector.cs index c5ef9a0b2b..3b0aa03492 100644 --- a/src/Avalonia.Base/Styling/OrSelector.cs +++ b/src/Avalonia.Base/Styling/OrSelector.cs @@ -71,9 +71,9 @@ namespace Avalonia.Styling var activators = new OrActivatorBuilder(); var neverThisInstance = false; - foreach (var selector in _selectors) + for (var i = 0; i < _selectors.Count; i++) { - var match = selector.Match(control, parent, subscribe); + var match = _selectors[i].Match(control, parent, subscribe); switch (match.Result) { @@ -108,16 +108,19 @@ namespace Avalonia.Styling internal override void ValidateNestingSelector(bool inControlTheme) { - foreach (var selector in _selectors) - selector.ValidateNestingSelector(inControlTheme); + for (var i = 0; i < _selectors.Count; i++) + { + _selectors[i].ValidateNestingSelector(inControlTheme); + } } private Type? EvaluateTargetType() { Type? result = null; - foreach (var selector in _selectors) + for (var i = 0; i < _selectors.Count; i++) { + var selector = _selectors[i]; if (selector.TargetType == null) { return null; diff --git a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs index 7883fd23ab..f8670cfdb3 100644 --- a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs +++ b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using Avalonia.Controls; using Avalonia.Styling.Activators; using Avalonia.Utilities; @@ -15,7 +14,7 @@ namespace Avalonia.Styling internal class TypeNameAndClassSelector : Selector { private readonly Selector? _previous; - private readonly Lazy> _classes = new Lazy>(() => new List()); + private List? _classes; private Type? _targetType; private string? _selectorString; @@ -81,7 +80,7 @@ namespace Avalonia.Styling /// /// The style classes which the selector matches. /// - public IList Classes => _classes.Value; + public IList Classes => _classes ??= new(); /// public override string ToString(Style? owner) @@ -122,16 +121,16 @@ namespace Avalonia.Styling return SelectorMatch.NeverThisInstance; } - if (_classes.IsValueCreated && _classes.Value.Count > 0) + if (_classes is { Count: > 0 }) { if (subscribe) { - var observable = new StyleClassActivator((Classes)control.Classes, _classes.Value); + var observable = new StyleClassActivator(control.Classes, _classes); return new SelectorMatch(observable); } - if (!StyleClassActivator.AreClassesMatching(control.Classes, Classes)) + if (!StyleClassActivator.AreClassesMatching(control.Classes, _classes)) { return SelectorMatch.NeverThisInstance; } @@ -172,9 +171,9 @@ namespace Avalonia.Styling builder.Append(Name); } - if (_classes.IsValueCreated && _classes.Value.Count > 0) + if (_classes is { Count: > 0 }) { - foreach (var c in Classes) + foreach (var c in _classes) { if (!c.StartsWith(":")) { diff --git a/src/Avalonia.Base/Utilities/AvaloniaPropertyDictionary.cs b/src/Avalonia.Base/Utilities/AvaloniaPropertyDictionary.cs index 5cac2ef658..0589abb2dd 100644 --- a/src/Avalonia.Base/Utilities/AvaloniaPropertyDictionary.cs +++ b/src/Avalonia.Base/Utilities/AvaloniaPropertyDictionary.cs @@ -304,13 +304,9 @@ namespace Avalonia.Utilities { if (_entryCount == _entries!.Length) { - const double growthFactor = 1.2; - var newSize = (int)(_entryCount * growthFactor); - - if (newSize == _entryCount) - { - newSize++; - } + var newSize = _entryCount == DefaultInitialCapacity ? + DefaultInitialCapacity * 2 : + (int)(_entryCount * 1.5); var destEntries = new Entry[newSize]; diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index 8b0cc06136..df0c5b100f 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -732,6 +732,23 @@ namespace Avalonia } } + internal override void OnTemplatedParentControlThemeChanged() + { + base.OnTemplatedParentControlThemeChanged(); + + var count = VisualChildren.Count; + var templatedParent = TemplatedParent; + + for (var i = 0; i < count; ++i) + { + if (VisualChildren[i] is StyledElement child && + child.TemplatedParent == templatedParent) + { + child.OnTemplatedParentControlThemeChanged(); + } + } + } + /// /// Computes the value according to the /// and diff --git a/src/Avalonia.Controls.DataGrid/Themes/Simple.xaml b/src/Avalonia.Controls.DataGrid/Themes/Simple.xaml index 76e44e4daf..464a12770e 100644 --- a/src/Avalonia.Controls.DataGrid/Themes/Simple.xaml +++ b/src/Avalonia.Controls.DataGrid/Themes/Simple.xaml @@ -202,7 +202,7 @@ @@ -129,7 +129,7 @@ - + internal class CombinedGeometryImpl : GeometryImpl { - public CombinedGeometryImpl(GeometryCombineMode combineMode, Geometry g1, Geometry g2) + public CombinedGeometryImpl(SKPath? stroke, SKPath? fill) { - var path1 = (g1.PlatformImpl as GeometryImpl)?.EffectivePath; - var path2 = (g2.PlatformImpl as GeometryImpl)?.EffectivePath; + StrokePath = stroke; + FillPath = fill; + Bounds = (stroke ?? fill)?.TightBounds.ToAvaloniaRect() ?? default; + } + + public static CombinedGeometryImpl ForceCreate(GeometryCombineMode combineMode, Geometry g1, Geometry g2) + { + if (g1.PlatformImpl is GeometryImpl i1 + && g2.PlatformImpl is GeometryImpl i2 + && TryCreate(combineMode, i1, i2) is { } result) + return result; + + return new(null, null); + } + public static CombinedGeometryImpl? TryCreate(GeometryCombineMode combineMode, GeometryImpl g1, GeometryImpl g2) + { var op = combineMode switch { GeometryCombineMode.Intersect => SKPathOp.Intersect, @@ -21,13 +35,28 @@ namespace Avalonia.Skia _ => SKPathOp.Union }; - var path = path1?.Op(path2, op); + var stroke = + g1.StrokePath != null && g2.StrokePath != null + ? g1.StrokePath.Op(g2.StrokePath, op) + : null; + + SKPath? fill = null; + if (g1.FillPath != null && g2.FillPath != null) + { + // Reuse stroke if fill paths are the same + if (ReferenceEquals(g1.FillPath, g1.StrokePath) && ReferenceEquals(g2.FillPath, g2.StrokePath)) + fill = stroke; + else + fill = g1.FillPath.Op(g2.FillPath, op); + } - EffectivePath = path; - Bounds = path?.Bounds.ToAvaloniaRect() ?? default; + if (stroke == null && fill == null) + return null; + return new CombinedGeometryImpl(stroke, fill); } public override Rect Bounds { get; } - public override SKPath? EffectivePath { get; } + public override SKPath? StrokePath { get; } + public override SKPath? FillPath { get; } } } diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index db7b068543..e3e2f664c3 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -230,20 +230,21 @@ namespace Avalonia.Skia var impl = (GeometryImpl) geometry; var size = geometry.Bounds.Size; - if (brush is not null) + if (brush is not null && impl.FillPath != null) { using (var fill = CreatePaint(_fillPaint, brush, size)) { - Canvas.DrawPath(impl.EffectivePath, fill.Paint); + Canvas.DrawPath(impl.FillPath, fill.Paint); } } if (pen is not null + && impl.StrokePath != null && TryCreatePaint(_strokePaint, pen, size.Inflate(new Thickness(pen.Thickness / 2))) is { } stroke) { using (stroke) { - Canvas.DrawPath(impl.EffectivePath, stroke.Paint); + Canvas.DrawPath(impl.StrokePath, stroke.Paint); } } } @@ -515,7 +516,7 @@ namespace Avalonia.Skia return; } - using (var paintWrapper = CreatePaint(_fillPaint, foreground, glyphRun.Item.Size)) + using (var paintWrapper = CreatePaint(_fillPaint, foreground, glyphRun.Item.Bounds.Size)) { var glyphRunImpl = (GlyphRunImpl)glyphRun.Item; @@ -639,7 +640,7 @@ namespace Avalonia.Skia { CheckLease(); Canvas.Save(); - Canvas.ClipPath(((GeometryImpl)clip).EffectivePath, SKClipOperation.Intersect, true); + Canvas.ClipPath(((GeometryImpl)clip).FillPath, SKClipOperation.Intersect, true); } /// diff --git a/src/Skia/Avalonia.Skia/EllipseGeometryImpl.cs b/src/Skia/Avalonia.Skia/EllipseGeometryImpl.cs index ac05691c67..64976d4eea 100644 --- a/src/Skia/Avalonia.Skia/EllipseGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/EllipseGeometryImpl.cs @@ -8,14 +8,15 @@ namespace Avalonia.Skia internal class EllipseGeometryImpl : GeometryImpl { public override Rect Bounds { get; } - public override SKPath EffectivePath { get; } + public override SKPath StrokePath { get; } + public override SKPath FillPath => StrokePath; public EllipseGeometryImpl(Rect rect) { var path = new SKPath(); path.AddOval(rect.ToSKRect()); - EffectivePath = path; + StrokePath = path; Bounds = rect; } } diff --git a/src/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs index d53dcd2df3..90a2f9169b 100644 --- a/src/Skia/Avalonia.Skia/FontManagerImpl.cs +++ b/src/Skia/Avalonia.Skia/FontManagerImpl.cs @@ -99,7 +99,20 @@ namespace Avalonia.Skia { SKTypeface? skTypeface = null; - if (typeface.FontFamily.Key is null) + if(typeface.FontFamily.Key is not null) + { + var fontCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(typeface.FontFamily); + + skTypeface = fontCollection.Get(typeface); + + if (skTypeface is null && !typeface.FontFamily.FamilyNames.HasFallbacks) + { + throw new InvalidOperationException( + $"Could not create glyph typeface for: {typeface.FontFamily.Name}."); + } + } + + if (skTypeface is null) { var defaultName = SKTypeface.Default.FamilyName; @@ -128,13 +141,7 @@ namespace Avalonia.Skia skTypeface ??= _skFontManager.MatchTypeface(SKTypeface.Default, fontStyle) ?? SKTypeface.Default; } - else - { - var fontCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(typeface.FontFamily); - - skTypeface = fontCollection.Get(typeface); - } - + if (skTypeface == null) { throw new InvalidOperationException( diff --git a/src/Skia/Avalonia.Skia/GeometryGroupImpl.cs b/src/Skia/Avalonia.Skia/GeometryGroupImpl.cs index 2828f9a9c1..01be42bad0 100644 --- a/src/Skia/Avalonia.Skia/GeometryGroupImpl.cs +++ b/src/Skia/Avalonia.Skia/GeometryGroupImpl.cs @@ -11,26 +11,51 @@ namespace Avalonia.Skia { public GeometryGroupImpl(FillRule fillRule, IReadOnlyList children) { - var path = new SKPath + var fillType = fillRule == FillRule.NonZero ? SKPathFillType.Winding : SKPathFillType.EvenOdd; + var count = children.Count; + + var stroke = new SKPath { - FillType = fillRule == FillRule.NonZero ? SKPathFillType.Winding : SKPathFillType.EvenOdd, + FillType = fillType }; - - var count = children.Count; + bool requiresFillPass = false; for (var i = 0; i < count; ++i) { - if (children[i].PlatformImpl is GeometryImpl { EffectivePath: { } effectivePath }) + if (children[i].PlatformImpl is GeometryImpl geo) { - path.AddPath(effectivePath); + if (geo.StrokePath != null) + stroke.AddPath(geo.StrokePath); + if (!ReferenceEquals(geo.StrokePath, geo.FillPath)) + requiresFillPass = true; } } + + StrokePath = stroke; + + if (requiresFillPass) + { + var fill = new SKPath + { + FillType = fillType + }; + + for (var i = 0; i < count; ++i) + { + if (children[i].PlatformImpl is GeometryImpl { FillPath: { } fillPath }) + fill.AddPath(fillPath); + } + + FillPath = fill; + } + else + FillPath = stroke; - EffectivePath = path; - Bounds = path.Bounds.ToAvaloniaRect(); + Bounds = stroke.TightBounds.ToAvaloniaRect(); } public override Rect Bounds { get; } - public override SKPath EffectivePath { get; } + public override SKPath StrokePath { get; } + public override SKPath FillPath { get; } } } diff --git a/src/Skia/Avalonia.Skia/GeometryImpl.cs b/src/Skia/Avalonia.Skia/GeometryImpl.cs index 34270c2078..aee84d1346 100644 --- a/src/Skia/Avalonia.Skia/GeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/GeometryImpl.cs @@ -14,7 +14,7 @@ namespace Avalonia.Skia private PathCache _pathCache; private SKPathMeasure? _cachedPathMeasure; - private SKPathMeasure CachedPathMeasure => _cachedPathMeasure ??= new SKPathMeasure(EffectivePath!); + private SKPathMeasure CachedPathMeasure => _cachedPathMeasure ??= new SKPathMeasure(StrokePath!); /// public abstract Rect Bounds { get; } @@ -24,19 +24,20 @@ namespace Avalonia.Skia { get { - if (EffectivePath is null) + if (StrokePath is null) return 0; return CachedPathMeasure.Length; } } - public abstract SKPath? EffectivePath { get; } + public abstract SKPath? StrokePath { get; } + public abstract SKPath? FillPath { get; } /// public bool FillContains(Point point) { - return PathContainsCore(EffectivePath, point); + return PathContainsCore(FillPath, point); } /// @@ -74,7 +75,7 @@ namespace Avalonia.Skia var paint = SKPaintCache.Shared.Get(); paint.IsStroke = true; paint.StrokeWidth = strokeWidth; - paint.GetFillPath(EffectivePath, strokePath); + paint.GetFillPath(StrokePath, strokePath); SKPaintCache.Shared.ReturnReset(paint); @@ -96,14 +97,10 @@ namespace Avalonia.Skia /// public IGeometryImpl? Intersect(IGeometryImpl geometry) { - if (EffectivePath is { } path - && (geometry as GeometryImpl)?.EffectivePath is { } otherPath - && path.Op(otherPath, SKPathOp.Intersect) is { } result) - { - return new StreamGeometryImpl(result); - } - - return null; + var other = geometry as GeometryImpl; + if (other == null) + return null; + return CombinedGeometryImpl.TryCreate(GeometryCombineMode.Intersect, this, other); } /// @@ -128,7 +125,7 @@ namespace Avalonia.Skia /// public bool TryGetPointAtDistance(double distance, out Point point) { - if (EffectivePath is null) + if (StrokePath is null) { point = new Point(); return false; @@ -142,7 +139,7 @@ namespace Avalonia.Skia /// public bool TryGetPointAndTangentAtDistance(double distance, out Point point, out Point tangent) { - if (EffectivePath is null) + if (StrokePath is null) { point = new Point(); tangent = new Point(); @@ -158,7 +155,7 @@ namespace Avalonia.Skia public bool TryGetSegment(double startDistance, double stopDistance, bool startOnBeginFigure, [NotNullWhen(true)] out IGeometryImpl? segmentGeometry) { - if (EffectivePath is null) + if (StrokePath is null) { segmentGeometry = null; return false; @@ -172,7 +169,7 @@ namespace Avalonia.Skia if (res) { - segmentGeometry = new StreamGeometryImpl(_skPathSegment); + segmentGeometry = new StreamGeometryImpl(_skPathSegment, null); } return res; diff --git a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs index 079eea7bef..0521e238f3 100644 --- a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs +++ b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs @@ -11,7 +11,7 @@ namespace Avalonia.Skia { TextBlob = textBlob ?? throw new ArgumentNullException(nameof(textBlob)); - Size = size; + Bounds = new Rect(new Point(baselineOrigin.X, 0), size); BaselineOrigin = baselineOrigin; } @@ -21,7 +21,7 @@ namespace Avalonia.Skia /// public SKTextBlob TextBlob { get; } - public Size Size { get; } + public Rect Bounds { get; } public Point BaselineOrigin { get; } diff --git a/src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs b/src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs index ec24f8f624..b774ddf411 100644 --- a/src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs +++ b/src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs @@ -13,7 +13,6 @@ namespace Avalonia.Skia.Helpers /// /// /// - /// /// DrawingContext public static IDrawingContextImpl WrapSkiaCanvas(SKCanvas canvas, Vector dpi) { diff --git a/src/Skia/Avalonia.Skia/LineGeometryImpl.cs b/src/Skia/Avalonia.Skia/LineGeometryImpl.cs index b102a4c119..2ac0a2e18c 100644 --- a/src/Skia/Avalonia.Skia/LineGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/LineGeometryImpl.cs @@ -9,7 +9,8 @@ namespace Avalonia.Skia internal class LineGeometryImpl : GeometryImpl { public override Rect Bounds { get; } - public override SKPath EffectivePath { get; } + public override SKPath StrokePath { get; } + public override SKPath? FillPath => null; public LineGeometryImpl(Point p1, Point p2) { @@ -17,7 +18,7 @@ namespace Avalonia.Skia path.MoveTo(p1.ToSKPoint()); path.LineTo(p2.ToSKPoint()); - EffectivePath = path; + StrokePath = path; Bounds = new Rect( new Point(Math.Min(p1.X, p2.X), Math.Min(p1.Y, p2.Y)), new Point(Math.Max(p1.X, p2.X), Math.Max(p1.Y, p2.Y))); diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index ab1c6b8816..9c4b326f14 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -65,7 +65,7 @@ namespace Avalonia.Skia public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) { - return new CombinedGeometryImpl(combineMode, g1, g2); + return CombinedGeometryImpl.ForceCreate(combineMode, g1, g2); } public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun) @@ -104,7 +104,7 @@ namespace Avalonia.Skia SKFontCache.Shared.Return(skFont); - return new StreamGeometryImpl(path); + return new StreamGeometryImpl(path, path); } /// diff --git a/src/Skia/Avalonia.Skia/RectangleGeometryImpl.cs b/src/Skia/Avalonia.Skia/RectangleGeometryImpl.cs index 93d453e8f0..2d127e07c9 100644 --- a/src/Skia/Avalonia.Skia/RectangleGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/RectangleGeometryImpl.cs @@ -8,14 +8,15 @@ namespace Avalonia.Skia internal class RectangleGeometryImpl : GeometryImpl { public override Rect Bounds { get; } - public override SKPath EffectivePath { get; } + public override SKPath StrokePath { get; } + public override SKPath? FillPath => StrokePath; public RectangleGeometryImpl(Rect rect) { var path = new SKPath(); path.AddRect(rect.ToSKRect()); - EffectivePath = path; + StrokePath = path; Bounds = rect; } } diff --git a/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs b/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs index 0c3289767e..eb081c4f50 100644 --- a/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Avalonia.Media; using Avalonia.Platform; using SkiaSharp; @@ -10,36 +11,39 @@ namespace Avalonia.Skia internal class StreamGeometryImpl : GeometryImpl, IStreamGeometryImpl { private Rect _bounds; - private readonly SKPath _effectivePath; + private readonly SKPath _strokePath; + private SKPath? _fillPath; /// /// Initializes a new instance of the class. /// - /// An existing Skia . + /// An existing Skia for the stroke. + /// An existing Skia for the fill, can also be null or the same as the stroke /// Precomputed path bounds. - public StreamGeometryImpl(SKPath path, Rect bounds) + public StreamGeometryImpl(SKPath stroke, SKPath? fill, Rect? bounds = null) { - _effectivePath = path; - _bounds = bounds; + _strokePath = stroke; + _fillPath = fill; + _bounds = bounds ?? stroke.TightBounds.ToAvaloniaRect(); } - /// - /// Initializes a new instance of the class. - /// - /// An existing Skia . - public StreamGeometryImpl(SKPath path) : this(path, path.TightBounds.ToAvaloniaRect()) + private StreamGeometryImpl(SKPath path) : this(path, path, default(Rect)) { + } /// /// Initializes a new instance of the class. /// - public StreamGeometryImpl() : this(CreateEmptyPath(), default) + public StreamGeometryImpl() : this(CreateEmptyPath()) { } - + /// - public override SKPath EffectivePath => _effectivePath; + public override SKPath? StrokePath => _strokePath; + + /// + public override SKPath? FillPath => _fillPath; /// public override Rect Bounds => _bounds; @@ -47,7 +51,9 @@ namespace Avalonia.Skia /// public IStreamGeometryImpl Clone() { - return new StreamGeometryImpl(_effectivePath.Clone(), Bounds); + var stroke = _strokePath.Clone(); + var fill = _fillPath == _strokePath ? stroke : _fillPath.Clone(); + return new StreamGeometryImpl(stroke, fill, Bounds); } /// @@ -74,7 +80,10 @@ namespace Avalonia.Skia private class StreamContext : IStreamGeometryContextImpl { private readonly StreamGeometryImpl _geometryImpl; - private readonly SKPath _path; + private SKPath Stroke => _geometryImpl._strokePath; + private SKPath Fill => _geometryImpl._fillPath ??= new(); + private bool _isFilled; + private bool Duplicate => _isFilled && !ReferenceEquals(_geometryImpl._fillPath, Stroke); /// /// Initializes a new instance of the class. @@ -83,52 +92,79 @@ namespace Avalonia.Skia public StreamContext(StreamGeometryImpl geometryImpl) { _geometryImpl = geometryImpl; - _path = _geometryImpl._effectivePath; } /// /// Will update bounds of passed geometry. public void Dispose() { - _geometryImpl._bounds = _path.TightBounds.ToAvaloniaRect(); + _geometryImpl._bounds = Stroke.TightBounds.ToAvaloniaRect(); _geometryImpl.InvalidateCaches(); } /// public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection) { - _path.ArcTo( + var arc = isLargeArc ? SKPathArcSize.Large : SKPathArcSize.Small; + var sweep = sweepDirection == SweepDirection.Clockwise + ? SKPathDirection.Clockwise + : SKPathDirection.CounterClockwise; + Stroke.ArcTo( (float)size.Width, (float)size.Height, (float)rotationAngle, - isLargeArc ? SKPathArcSize.Large : SKPathArcSize.Small, - sweepDirection == SweepDirection.Clockwise ? SKPathDirection.Clockwise : SKPathDirection.CounterClockwise, + arc, + sweep, (float)point.X, (float)point.Y); + if(Duplicate) + Fill.ArcTo( + (float)size.Width, + (float)size.Height, + (float)rotationAngle, + arc, + sweep, + (float)point.X, + (float)point.Y); } /// public void BeginFigure(Point startPoint, bool isFilled) { - _path.MoveTo((float)startPoint.X, (float)startPoint.Y); + if (!isFilled) + { + if (Stroke == Fill) + _geometryImpl._fillPath = Stroke.Clone(); + } + + _isFilled = isFilled; + Stroke.MoveTo((float)startPoint.X, (float)startPoint.Y); + if(Duplicate) + Fill.MoveTo((float)startPoint.X, (float)startPoint.Y); } /// public void CubicBezierTo(Point point1, Point point2, Point point3) { - _path.CubicTo((float)point1.X, (float)point1.Y, (float)point2.X, (float)point2.Y, (float)point3.X, (float)point3.Y); + Stroke.CubicTo((float)point1.X, (float)point1.Y, (float)point2.X, (float)point2.Y, (float)point3.X, (float)point3.Y); + if(Duplicate) + Fill.CubicTo((float)point1.X, (float)point1.Y, (float)point2.X, (float)point2.Y, (float)point3.X, (float)point3.Y); } /// public void QuadraticBezierTo(Point point1, Point point2) { - _path.QuadTo((float)point1.X, (float)point1.Y, (float)point2.X, (float)point2.Y); + Stroke.QuadTo((float)point1.X, (float)point1.Y, (float)point2.X, (float)point2.Y); + if(Duplicate) + Fill.QuadTo((float)point1.X, (float)point1.Y, (float)point2.X, (float)point2.Y); } /// public void LineTo(Point point) { - _path.LineTo((float)point.X, (float)point.Y); + Stroke.LineTo((float)point.X, (float)point.Y); + if(Duplicate) + Fill.LineTo((float)point.X, (float)point.Y); } /// @@ -136,14 +172,16 @@ namespace Avalonia.Skia { if (isClosed) { - _path.Close(); + Stroke.Close(); + if (Duplicate) + Fill.Close(); } } /// public void SetFillRule(FillRule fillRule) { - _path.FillType = fillRule == FillRule.EvenOdd ? SKPathFillType.EvenOdd : SKPathFillType.Winding; + Fill.FillType = fillRule == FillRule.EvenOdd ? SKPathFillType.EvenOdd : SKPathFillType.Winding; } } } diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs index e7dd4fb6da..153e4f0720 100644 --- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs +++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs @@ -56,8 +56,6 @@ namespace Avalonia.Skia var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel); - var targetInfos = shapedBuffer.GlyphInfos; - var glyphInfos = buffer.GetGlyphInfoSpan(); var glyphPositions = buffer.GetGlyphPositionSpan(); @@ -83,7 +81,7 @@ namespace Avalonia.Skia 4 * typeface.GetGlyphAdvance(glyphIndex) * textScale; } - targetInfos[i] = new Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset); + shapedBuffer[i] = new Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset); } return shapedBuffer; diff --git a/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs b/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs index fb3c2e403f..2dee8c318c 100644 --- a/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs @@ -17,16 +17,27 @@ namespace Avalonia.Skia { SourceGeometry = source; Transform = transform; + var matrix = transform.ToSKMatrix(); - var transformedPath = source.EffectivePath.Clone(); - transformedPath?.Transform(transform.ToSKMatrix()); - - EffectivePath = transformedPath; + var transformedPath = StrokePath = source.StrokePath.Clone(); + transformedPath?.Transform(matrix); + Bounds = transformedPath?.TightBounds.ToAvaloniaRect() ?? default; + + if (ReferenceEquals(source.StrokePath, source.FillPath)) + FillPath = transformedPath; + else if (source.FillPath != null) + { + FillPath = transformedPath = source.FillPath.Clone(); + transformedPath.Transform(matrix); + } } /// - public override SKPath? EffectivePath { get; } + public override SKPath? StrokePath { get; } + + /// + public override SKPath? FillPath { get; } /// public IGeometryImpl SourceGeometry { get; } diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index f9b5953e3f..87fa963871 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -387,7 +387,7 @@ namespace Avalonia.Direct2D1.Media /// The glyph run. public void DrawGlyphRun(IBrush foreground, IRef glyphRun) { - using (var brush = CreateBrush(foreground, glyphRun.Item.Size)) + using (var brush = CreateBrush(foreground, glyphRun.Item.Bounds.Size)) { var glyphRunImpl = (GlyphRunImpl)glyphRun.Item; diff --git a/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs index 446db47d92..2e7a4b67f6 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs @@ -9,12 +9,12 @@ namespace Avalonia.Direct2D1.Media { public GlyphRunImpl(GlyphRun glyphRun, Size size, Point baselineOrigin) { - Size = size; + Bounds = new Rect(new Point(baselineOrigin.X, 0), size); BaselineOrigin = baselineOrigin; GlyphRun = glyphRun; } - public Size Size { get; } + public Rect Bounds{ get; } public Point BaselineOrigin { get; } diff --git a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs index ff0fff6b14..d42bc3c5cf 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs @@ -52,8 +52,6 @@ namespace Avalonia.Direct2D1.Media var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel); - var targetInfos = shapedBuffer.GlyphInfos; - var glyphInfos = buffer.GetGlyphInfoSpan(); var glyphPositions = buffer.GetGlyphPositionSpan(); @@ -79,7 +77,7 @@ namespace Avalonia.Direct2D1.Media 4 * typeface.GetGlyphAdvance(glyphIndex) * textScale; } - targetInfos[i] = new Avalonia.Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset); + shapedBuffer[i] = new Avalonia.Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset); } return shapedBuffer; diff --git a/src/Windows/Avalonia.Win32/Input/KeyInterop.cs b/src/Windows/Avalonia.Win32/Input/KeyInterop.cs index f5b2d462ab..1ab4c0d2dc 100644 --- a/src/Windows/Avalonia.Win32/Input/KeyInterop.cs +++ b/src/Windows/Avalonia.Win32/Input/KeyInterop.cs @@ -4,7 +4,7 @@ using Avalonia.Win32.Interop; namespace Avalonia.Win32.Input { - static class KeyInterop + public static class KeyInterop { private static readonly Dictionary s_virtualKeyFromKey = new Dictionary { diff --git a/src/tools/DevAnalyzers/DevAnalyzers.csproj b/src/tools/DevAnalyzers/DevAnalyzers.csproj index 2d9331b5dc..6044eb6d2d 100644 --- a/src/tools/DevAnalyzers/DevAnalyzers.csproj +++ b/src/tools/DevAnalyzers/DevAnalyzers.csproj @@ -3,6 +3,7 @@ netstandard2.0 enable + True diff --git a/src/tools/DevGenerators/DevGenerators.csproj b/src/tools/DevGenerators/DevGenerators.csproj index 7e63987d1b..ff5a4aa81d 100644 --- a/src/tools/DevGenerators/DevGenerators.csproj +++ b/src/tools/DevGenerators/DevGenerators.csproj @@ -4,6 +4,7 @@ netstandard2.0 enable false + True diff --git a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs index 0a27602604..0c0b487dba 100644 --- a/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs +++ b/src/tools/PublicAnalyzers/AvaloniaPropertyAnalyzer.CompileAnalyzer.cs @@ -396,7 +396,7 @@ public partial class AvaloniaPropertyAnalyzer hostTypeRef = new(_avaloniaObjectType, Location.None); // assume that an attached property applies everywhere until we find its registration } - var result = new AvaloniaPropertyDescription(inferredName, propertyType, valueType) { HostType = hostTypeRef }; + var result = new AvaloniaPropertyDescription(inferredName, propertyType, valueType) { HostType = hostTypeRef }; // assume that the property is owned by its containing type at the point of assignment, until we find its registration result.SetAssignment(s, new(s.ContainingType, Location.None)); @@ -570,7 +570,7 @@ public partial class AvaloniaPropertyAnalyzer if (_allGetSetMethods.Contains(originalMethod)) { - if (invocation.Instance is IInstanceReferenceOperation { ReferenceKind: InstanceReferenceKind.ContainingTypeInstance } && + if (invocation.Instance is IInstanceReferenceOperation { ReferenceKind: InstanceReferenceKind.ContainingTypeInstance } && GetReferencedProperty(invocation.Arguments[0]) is { } refProp && refProp.description.AssignedTo.TryGetValue(refProp.storageSymbol, out var ownerType) && !DerivesFrom(context.ContainingSymbol.ContainingType, ownerType.Type) && @@ -694,9 +694,14 @@ public partial class AvaloniaPropertyAnalyzer void VerifyAccessor(IMethodSymbol? method, string verb, string methodName) { - if (method == null) + if (method is null) { - context.ReportDiagnostic(Diagnostic.Create(MissingAccessor, property.Locations[0], avaloniaPropertyStorage, verb, methodName)); + if (avaloniaPropertyStorage.DeclaredAccessibility == Accessibility.Public || + (avaloniaPropertyStorage.DeclaredAccessibility == Accessibility.Protected + && avaloniaPropertyStorage.ContainingSymbol.DeclaredAccessibility == Accessibility.Public)) + { + context.ReportDiagnostic(Diagnostic.Create(MissingAccessor, property.Locations[0], avaloniaPropertyStorage, verb, methodName)); + } } else if (method.DeclaredAccessibility != avaloniaPropertyStorage.DeclaredAccessibility && method.DeclaredAccessibility != property.DeclaredAccessibility) { diff --git a/tests/Avalonia.Base.UnitTests/AssetLoaderTests.cs b/tests/Avalonia.Base.UnitTests/AssetLoaderTests.cs index 28fb19e119..894b6578e3 100644 --- a/tests/Avalonia.Base.UnitTests/AssetLoaderTests.cs +++ b/tests/Avalonia.Base.UnitTests/AssetLoaderTests.cs @@ -9,7 +9,7 @@ namespace Avalonia.Base.UnitTests; public class AssetLoaderTests : IDisposable { - public class MockAssembly : Assembly {} + public class MockAssembly : Assembly { } private const string AssemblyNameWithWhitespace = "Awesome Library"; @@ -50,6 +50,17 @@ public class AssetLoaderTests : IDisposable Assert.Equal(AssemblyNameWithNonAscii, assemblyActual?.FullName); } + [Fact] + public void Invalid_AssemblyName_Should_Yield_Empty_Enumerable() + { + var uri = new Uri($"avares://InvalidAssembly"); + var loader = new AssetLoader(); + + var assemblyActual = loader.GetAssets(uri, null); + + Assert.Empty(assemblyActual); + } + private static IAssemblyDescriptor CreateAssemblyDescriptor(string assemblyName) { var assembly = Mock.Of(); diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs index 8ad36a583e..c850fbdb08 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetCurrentValue.cs @@ -1,6 +1,9 @@ using System; +using Avalonia.Controls; using Avalonia.Data; using Avalonia.Diagnostics; +using Avalonia.Styling; +using Avalonia.UnitTests; using Xunit; using Observable = Avalonia.Reactive.Observable; @@ -275,6 +278,79 @@ namespace Avalonia.Base.UnitTests Assert.Equal("style", target.Foo); } + [Fact] + public void SetCurrent_Value_Persists_When_Toggling_Style_1() + { + var target = new Class1(); + var root = new TestRoot(target) + { + Styles = + { + new Style(x => x.OfType().Class("foo")) + { + Setters = { new Setter(Class1.BarProperty, "bar") }, + } + } + }; + + root.LayoutManager.ExecuteInitialLayoutPass(); + + target.SetCurrentValue(Class1.FooProperty, "current"); + + Assert.Equal("current", target.Foo); + Assert.Equal("bardefault", target.Bar); + + target.Classes.Add("foo"); + + Assert.Equal("current", target.Foo); + Assert.Equal("bar", target.Bar); + + target.Classes.Remove("foo"); + + Assert.Equal("current", target.Foo); + Assert.Equal("bardefault", target.Bar); + } + + [Fact] + public void SetCurrent_Value_Persists_When_Toggling_Style_2() + { + var target = new Class1(); + var root = new TestRoot(target) + { + Styles = + { + new Style(x => x.OfType().Class("foo")) + { + Setters = + { + new Setter(Class1.BarProperty, "bar"), + new Setter(Class1.InheritedProperty, "inherited"), + }, + } + } + }; + + root.LayoutManager.ExecuteInitialLayoutPass(); + + target.SetCurrentValue(Class1.FooProperty, "current"); + + Assert.Equal("current", target.Foo); + Assert.Equal("bardefault", target.Bar); + Assert.Equal("inheriteddefault", target.Inherited); + + target.Classes.Add("foo"); + + Assert.Equal("current", target.Foo); + Assert.Equal("bar", target.Bar); + Assert.Equal("inherited", target.Inherited); + + target.Classes.Remove("foo"); + + Assert.Equal("current", target.Foo); + Assert.Equal("bardefault", target.Bar); + Assert.Equal("inheriteddefault", target.Inherited); + } + private BindingPriority GetPriority(AvaloniaObject target, AvaloniaProperty property) { return target.GetDiagnostic(property).Priority; @@ -285,16 +361,19 @@ namespace Avalonia.Base.UnitTests return target.GetDiagnostic(property).IsOverriddenCurrentValue; } - private class Class1 : AvaloniaObject + private class Class1 : Control { public static readonly StyledProperty FooProperty = AvaloniaProperty.Register(nameof(Foo), "foodefault"); + public static readonly StyledProperty BarProperty = + AvaloniaProperty.Register(nameof(Bar), "bardefault"); public static readonly StyledProperty InheritedProperty = AvaloniaProperty.Register(nameof(Inherited), "inheriteddefault", inherits: true); public static readonly StyledProperty CoercedProperty = AvaloniaProperty.Register(nameof(Coerced), coerce: Coerce); public string Foo => GetValue(FooProperty); + public string Bar => GetValue(BarProperty); public string Inherited => GetValue(InheritedProperty); public double Coerced => GetValue(CoercedProperty); public double CoerceMax { get; set; } = 100; diff --git a/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs b/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs index 60603937d9..6ba648af78 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs @@ -569,6 +569,46 @@ public class StyledElementTests_Theming Assert.Equal(Brushes.Green, border.Background); } + [Fact] + public void TemplatedParent_Theme_Change_Applies_Recursively_To_VisualChildren() + { + var theme = CreateDerivedTheme(); + var target = CreateTarget(); + + Assert.Null(target.Theme); + Assert.Null(target.Template); + + var root = CreateRoot(target, theme.BasedOn); + + Assert.NotNull(target.Theme); + Assert.NotNull(target.Template); + + root.Styles.Add(new Style(x => x.OfType().Class("foo")) + { + Setters = { new Setter(StyledElement.ThemeProperty, theme) } + }); + + root.LayoutManager.ExecuteLayoutPass(); + + var border = Assert.IsType(target.VisualChild); + var inner = Assert.IsType(border.Child); + + Assert.Equal(Brushes.Red, border.Background); + Assert.Equal(Brushes.Red, inner.Background); + + Assert.Equal(null, inner.BorderBrush); + Assert.Equal(null, inner.BorderBrush); + + target.Classes.Add("foo"); + root.LayoutManager.ExecuteLayoutPass(); + + Assert.Equal(Brushes.Green, border.Background); + Assert.Equal(Brushes.Green, inner.Background); + + Assert.Equal(Brushes.Cyan, inner.BorderBrush); + Assert.Equal(Brushes.Cyan, inner.BorderBrush); + } + private static ThemedControl CreateTarget() { return new ThemedControl(); @@ -595,7 +635,8 @@ public class StyledElementTests_Theming private static ControlTheme CreateTheme(string tag = "theme") { - var template = new FuncControlTemplate((o, n) => new Border()); + var template = new FuncControlTemplate( + (o, n) => new Border() { Child = new Border() }); return new ControlTheme { diff --git a/tests/Avalonia.Benchmarks/NullGlyphRun.cs b/tests/Avalonia.Benchmarks/NullGlyphRun.cs index c4707c78c8..5b584f302d 100644 --- a/tests/Avalonia.Benchmarks/NullGlyphRun.cs +++ b/tests/Avalonia.Benchmarks/NullGlyphRun.cs @@ -5,7 +5,7 @@ namespace Avalonia.Benchmarks { internal class NullGlyphRun : IGlyphRunImpl { - public Size Size => default; + public Rect Bounds => default; public Point BaselineOrigin => default; diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index f074972cef..3aaf62f0bf 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -73,6 +73,19 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(target, target.Presenter.Panel.TemplatedParent); } + [Fact] + public void Panel_Should_Have_ItemsHost_Set_To_True() + { + var target = new ItemsControl(); + + target.Template = GetTemplate(); + target.Items = new[] { "Foo" }; + target.ApplyTemplate(); + target.Presenter!.ApplyTemplate(); + + Assert.True(target.Presenter.Panel!.IsItemsHost); + } + [Fact] public void Container_Should_Have_TemplatedParent_Set_To_Null() { @@ -634,7 +647,7 @@ namespace Avalonia.Controls.UnitTests target.ApplyTemplate(); target.Presenter.ApplyTemplate(); - var item = target.Presenter.Panel.LogicalChildren[0]; + var item = target.LogicalChildren[0]; Assert.Null(NameScope.GetNameScope((TextBlock)item)); } diff --git a/tests/Avalonia.Controls.UnitTests/PanelTests.cs b/tests/Avalonia.Controls.UnitTests/PanelTests.cs index a31f0dd4c2..82f133d533 100644 --- a/tests/Avalonia.Controls.UnitTests/PanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/PanelTests.cs @@ -141,5 +141,20 @@ namespace Avalonia.Controls.UnitTests var panel = new Panel(); Assert.Throws(() => panel.Children.Add(null!)); } + + [Fact] + public void Adding_Control_To_Items_Host_Panel_Should_Not_Affect_Logical_Children() + { + var child = new Control(); + var realParent = new ContentControl { Content = child }; + var panel = new Panel { IsItemsHost = true }; + + panel.Children.Add(child); + + Assert.Empty(panel.LogicalChildren); + Assert.Same(child.Parent, realParent); + Assert.Same(child.GetLogicalParent(), realParent); + Assert.Same(child.GetVisualParent(), panel); + } } } diff --git a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs index 15ff6e68e3..e5c96dcab6 100644 --- a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs @@ -11,9 +11,6 @@ namespace Avalonia.Controls.UnitTests.Platform { public class DefaultMenuInteractionHandlerTests { - static PointerEventArgs CreateArgs(RoutedEvent ev, object source) - => new PointerEventArgs(ev, source, new FakePointer(), (Visual)source, default, 0, PointerPointProperties.None, default); - static PointerPressedEventArgs CreatePressed(object source) => new PointerPressedEventArgs(source, new FakePointer(), (Visual)source, default,0, new PointerPointProperties (RawInputModifiers.None, PointerUpdateKind.LeftButtonPressed), default); @@ -171,7 +168,7 @@ namespace Avalonia.Controls.UnitTests.Platform var menu = new Mock(); var item = CreateMockMenuItem(isTopLevel: true, hasSubMenu: true, isSubMenuOpen: true, parent: menu.Object); var nextItem = CreateMockMenuItem(isTopLevel: true, hasSubMenu: true, parent: menu.Object); - var e = CreateArgs(MenuItem.PointerEnteredItemEvent, nextItem.Object); + var e = new RoutedEventArgs(MenuItem.PointerEnteredItemEvent, nextItem.Object); menu.SetupGet(x => x.SelectedItem).Returns(item.Object); @@ -191,7 +188,7 @@ namespace Avalonia.Controls.UnitTests.Platform var target = new DefaultMenuInteractionHandler(false); var menu = new Mock(); var item = CreateMockMenuItem(isTopLevel: true, parent: menu.Object); - var e = CreateArgs(MenuItem.PointerExitedItemEvent, item.Object); + var e = new RoutedEventArgs(MenuItem.PointerExitedItemEvent, item.Object); menu.SetupGet(x => x.SelectedItem).Returns(item.Object); target.PointerExited(item, e); @@ -206,7 +203,7 @@ namespace Avalonia.Controls.UnitTests.Platform var target = new DefaultMenuInteractionHandler(false); var menu = new Mock(); var item = CreateMockMenuItem(isTopLevel: true, parent: menu.Object); - var e = CreateArgs(MenuItem.PointerExitedItemEvent, item.Object); + var e = new RoutedEventArgs(MenuItem.PointerExitedItemEvent, item.Object); menu.SetupGet(x => x.IsOpen).Returns(true); menu.SetupGet(x => x.SelectedItem).Returns(item.Object); @@ -365,7 +362,7 @@ namespace Avalonia.Controls.UnitTests.Platform var menu = Mock.Of(); var parentItem = CreateMockMenuItem(isTopLevel: true, hasSubMenu: true, parent: menu); var item = CreateMockMenuItem(parent: parentItem.Object); - var e = CreateArgs(MenuItem.PointerEnteredItemEvent, item.Object); + var e = new RoutedEventArgs(MenuItem.PointerEnteredItemEvent, item.Object); target.PointerEntered(item.Object, e); @@ -381,7 +378,7 @@ namespace Avalonia.Controls.UnitTests.Platform var menu = Mock.Of(); var parentItem = CreateMockMenuItem(isTopLevel: true, hasSubMenu: true, parent: menu); var item = CreateMockMenuItem(hasSubMenu: true, parent: parentItem.Object); - var e = CreateArgs(MenuItem.PointerEnteredItemEvent, item.Object); + var e = new RoutedEventArgs(MenuItem.PointerEnteredItemEvent, item.Object); target.PointerEntered(item.Object, e); item.Verify(x => x.Open(), Times.Never); @@ -401,7 +398,7 @@ namespace Avalonia.Controls.UnitTests.Platform var parentItem = CreateMockMenuItem(isTopLevel: true, hasSubMenu: true, parent: menu); var item = CreateMockMenuItem(parent: parentItem.Object); var sibling = CreateMockMenuItem(hasSubMenu: true, isSubMenuOpen: true, parent: parentItem.Object); - var e = CreateArgs(MenuItem.PointerEnteredItemEvent, item.Object); + var e = new RoutedEventArgs(MenuItem.PointerEnteredItemEvent, item.Object); parentItem.SetupGet(x => x.SubItems).Returns(new[] { item.Object, sibling.Object }); @@ -421,7 +418,7 @@ namespace Avalonia.Controls.UnitTests.Platform var menu = Mock.Of(); var parentItem = CreateMockMenuItem(isTopLevel: true, hasSubMenu: true, parent: menu); var item = CreateMockMenuItem(parent: parentItem.Object); - var e = CreateArgs(MenuItem.PointerExitedItemEvent, item.Object); + var e = new RoutedEventArgs(MenuItem.PointerExitedItemEvent, item.Object); parentItem.SetupGet(x => x.SelectedItem).Returns(item.Object); target.PointerExited(item, e); @@ -438,7 +435,7 @@ namespace Avalonia.Controls.UnitTests.Platform var parentItem = CreateMockMenuItem(isTopLevel: true, hasSubMenu: true, parent: menu); var item = CreateMockMenuItem(parent: parentItem.Object); var sibling = CreateMockMenuItem(parent: parentItem.Object); - var e = CreateArgs(MenuItem.PointerExitedItemEvent, item.Object); + var e = new RoutedEventArgs(MenuItem.PointerExitedItemEvent, item.Object); parentItem.SetupGet(x => x.SelectedItem).Returns(sibling.Object); target.PointerExited(item, e); @@ -454,7 +451,7 @@ namespace Avalonia.Controls.UnitTests.Platform var menu = Mock.Of(); var parentItem = CreateMockMenuItem(isTopLevel: true, hasSubMenu: true, parent: menu); var item = CreateMockMenuItem(hasSubMenu: true, parent: parentItem.Object); - var e = CreateArgs(MenuItem.PointerExitedItemEvent, item.Object); + var e = new RoutedEventArgs(MenuItem.PointerExitedItemEvent, item.Object); item.Setup(x => x.IsPointerOverSubMenu).Returns(true); target.PointerExited(item, e); @@ -488,8 +485,8 @@ namespace Avalonia.Controls.UnitTests.Platform var parentItem = CreateMockMenuItem(isTopLevel: true, hasSubMenu: true, parent: menu); var item = CreateMockMenuItem(hasSubMenu: true, parent: parentItem.Object); var childItem = CreateMockMenuItem(parent: item.Object); - var enter = CreateArgs(MenuItem.PointerEnteredItemEvent, item.Object); - var leave = CreateArgs(MenuItem.PointerExitedItemEvent, item.Object); + var enter = new RoutedEventArgs(MenuItem.PointerEnteredItemEvent, item.Object); + var leave = new RoutedEventArgs(MenuItem.PointerExitedItemEvent, item.Object); // Pointer enters item; item is selected. target.PointerEntered(item, enter); diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index 4804b29fee..bc1225e0e8 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -18,6 +18,7 @@ using Avalonia.Input; using Avalonia.Rendering; using System.Threading.Tasks; using Avalonia.Threading; +using Avalonia.Interactivity; namespace Avalonia.Controls.UnitTests.Primitives { @@ -1048,6 +1049,30 @@ namespace Avalonia.Controls.UnitTests.Primitives } } + [Fact] + public void Events_Should_Be_Routed_To_Popup_Parent() + { + using (CreateServices()) + { + var popupContent = new Border(); + var popup = new Popup { Child = popupContent }; + var popupParent = new Border { Child = popup }; + var root = PreparedWindow(popupParent); + var raised = 0; + + root.LayoutManager.ExecuteInitialLayoutPass(); + popup.Open(); + root.LayoutManager.ExecuteLayoutPass(); + + var ev = new RoutedEventArgs(Button.ClickEvent); + + popupParent.AddHandler(Button.ClickEvent, (s, e) => ++raised); + popupContent.RaiseEvent(ev); + + Assert.Equal(1, raised); + } + } + private IDisposable CreateServices() { return UnitTestApplication.Start(TestServices.StyledWindow.With(windowingPlatform: diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index 4c5397db60..0f72b2101a 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs @@ -327,7 +327,7 @@ namespace Avalonia.Controls.UnitTests ApplyTemplate(target); - var logicalChildren = target.ItemsPresenterPart.Panel.GetLogicalChildren(); + var logicalChildren = target.GetLogicalChildren(); var result = logicalChildren .OfType() diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 1a9fb33b7e..2ca716fa8f 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -1178,7 +1178,7 @@ namespace Avalonia.Controls.UnitTests target.ApplyTemplate(); target.Presenter.ApplyTemplate(); - var item = target.Presenter.Panel.LogicalChildren[0]; + var item = target.LogicalChildren[0]; Assert.Null(NameScope.GetNameScope((TreeViewItem)item)); } diff --git a/tests/Avalonia.IntegrationTests.Appium/AutomationTests.cs b/tests/Avalonia.IntegrationTests.Appium/AutomationTests.cs index bad015506f..4d8760ad61 100644 --- a/tests/Avalonia.IntegrationTests.Appium/AutomationTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/AutomationTests.cs @@ -8,7 +8,7 @@ namespace Avalonia.IntegrationTests.Appium { private readonly AppiumDriver _session; - public AutomationTests(TestAppFixture fixture) + public AutomationTests(DefaultAppFixture fixture) { _session = fixture.Session; diff --git a/tests/Avalonia.IntegrationTests.Appium/ButtonTests.cs b/tests/Avalonia.IntegrationTests.Appium/ButtonTests.cs index 6c630ae782..c0a5414ee3 100644 --- a/tests/Avalonia.IntegrationTests.Appium/ButtonTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/ButtonTests.cs @@ -9,7 +9,7 @@ namespace Avalonia.IntegrationTests.Appium { private readonly AppiumDriver _session; - public ButtonTests(TestAppFixture fixture) + public ButtonTests(DefaultAppFixture fixture) { _session = fixture.Session; diff --git a/tests/Avalonia.IntegrationTests.Appium/CheckBoxTests.cs b/tests/Avalonia.IntegrationTests.Appium/CheckBoxTests.cs index 02e7ac60c4..6c154fa268 100644 --- a/tests/Avalonia.IntegrationTests.Appium/CheckBoxTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/CheckBoxTests.cs @@ -8,7 +8,7 @@ namespace Avalonia.IntegrationTests.Appium { private readonly AppiumDriver _session; - public CheckBoxTests(TestAppFixture fixture) + public CheckBoxTests(DefaultAppFixture fixture) { _session = fixture.Session; diff --git a/tests/Avalonia.IntegrationTests.Appium/CollectionDefinitions.cs b/tests/Avalonia.IntegrationTests.Appium/CollectionDefinitions.cs new file mode 100644 index 0000000000..1e9fa22d9e --- /dev/null +++ b/tests/Avalonia.IntegrationTests.Appium/CollectionDefinitions.cs @@ -0,0 +1,14 @@ +using Xunit; + +namespace Avalonia.IntegrationTests.Appium +{ + [CollectionDefinition("Default")] + public class DefaultCollection : ICollectionFixture + { + } + + [CollectionDefinition("OverlayPopups")] + public class OverlayPopupsCollection : ICollectionFixture + { + } +} diff --git a/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs b/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs index 8df7873582..9e35d366d2 100644 --- a/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs @@ -4,12 +4,11 @@ using Xunit; namespace Avalonia.IntegrationTests.Appium { - [Collection("Default")] - public class ComboBoxTests + public abstract class ComboBoxTests { private readonly AppiumDriver _session; - public ComboBoxTests(TestAppFixture fixture) + public ComboBoxTests(DefaultAppFixture fixture) { _session = fixture.Session; @@ -153,5 +152,17 @@ namespace Avalonia.IntegrationTests.Appium Assert.Equal(string.Empty, comboBox.GetComboBoxValue()); } + + [Collection("Default")] + public class Default : ComboBoxTests + { + public Default(DefaultAppFixture fixture) : base(fixture) { } + } + + [Collection("OverlayPopups")] + public class OverlayPopups : ComboBoxTests + { + public OverlayPopups(OverlayPopupsAppFixture fixture) : base(fixture) { } + } } } diff --git a/tests/Avalonia.IntegrationTests.Appium/TestAppFixture.cs b/tests/Avalonia.IntegrationTests.Appium/DefaultAppFixture.cs similarity index 62% rename from tests/Avalonia.IntegrationTests.Appium/TestAppFixture.cs rename to tests/Avalonia.IntegrationTests.Appium/DefaultAppFixture.cs index d71f9e9bcc..bb08cc0514 100644 --- a/tests/Avalonia.IntegrationTests.Appium/TestAppFixture.cs +++ b/tests/Avalonia.IntegrationTests.Appium/DefaultAppFixture.cs @@ -9,25 +9,21 @@ using OpenQA.Selenium.Appium.Windows; namespace Avalonia.IntegrationTests.Appium { - public class TestAppFixture : IDisposable + public class DefaultAppFixture : IDisposable { private const string TestAppPath = @"..\..\..\..\..\samples\IntegrationTestApp\bin\Debug\net7.0\IntegrationTestApp.exe"; private const string TestAppBundleId = "net.avaloniaui.avalonia.integrationtestapp"; - public TestAppFixture() + public DefaultAppFixture() { - var opts = new AppiumOptions(); - var path = Path.GetFullPath(TestAppPath); + var options = new AppiumOptions(); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - opts.AddAdditionalCapability(MobileCapabilityType.App, path); - opts.AddAdditionalCapability(MobileCapabilityType.PlatformName, MobilePlatform.Windows); - opts.AddAdditionalCapability(MobileCapabilityType.DeviceName, "WindowsPC"); - + ConfigureWin32Options(options); Session = new WindowsDriver( new Uri("http://127.0.0.1:4723"), - opts); + options); // https://github.com/microsoft/WinAppDriver/issues/1025 SetForegroundWindow(new IntPtr(int.Parse( @@ -36,14 +32,10 @@ namespace Avalonia.IntegrationTests.Appium } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - opts.AddAdditionalCapability("appium:bundleId", TestAppBundleId); - opts.AddAdditionalCapability(MobileCapabilityType.PlatformName, MobilePlatform.MacOS); - opts.AddAdditionalCapability(MobileCapabilityType.AutomationName, "mac2"); - opts.AddAdditionalCapability("appium:showServerLogs", true); - + ConfigureMacOptions(options); Session = new MacDriver( new Uri("http://127.0.0.1:4723/wd/hub"), - opts); + options); } else { @@ -51,6 +43,22 @@ namespace Avalonia.IntegrationTests.Appium } } + protected virtual void ConfigureWin32Options(AppiumOptions options) + { + var path = Path.GetFullPath(TestAppPath); + options.AddAdditionalCapability(MobileCapabilityType.App, path); + options.AddAdditionalCapability(MobileCapabilityType.PlatformName, MobilePlatform.Windows); + options.AddAdditionalCapability(MobileCapabilityType.DeviceName, "WindowsPC"); + } + + protected virtual void ConfigureMacOptions(AppiumOptions options) + { + options.AddAdditionalCapability("appium:bundleId", TestAppBundleId); + options.AddAdditionalCapability(MobileCapabilityType.PlatformName, MobilePlatform.MacOS); + options.AddAdditionalCapability(MobileCapabilityType.AutomationName, "mac2"); + options.AddAdditionalCapability("appium:showServerLogs", true); + } + public AppiumDriver Session { get; } public void Dispose() diff --git a/tests/Avalonia.IntegrationTests.Appium/DefaultCollection.cs b/tests/Avalonia.IntegrationTests.Appium/DefaultCollection.cs deleted file mode 100644 index bb2dd1fbec..0000000000 --- a/tests/Avalonia.IntegrationTests.Appium/DefaultCollection.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Xunit; - -namespace Avalonia.IntegrationTests.Appium -{ - [CollectionDefinition("Default")] - public class DefaultCollection : ICollectionFixture - { - } -} diff --git a/tests/Avalonia.IntegrationTests.Appium/GestureTests.cs b/tests/Avalonia.IntegrationTests.Appium/GestureTests.cs index 9745f993cb..65864cc649 100644 --- a/tests/Avalonia.IntegrationTests.Appium/GestureTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/GestureTests.cs @@ -11,7 +11,7 @@ namespace Avalonia.IntegrationTests.Appium { private readonly AppiumDriver _session; - public GestureTests(TestAppFixture fixture) + public GestureTests(DefaultAppFixture fixture) { _session = fixture.Session; diff --git a/tests/Avalonia.IntegrationTests.Appium/ListBoxTests.cs b/tests/Avalonia.IntegrationTests.Appium/ListBoxTests.cs index e2943b3349..5c81c20af1 100644 --- a/tests/Avalonia.IntegrationTests.Appium/ListBoxTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/ListBoxTests.cs @@ -11,7 +11,7 @@ namespace Avalonia.IntegrationTests.Appium { private readonly AppiumDriver _session; - public ListBoxTests(TestAppFixture fixture) + public ListBoxTests(DefaultAppFixture fixture) { _session = fixture.Session; diff --git a/tests/Avalonia.IntegrationTests.Appium/MenuTests.cs b/tests/Avalonia.IntegrationTests.Appium/MenuTests.cs index 3f1fe7de12..5f57dfbc19 100644 --- a/tests/Avalonia.IntegrationTests.Appium/MenuTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/MenuTests.cs @@ -7,11 +7,11 @@ using Xunit; namespace Avalonia.IntegrationTests.Appium { [Collection("Default")] - public class MenuTests + public abstract class MenuTests { private readonly AppiumDriver _session; - public MenuTests(TestAppFixture fixture) + public MenuTests(DefaultAppFixture fixture) { _session = fixture.Session; @@ -181,5 +181,17 @@ namespace Avalonia.IntegrationTests.Appium var tab = tabs.FindElementByName("Menu"); tab.MovePointerOver(); } + + [Collection("Default")] + public class Default : MenuTests + { + public Default(DefaultAppFixture fixture) : base(fixture) { } + } + + [Collection("OverlayPopups")] + public class OverlayPopups : MenuTests + { + public OverlayPopups(OverlayPopupsAppFixture fixture) : base(fixture) { } + } } } diff --git a/tests/Avalonia.IntegrationTests.Appium/NativeMenuTests.cs b/tests/Avalonia.IntegrationTests.Appium/NativeMenuTests.cs index 7858c4cc81..20594a9774 100644 --- a/tests/Avalonia.IntegrationTests.Appium/NativeMenuTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/NativeMenuTests.cs @@ -8,7 +8,7 @@ namespace Avalonia.IntegrationTests.Appium { private readonly AppiumDriver _session; - public NativeMenuTests(TestAppFixture fixture) + public NativeMenuTests(DefaultAppFixture fixture) { _session = fixture.Session; diff --git a/tests/Avalonia.IntegrationTests.Appium/OverlayPopupsAppFixture.cs b/tests/Avalonia.IntegrationTests.Appium/OverlayPopupsAppFixture.cs new file mode 100644 index 0000000000..1f8646888d --- /dev/null +++ b/tests/Avalonia.IntegrationTests.Appium/OverlayPopupsAppFixture.cs @@ -0,0 +1,19 @@ +using OpenQA.Selenium.Appium; + +namespace Avalonia.IntegrationTests.Appium +{ + public class OverlayPopupsAppFixture : DefaultAppFixture + { + protected override void ConfigureWin32Options(AppiumOptions options) + { + base.ConfigureWin32Options(options); + options.AddAdditionalCapability("appArguments", "--overlayPopups"); + } + + protected override void ConfigureMacOptions(AppiumOptions options) + { + base.ConfigureMacOptions(options); + options.AddAdditionalCapability("appium:arguments", new[] { "--overlayPopups" }); + } + } +} diff --git a/tests/Avalonia.IntegrationTests.Appium/RadioButtonTests.cs b/tests/Avalonia.IntegrationTests.Appium/RadioButtonTests.cs index 5bd0a05155..26a8577cb0 100644 --- a/tests/Avalonia.IntegrationTests.Appium/RadioButtonTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/RadioButtonTests.cs @@ -1,10 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using OpenQA.Selenium.Appium; +using OpenQA.Selenium.Appium; using Xunit; namespace Avalonia.IntegrationTests.Appium @@ -14,7 +8,7 @@ namespace Avalonia.IntegrationTests.Appium { private readonly AppiumDriver _session; - public RadioButtonTests(TestAppFixture fixture) + public RadioButtonTests(DefaultAppFixture fixture) { _session = fixture.Session; diff --git a/tests/Avalonia.IntegrationTests.Appium/SliderTests.cs b/tests/Avalonia.IntegrationTests.Appium/SliderTests.cs index 7fa5eb83ee..9371a49ade 100644 --- a/tests/Avalonia.IntegrationTests.Appium/SliderTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/SliderTests.cs @@ -10,7 +10,7 @@ namespace Avalonia.IntegrationTests.Appium { private readonly AppiumDriver _session; - public SliderTests(TestAppFixture fixture) + public SliderTests(DefaultAppFixture fixture) { _session = fixture.Session; diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs index ec24caa18c..a2bfb618d6 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests.cs @@ -19,7 +19,7 @@ namespace Avalonia.IntegrationTests.Appium { private readonly AppiumDriver _session; - public WindowTests(TestAppFixture fixture) + public WindowTests(DefaultAppFixture fixture) { _session = fixture.Session; diff --git a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs index 55812d8df7..2eaaf2e0a8 100644 --- a/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs +++ b/tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs @@ -16,7 +16,7 @@ namespace Avalonia.IntegrationTests.Appium { private readonly AppiumDriver _session; - public WindowTests_MacOS(TestAppFixture fixture) + public WindowTests_MacOS(DefaultAppFixture fixture) { var retry = 0; diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index 0b33cd9d97..27634b457b 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -847,6 +847,30 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions Assert.Equal("test", target.Text); } } + + [Fact] + public void ResolvesRelativeSourceBindingEvenLongerForm() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var target = window.FindControl("text"); + + window.ApplyTemplate(); + window.Presenter.ApplyTemplate(); + target.ApplyTemplate(); + + Assert.Equal("test", target.Text); + } + } [Fact] public void ResolvesRelativeSourceBindingFromTemplate() @@ -1735,7 +1759,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions "; + X='{CompiledBinding StringProperty, DataType=local:TestDataContext}' />"; var control = (AssignBindingControl)AvaloniaRuntimeXamlLoader.Load(new RuntimeXamlLoaderDocument(xaml), new RuntimeXamlLoaderConfiguration { UseCompiledBindingsByDefault = true }); var compiledPath = ((CompiledBindingExtension)control.X).Path; @@ -1745,6 +1769,33 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions } } + [Fact] + public void Should_Bind_To_Nested_Generic_Property() + { + // See https://github.com/AvaloniaUI/Avalonia/issues/10485 + // This code works fine with SRE, and test is passing, but it fails on Cecil. + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var comboBox = window.FindControl("comboBox"); + + var dataContext = new TestDataContext(); + dataContext.GenericProperty.Add(123); + dataContext.GenericProperty.CurrentItem = 123; + window.DataContext = dataContext; + + Assert.Equal(123, comboBox.SelectedItem); + } + } + static void Throws(string type, Action cb) { try @@ -1837,8 +1888,10 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions public string ExplicitProperty => "Bye"; - public static string StaticProperty => "World"; + public static string StaticProperty => "World"; + public ListItemCollectionView GenericProperty { get; } = new(); + public class NonIntegerIndexer : NotifyingBase, INonIntegerIndexerDerived { private readonly Dictionary _storage = new Dictionary(); @@ -1858,6 +1911,11 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions } } + public class ListItemCollectionView : List + { + public T CurrentItem { get; set; } + } + public class MethodDataContext { public void Action() { } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs index c8be1c6d19..1b795a304c 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs @@ -702,9 +702,10 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml // EndInit should be second-to-last operation, as last operation will be // caused by styling being applied on EndInit. - Assert.Equal("EndInit 0", tracker.Order[tracker.Order.Count - 2]); + Assert.Equal("EndInit 0", tracker.Order[tracker.Order.Count - 3]); // Caused by styling. + Assert.Equal("Property FontFamily Changed", tracker.Order[tracker.Order.Count - 2]); Assert.Equal("Property Foreground Changed", tracker.Order[tracker.Order.Count - 1]); } } diff --git a/tests/Avalonia.RenderTests/Shapes/PathTests.cs b/tests/Avalonia.RenderTests/Shapes/PathTests.cs index bac16cca88..bf375121de 100644 --- a/tests/Avalonia.RenderTests/Shapes/PathTests.cs +++ b/tests/Avalonia.RenderTests/Shapes/PathTests.cs @@ -376,5 +376,63 @@ namespace Avalonia.Direct2D1.RenderTests.Shapes await RenderToFile(target); CompareImages(); } + + [Fact] + public async Task BeginFigure_IsFilled_Is_Respected() + { + var target = new Border + { + Width = 200, + Height = 200, + Background = Brushes.White, + Child = new Path + { + Fill = Brushes.Black, + Stroke = Brushes.Black, + StrokeThickness = 10, + Data = new PathGeometry() + { + Figures = new() + { + new PathFigure + { + IsFilled = false, IsClosed = false, + StartPoint = new Point(170,170), + Segments = new () + { + new LineSegment + { + Point = new Point(60, 170) + }, + new LineSegment + { + Point = new Point(60, 60) + } + } + }, + new PathFigure + { + IsFilled = true, IsClosed = true, + StartPoint = new Point(60,20), + Segments = new () + { + new LineSegment + { + Point = new Point(20, 60) + }, + new LineSegment + { + Point = new Point(100, 60) + } + } + } + } + } + } + }; + + await RenderToFile(target); + CompareImages(); + } } } diff --git a/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs index bfe03030c6..acdc242f0d 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs @@ -110,7 +110,7 @@ namespace Avalonia.Skia.UnitTests.Media if (glyphRun.IsLeftToRight) { var characterHit = - glyphRun.GetCharacterHitFromDistance(glyphRun.Size.Width, out _); + glyphRun.GetCharacterHitFromDistance(glyphRun.Bounds.Width, out _); Assert.Equal(glyphRun.Characters.Length, characterHit.FirstCharacterIndex + characterHit.TrailingLength); } @@ -157,9 +157,9 @@ namespace Avalonia.Skia.UnitTests.Media private static List BuildRects(GlyphRun glyphRun) { - var height = glyphRun.Size.Height; + var height = glyphRun.Bounds.Height; - var currentX = glyphRun.IsLeftToRight ? 0d : glyphRun.Size.Width; + var currentX = glyphRun.IsLeftToRight ? 0d : glyphRun.Bounds.Width; var rects = new List(glyphRun.GlyphInfos!.Count); @@ -216,12 +216,12 @@ namespace Avalonia.Skia.UnitTests.Media shapedBuffer.GlyphTypeface, shapedBuffer.FontRenderingEmSize, shapedBuffer.Text, - shapedBuffer.GlyphInfos, + shapedBuffer, biDiLevel: shapedBuffer.BidiLevel); if(shapedBuffer.BidiLevel == 1) { - shapedBuffer.GlyphInfos.Span.Reverse(); + shapedBuffer.Reverse(); } return glyphRun; diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index 9a7460c218..a24da38fdd 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -199,7 +199,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting => textLayout.TextLines .Select(line => string.Join('|', line.TextRuns .Cast() - .SelectMany(run => run.ShapedBuffer.GlyphInfos, (_, glyph) => glyph.GlyphIndex))) + .SelectMany(run => run.ShapedBuffer, (_, glyph) => glyph.GlyphIndex))) .ToList(); } @@ -457,7 +457,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var glyphRun = shapedRun.GlyphRun; - var width = glyphRun.Size.Width; + var width = glyphRun.Bounds.Width; var characterHit = glyphRun.GetCharacterHitFromDistance(width, out _); @@ -489,7 +489,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { var shapedRun = (ShapedTextRun)textRun; - var glyphClusters = shapedRun.ShapedBuffer.GlyphInfos.Select(glyph => glyph.GlyphCluster).ToArray(); + var glyphClusters = shapedRun.ShapedBuffer.Select(glyph => glyph.GlyphCluster).ToArray(); var expected = clusters.Skip(index).Take(glyphClusters.Length).ToArray(); @@ -522,11 +522,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(expectedLength, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphInfos.Count); - Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).ShapedBuffer.GlyphInfos[5].GlyphCluster); + Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).ShapedBuffer[5].GlyphCluster); if (expectedLength == 7) { - Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).ShapedBuffer.GlyphInfos[6].GlyphCluster); + Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).ShapedBuffer[6].GlyphCluster); } } } @@ -783,7 +783,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var rects = layout.TextLines .SelectMany(x => x.TextRuns.Cast()) - .SelectMany(x => x.ShapedBuffer.GlyphInfos, (_, glyph) => glyph.GlyphAdvance) + .SelectMany(x => x.ShapedBuffer, (_, glyph) => glyph.GlyphAdvance) .ToArray(); for (var i = 0; i < SingleLineText.Length; i++) @@ -939,7 +939,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var firstRun = (ShapedTextRun)textLine.TextRuns[0]; - var firstCluster = firstRun.ShapedBuffer.GlyphInfos[0].GlyphCluster; + var firstCluster = firstRun.ShapedBuffer[0].GlyphCluster; var characterHit = textLine.GetCharacterHitFromDistance(0); @@ -953,7 +953,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(characterHit.FirstCharacterIndex)); - var firstAdvance = firstRun.ShapedBuffer.GlyphInfos[0].GlyphAdvance; + var firstAdvance = firstRun.ShapedBuffer[0].GlyphAdvance; Assert.Equal(firstAdvance, distance, 5); @@ -996,9 +996,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var shapedRuns = textLine.TextRuns.Cast().ToList(); - var clusters = shapedRuns.SelectMany(x => x.ShapedBuffer.GlyphInfos, (_, glyph) => glyph.GlyphCluster).ToList(); + var clusters = shapedRuns.SelectMany(x => x.ShapedBuffer, (_, glyph) => glyph.GlyphCluster).ToList(); - var glyphAdvances = shapedRuns.SelectMany(x => x.ShapedBuffer.GlyphInfos, (_, glyph) => glyph.GlyphAdvance).ToList(); + var glyphAdvances = shapedRuns.SelectMany(x => x.ShapedBuffer, (_, glyph) => glyph.GlyphAdvance).ToList(); var currentX = 0.0; diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index bd64d72a4d..0cfa78d007 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -95,7 +95,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { var shapedRun = (ShapedTextRun)textRun; - var runClusters = shapedRun.ShapedBuffer.GlyphInfos.Select(glyph => glyph.GlyphCluster); + var runClusters = shapedRun.ShapedBuffer.Select(glyph => glyph.GlyphCluster); clusters.AddRange(shapedRun.IsReversed ? runClusters.Reverse() : runClusters); } @@ -142,7 +142,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { var shapedRun = (ShapedTextRun)textRun; - var runClusters = shapedRun.ShapedBuffer.GlyphInfos.Select(glyph => glyph.GlyphCluster); + var runClusters = shapedRun.ShapedBuffer.Select(glyph => glyph.GlyphCluster); clusters.AddRange(shapedRun.IsReversed ? runClusters.Reverse() : runClusters); } @@ -249,7 +249,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var clusters = textLine.TextRuns .Cast() - .SelectMany(x => x.ShapedBuffer.GlyphInfos, (_, glyph) => glyph.GlyphCluster) + .SelectMany(x => x.ShapedBuffer, (_, glyph) => glyph.GlyphCluster) .ToArray(); var previousCharacterHit = new CharacterHit(text.Length); @@ -752,7 +752,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { var shapedBuffer = textRun.ShapedBuffer; - var currentClusters = shapedBuffer.GlyphInfos.Select(glyph => glyph.GlyphCluster).ToList(); + var currentClusters = shapedBuffer.Select(glyph => glyph.GlyphCluster).ToList(); foreach (var currentCluster in currentClusters) { @@ -785,11 +785,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { var shapedBuffer = textRun.ShapedBuffer; - for (var index = 0; index < shapedBuffer.GlyphInfos.Length; index++) + for (var index = 0; index < shapedBuffer.Length; index++) { - var currentCluster = shapedBuffer.GlyphInfos[index].GlyphCluster; + var currentCluster = shapedBuffer[index].GlyphCluster; - var advance = shapedBuffer.GlyphInfos[index].GlyphAdvance; + var advance = shapedBuffer[index].GlyphAdvance; if (lastCluster != currentCluster) { diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs index 3f02867aa9..92778793c2 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs @@ -19,10 +19,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var shapedBuffer = TextShaper.Current.ShapeText(text, options); Assert.Equal(shapedBuffer.Length, text.Length); - Assert.Equal(shapedBuffer.GlyphInfos.Length, text.Length); - Assert.Equal(0, shapedBuffer.GlyphInfos[0].GlyphCluster); - Assert.Equal(1, shapedBuffer.GlyphInfos[1].GlyphCluster); - Assert.Equal(1, shapedBuffer.GlyphInfos[2].GlyphCluster); + Assert.Equal(shapedBuffer.Length, text.Length); + Assert.Equal(0, shapedBuffer[0].GlyphCluster); + Assert.Equal(1, shapedBuffer[1].GlyphCluster); + Assert.Equal(1, shapedBuffer[2].GlyphCluster); } } @@ -36,7 +36,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var shapedBuffer = TextShaper.Current.ShapeText(text, options); Assert.Equal(shapedBuffer.Length, text.Length); - Assert.Equal(100, shapedBuffer.GlyphInfos[0].GlyphAdvance); + Assert.Equal(100, shapedBuffer[0].GlyphAdvance); } } diff --git a/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs b/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs index 0448ecd41f..375f87259d 100644 --- a/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs +++ b/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs @@ -52,8 +52,6 @@ namespace Avalonia.UnitTests var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel); - var targetInfos = shapedBuffer.GlyphInfos; - var glyphInfos = buffer.GetGlyphInfoSpan(); var glyphPositions = buffer.GetGlyphPositionSpan(); @@ -79,7 +77,7 @@ namespace Avalonia.UnitTests 4 * typeface.GetGlyphAdvance(glyphIndex) * textScale; } - targetInfos[i] = new Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset); + shapedBuffer[i] = new Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset); } return shapedBuffer; diff --git a/tests/Avalonia.UnitTests/MockGlyphRun.cs b/tests/Avalonia.UnitTests/MockGlyphRun.cs index 0319803a5e..4561d3b3f2 100644 --- a/tests/Avalonia.UnitTests/MockGlyphRun.cs +++ b/tests/Avalonia.UnitTests/MockGlyphRun.cs @@ -16,10 +16,10 @@ namespace Avalonia.UnitTests width += glyphInfos[i].GlyphAdvance; } - Size = new Size(width, 10); + Bounds = new Rect(new Size(width, 10)); } - public Size Size { get; } + public Rect Bounds { get; } public Point BaselineOrigin => new Point(0, 8); diff --git a/tests/Avalonia.UnitTests/MockTextShaperImpl.cs b/tests/Avalonia.UnitTests/MockTextShaperImpl.cs index b810caabd9..b5f4777192 100644 --- a/tests/Avalonia.UnitTests/MockTextShaperImpl.cs +++ b/tests/Avalonia.UnitTests/MockTextShaperImpl.cs @@ -13,7 +13,6 @@ namespace Avalonia.UnitTests var fontRenderingEmSize = options.FontRenderingEmSize; var bidiLevel = options.BidiLevel; var shapedBuffer = new ShapedBuffer(text, text.Length, typeface, fontRenderingEmSize, bidiLevel); - var targetInfos = shapedBuffer.GlyphInfos; var textSpan = text.Span; var textStartIndex = TextTestHelper.GetStartCharIndex(text); @@ -27,7 +26,7 @@ namespace Avalonia.UnitTests for (var j = 0; j < count; ++j) { - targetInfos[i + j] = new GlyphInfo(glyphIndex, glyphCluster, 10); + shapedBuffer[i + j] = new GlyphInfo(glyphIndex, glyphCluster, 10); } i += count; diff --git a/tests/TestFiles/Direct2D1/Shapes/Path/BeginFigure_IsFilled_Is_Respected.expected.png b/tests/TestFiles/Direct2D1/Shapes/Path/BeginFigure_IsFilled_Is_Respected.expected.png new file mode 100644 index 0000000000..d67d71c520 Binary files /dev/null and b/tests/TestFiles/Direct2D1/Shapes/Path/BeginFigure_IsFilled_Is_Respected.expected.png differ diff --git a/tests/TestFiles/Skia/Shapes/Path/BeginFigure_IsFilled_Is_Respected.expected.png b/tests/TestFiles/Skia/Shapes/Path/BeginFigure_IsFilled_Is_Respected.expected.png new file mode 100644 index 0000000000..d67d71c520 Binary files /dev/null and b/tests/TestFiles/Skia/Shapes/Path/BeginFigure_IsFilled_Is_Respected.expected.png differ