Browse Source

Merge branch 'master' into features/Issue_11334

pull/11575/head
workgroupengineering 3 years ago
committed by GitHub
parent
commit
c428ff369d
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      Avalonia.sln
  2. 3
      NuGet.Config
  3. 13
      build/ApiDiff.props
  4. 4
      native/Avalonia.Native/src/OSX/AvnView.mm
  5. 29
      native/Avalonia.Native/src/OSX/platformthreading.mm
  6. 1
      src/Avalonia.Base/Avalonia.Base.csproj
  7. 45
      src/Avalonia.Base/Media/RenderOptions.cs
  8. 2
      src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataGeometryNode.cs
  9. 2
      src/Avalonia.Base/Rendering/Composition/ICompositionTargetDebugEvents.cs
  10. 1
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.DirtyProperties.cs
  11. 22
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
  12. 31
      src/Avalonia.Base/Rendering/SceneGraph/GeometryBoundsHelper.cs
  13. 1
      src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj
  14. 1
      src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj
  15. 1
      src/Avalonia.Controls.ItemsRepeater/Avalonia.Controls.ItemsRepeater.csproj
  16. 1
      src/Avalonia.Controls/Avalonia.Controls.csproj
  17. 27
      src/Avalonia.Controls/Documents/Inline.cs
  18. 2
      src/Avalonia.Controls/Primitives/Track.cs
  19. 4
      src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs
  20. 2
      src/Avalonia.Controls/TextBlock.cs
  21. 34
      src/Avalonia.Controls/TextBox.cs
  22. 1
      src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj
  23. 1
      src/Avalonia.Desktop/Avalonia.Desktop.csproj
  24. 1
      src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj
  25. 1
      src/Avalonia.Dialogs/Avalonia.Dialogs.csproj
  26. 1
      src/Avalonia.ReactiveUI/Avalonia.ReactiveUI.csproj
  27. 3
      src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj
  28. 1
      src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj
  29. 1
      src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj
  30. 3
      src/Headless/Avalonia.Headless.NUnit/Avalonia.Headless.NUnit.csproj
  31. 1
      src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj
  32. 1
      src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj
  33. 1
      src/Headless/Avalonia.Headless/Avalonia.Headless.csproj
  34. 1
      src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
  35. 1
      src/Markup/Avalonia.Markup/Avalonia.Markup.csproj
  36. 55
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  37. 151
      src/Skia/Avalonia.Skia/GeometryImpl.cs
  38. 20
      src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs
  39. 19
      src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs
  40. 3
      src/Windows/Avalonia.Win32/WindowImpl.cs
  41. 68
      tests/Avalonia.Base.UnitTests/Rendering/CompositorInvalidationClippingTests.cs
  42. 21
      tests/Avalonia.Controls.UnitTests/TextBlockTests.cs
  43. 79
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
  44. 43
      tests/Avalonia.Skia.UnitTests/RenderBoundsTests.cs
  45. 17
      tests/Avalonia.UnitTests/CompositorTestServices.cs

1
Avalonia.sln

@ -91,7 +91,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.NetCore", "s
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1-27F5-4255-9AFC-04ABFD11683A}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1-27F5-4255-9AFC-04ABFD11683A}"
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
build\ApiDiff.props = build\ApiDiff.props
build\AvaloniaPublicKey.props = build\AvaloniaPublicKey.props build\AvaloniaPublicKey.props = build\AvaloniaPublicKey.props
build\Base.props = build\Base.props build\Base.props = build\Base.props
build\Binding.props = build\Binding.props build\Binding.props = build\Binding.props

3
NuGet.Config

@ -3,8 +3,7 @@
<configuration> <configuration>
<packageSources> <packageSources>
<clear /> <clear />
<add key="api.nuget.org" value="https://api.nuget.org/v3/index.json" /> <add key="api.nuget.org" value="https://api.nuget.org/v3/index.json" />
<add key="dotnet-eng" value="https://nuget.avaloniaui.net/repository/avalonia-devdeps/index.json" protocolVersion="3" />
<add key="skiasharp" value="https://aka.ms/skiasharp-eap/index.json" /> <add key="skiasharp" value="https://aka.ms/skiasharp-eap/index.json" />
</packageSources> </packageSources>
</configuration> </configuration>

13
build/ApiDiff.props

@ -1,13 +0,0 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ApiContractPackageVersion>0.10.0</ApiContractPackageVersion>
<NugetPackageName Condition="'$(PackageId)' != ''">$(PackageId)</NugetPackageName>
<NugetPackageName Condition="'$(PackageId)' == ''">Avalonia</NugetPackageName>
<RunApiCompat Condition="'$(TargetFramework)' == 'net6.0'">false</RunApiCompat>
</PropertyGroup>
<ItemGroup>
<PackageDownload Include="$(NugetPackageName)" Version="[$(ApiContractPackageVersion)]" />
<PackageReference Include="Microsoft.DotNet.ApiCompat" Version="5.0.0-beta.20372.2" PrivateAssets="All" />
<ResolvedMatchingContract Include="$(NuGetPackageRoot)\$(NugetPackageName.ToLowerInvariant())\$(ApiContractPackageVersion)\lib\$(TargetFramework)\$(AssemblyName).dll" />
</ItemGroup>
</Project>

4
native/Avalonia.Native/src/OSX/AvnView.mm

@ -538,10 +538,10 @@
{ {
_lastKeyHandled = false; _lastKeyHandled = false;
[[self inputContext] handleEvent:event]; [self keyboardEvent:event withType:KeyDown];
if(!_lastKeyHandled){ if(!_lastKeyHandled){
[self keyboardEvent:event withType:KeyDown]; [[self inputContext] handleEvent:event];
} }
} }

29
native/Avalonia.Native/src/OSX/platformthreading.mm

@ -17,7 +17,6 @@ public:
Cancelled = true; Cancelled = true;
if(Running) if(Running)
{ {
Running = false;
if(![NSThread isMainThread]) if(![NSThread isMainThread])
{ {
AddRef(); AddRef();
@ -28,22 +27,22 @@ public:
}); });
return; return;
}; };
Running = false;
if(IsApp) if(IsApp)
[NSApp stop:nil]; [NSApp stop:nil];
else
{ // Wakeup the event loop
// Wakeup the event loop NSEvent* event = [NSEvent otherEventWithType:NSEventTypeApplicationDefined
NSEvent* event = [NSEvent otherEventWithType:NSEventTypeApplicationDefined location:NSMakePoint(0, 0)
location:NSMakePoint(0, 0) modifierFlags:0
modifierFlags:0 timestamp:0
timestamp:0 windowNumber:0
windowNumber:0 context:nil
context:nil subtype:0
subtype:0 data1:0
data1:0 data2:0];
data2:0]; [NSApp postEvent:event atStart:YES];
[NSApp postEvent:event atStart:YES];
}
} }
}; };
}; };

1
src/Avalonia.Base/Avalonia.Base.csproj

@ -15,7 +15,6 @@
<Import Project="..\..\build\Base.props" /> <Import Project="..\..\build\Base.props" />
<Import Project="..\..\build\Binding.props" /> <Import Project="..\..\build\Binding.props" />
<Import Project="..\..\build\System.Memory.props" /> <Import Project="..\..\build\System.Memory.props" />
<Import Project="..\..\build\ApiDiff.props" />
<Import Project="..\..\build\NullableEnable.props" /> <Import Project="..\..\build\NullableEnable.props" />
<Import Project="..\..\build\TrimmingEnable.props" /> <Import Project="..\..\build\TrimmingEnable.props" />
<Import Project="..\..\build\DevAnalyzers.props" /> <Import Project="..\..\build\DevAnalyzers.props" />

45
src/Avalonia.Base/Media/RenderOptions.cs

@ -8,12 +8,13 @@ namespace Avalonia.Media
public EdgeMode EdgeMode { get; init; } public EdgeMode EdgeMode { get; init; }
public TextRenderingMode TextRenderingMode { get; init; } public TextRenderingMode TextRenderingMode { get; init; }
public BitmapBlendingMode BitmapBlendingMode { get; init; } public BitmapBlendingMode BitmapBlendingMode { get; init; }
public bool? RequiresFullOpacityHandling { get; init; }
/// <summary> /// <summary>
/// Gets the value of the BitmapInterpolationMode attached property for a visual. /// Gets the value of the BitmapInterpolationMode attached property for a visual.
/// </summary> /// </summary>
/// <param name="visual">The control.</param> /// <param name="visual">The control.</param>
/// <returns>The control's left coordinate.</returns> /// <returns>The value.</returns>
public static BitmapInterpolationMode GetBitmapInterpolationMode(Visual visual) public static BitmapInterpolationMode GetBitmapInterpolationMode(Visual visual)
{ {
return visual.RenderOptions.BitmapInterpolationMode; return visual.RenderOptions.BitmapInterpolationMode;
@ -23,7 +24,7 @@ namespace Avalonia.Media
/// Sets the value of the BitmapInterpolationMode attached property for a visual. /// Sets the value of the BitmapInterpolationMode attached property for a visual.
/// </summary> /// </summary>
/// <param name="visual">The control.</param> /// <param name="visual">The control.</param>
/// <param name="value">The left value.</param> /// <param name="value">The value.</param>
public static void SetBitmapInterpolationMode(Visual visual, BitmapInterpolationMode value) public static void SetBitmapInterpolationMode(Visual visual, BitmapInterpolationMode value)
{ {
visual.RenderOptions = visual.RenderOptions with { BitmapInterpolationMode = value }; visual.RenderOptions = visual.RenderOptions with { BitmapInterpolationMode = value };
@ -33,7 +34,7 @@ namespace Avalonia.Media
/// Gets the value of the BitmapBlendingMode attached property for a visual. /// Gets the value of the BitmapBlendingMode attached property for a visual.
/// </summary> /// </summary>
/// <param name="visual">The control.</param> /// <param name="visual">The control.</param>
/// <returns>The control's left coordinate.</returns> /// <returns>The value.</returns>
public static BitmapBlendingMode GetBitmapBlendingMode(Visual visual) public static BitmapBlendingMode GetBitmapBlendingMode(Visual visual)
{ {
return visual.RenderOptions.BitmapBlendingMode; return visual.RenderOptions.BitmapBlendingMode;
@ -53,7 +54,7 @@ namespace Avalonia.Media
/// Gets the value of the EdgeMode attached property for a visual. /// Gets the value of the EdgeMode attached property for a visual.
/// </summary> /// </summary>
/// <param name="visual">The control.</param> /// <param name="visual">The control.</param>
/// <returns>The control's left coordinate.</returns> /// <returns>The value.</returns>
public static EdgeMode GetEdgeMode(Visual visual) public static EdgeMode GetEdgeMode(Visual visual)
{ {
return visual.RenderOptions.EdgeMode; return visual.RenderOptions.EdgeMode;
@ -63,7 +64,7 @@ namespace Avalonia.Media
/// Sets the value of the EdgeMode attached property for a visual. /// Sets the value of the EdgeMode attached property for a visual.
/// </summary> /// </summary>
/// <param name="visual">The control.</param> /// <param name="visual">The control.</param>
/// <param name="value">The left value.</param> /// <param name="value">The value.</param>
public static void SetEdgeMode(Visual visual, EdgeMode value) public static void SetEdgeMode(Visual visual, EdgeMode value)
{ {
visual.RenderOptions = visual.RenderOptions with { EdgeMode = value }; visual.RenderOptions = visual.RenderOptions with { EdgeMode = value };
@ -73,7 +74,7 @@ namespace Avalonia.Media
/// Gets the value of the TextRenderingMode attached property for a visual. /// Gets the value of the TextRenderingMode attached property for a visual.
/// </summary> /// </summary>
/// <param name="visual">The control.</param> /// <param name="visual">The control.</param>
/// <returns>The control's left coordinate.</returns> /// <returns>The value.</returns>
public static TextRenderingMode GetTextRenderingMode(Visual visual) public static TextRenderingMode GetTextRenderingMode(Visual visual)
{ {
return visual.RenderOptions.TextRenderingMode; return visual.RenderOptions.TextRenderingMode;
@ -83,12 +84,32 @@ namespace Avalonia.Media
/// Sets the value of the TextRenderingMode attached property for a visual. /// Sets the value of the TextRenderingMode attached property for a visual.
/// </summary> /// </summary>
/// <param name="visual">The control.</param> /// <param name="visual">The control.</param>
/// <param name="value">The left value.</param> /// <param name="value">The value.</param>
public static void SetTextRenderingMode(Visual visual, TextRenderingMode value) public static void SetTextRenderingMode(Visual visual, TextRenderingMode value)
{ {
visual.RenderOptions = visual.RenderOptions with { TextRenderingMode = value }; visual.RenderOptions = visual.RenderOptions with { TextRenderingMode = value };
} }
/// <summary>
/// Gets the value of the RequiresFullOpacityHandling attached property for a visual.
/// </summary>
/// <param name="visual">The control.</param>
/// <returns>The value.</returns>
public static bool? GetRequiresFullOpacityHandling(Visual visual)
{
return visual.RenderOptions.RequiresFullOpacityHandling;
}
/// <summary>
/// Sets the value of the RequiresFullOpacityHandling attached property for a visual.
/// </summary>
/// <param name="visual">The control.</param>
/// <param name="value">The value.</param>
public static void SetRequiresFullOpacityHandling(Visual visual, bool? value)
{
visual.RenderOptions = visual.RenderOptions with { RequiresFullOpacityHandling = value };
}
public RenderOptions MergeWith(RenderOptions other) public RenderOptions MergeWith(RenderOptions other)
{ {
var bitmapInterpolationMode = BitmapInterpolationMode; var bitmapInterpolationMode = BitmapInterpolationMode;
@ -119,12 +140,20 @@ namespace Avalonia.Media
bitmapBlendingMode = other.BitmapBlendingMode; bitmapBlendingMode = other.BitmapBlendingMode;
} }
var requiresFullOpacityHandling = RequiresFullOpacityHandling;
if (requiresFullOpacityHandling == null)
{
requiresFullOpacityHandling = other.RequiresFullOpacityHandling;
}
return new RenderOptions return new RenderOptions
{ {
BitmapInterpolationMode = bitmapInterpolationMode, BitmapInterpolationMode = bitmapInterpolationMode,
EdgeMode = edgeMode, EdgeMode = edgeMode,
TextRenderingMode = textRenderingMode, TextRenderingMode = textRenderingMode,
BitmapBlendingMode = bitmapBlendingMode BitmapBlendingMode = bitmapBlendingMode,
RequiresFullOpacityHandling = requiresFullOpacityHandling
}; };
} }
} }

2
src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataGeometryNode.cs

@ -25,5 +25,5 @@ class RenderDataGeometryNode : RenderDataBrushAndPenNode
context.Context.DrawGeometry(ServerBrush, ServerPen, Geometry!); context.Context.DrawGeometry(ServerBrush, ServerPen, Geometry!);
} }
public override Rect? Bounds => Geometry?.GetRenderBounds(ServerPen).CalculateBoundsWithLineCaps(ServerPen) ?? default; public override Rect? Bounds => Geometry?.GetRenderBounds(ServerPen) ?? default;
} }

2
src/Avalonia.Base/Rendering/Composition/ICompositionTargetDebugEvents.cs

@ -2,5 +2,7 @@ namespace Avalonia.Rendering.Composition;
internal interface ICompositionTargetDebugEvents internal interface ICompositionTargetDebugEvents
{ {
int RenderedVisuals { get; }
void IncrementRenderedVisuals();
void RectInvalidated(Rect rc); void RectInvalidated(Rect rc);
} }

1
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.DirtyProperties.cs

@ -38,6 +38,7 @@ partial class ServerCompositionVisual
CompositionVisualChangedFields.Size CompositionVisualChangedFields.Size
| CompositionVisualChangedFields.SizeAnimated | CompositionVisualChangedFields.SizeAnimated
| CompositionVisualChangedFields.ClipToBounds | CompositionVisualChangedFields.ClipToBounds
| CompositionVisualChangedFields.Clip
| CompositionVisualChangedFields.ClipToBoundsAnimated; | CompositionVisualChangedFields.ClipToBoundsAnimated;
partial void OnFieldsDeserialized(CompositionVisualChangedFields changed) partial void OnFieldsDeserialized(CompositionVisualChangedFields changed)

22
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs

@ -38,6 +38,7 @@ namespace Avalonia.Rendering.Composition.Server
return; return;
Root!.RenderedVisuals++; Root!.RenderedVisuals++;
Root!.DebugEvents?.IncrementRenderedVisuals();
var boundsRect = new Rect(new Size(Size.X, Size.Y)); var boundsRect = new Rect(new Size(Size.X, Size.Y));
@ -182,11 +183,24 @@ namespace Avalonia.Rendering.Composition.Server
if (_clipSizeDirty || positionChanged) if (_clipSizeDirty || positionChanged)
{ {
_transformedClipBounds = ClipToBounds Rect? transformedVisualBounds = null;
? new Rect(new Size(Size.X, Size.Y)) Rect? transformedClipBounds = null;
.TransformToAABB(GlobalTransformMatrix)
: null;
if (ClipToBounds)
transformedVisualBounds = new Rect(new Size(Size.X, Size.Y)).TransformToAABB(GlobalTransformMatrix);
if (Clip != null)
transformedClipBounds = Clip.Bounds.TransformToAABB(GlobalTransformMatrix);
if (transformedVisualBounds != null && transformedClipBounds != null)
_transformedClipBounds = transformedVisualBounds.Value.Intersect(transformedClipBounds.Value);
else if (transformedVisualBounds != null)
_transformedClipBounds = transformedVisualBounds;
else if (transformedClipBounds != null)
_transformedClipBounds = transformedClipBounds;
else
_transformedClipBounds = null;
_clipSizeDirty = false; _clipSizeDirty = false;
} }

31
src/Avalonia.Base/Rendering/SceneGraph/GeometryBoundsHelper.cs

@ -1,31 +0,0 @@
using System;
using Avalonia.Media;
using Avalonia.Utilities;
namespace Avalonia.Rendering.SceneGraph;
internal static class GeometryBoundsHelper
{
/// <summary>
/// Calculates the bounds of a given geometry with respect to the pens <see cref="IPen.LineCap"/>
/// </summary>
/// <param name="originalBounds">The calculated bounds without <see cref="IPen.LineCap"/>s</param>
/// <param name="pen">The pen with information about the <see cref="IPen.LineCap"/>s</param>
/// <returns></returns>
public static Rect CalculateBoundsWithLineCaps(this Rect originalBounds, IPen? pen)
{
if (pen is null || MathUtilities.IsZero(pen.Thickness)) return originalBounds;
switch (pen.LineCap)
{
case PenLineCap.Flat:
return originalBounds;
case PenLineCap.Round:
return originalBounds.Inflate(pen.Thickness / 2);
case PenLineCap.Square:
return originalBounds.Inflate(pen.Thickness);
default:
throw new ArgumentOutOfRangeException();
}
}
}

1
src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj

@ -17,7 +17,6 @@
</ItemGroup> </ItemGroup>
<Import Project="..\..\build\EmbedXaml.props" /> <Import Project="..\..\build\EmbedXaml.props" />
<Import Project="..\..\build\BuildTargets.targets" /> <Import Project="..\..\build\BuildTargets.targets" />
<!--<Import Project="..\..\build\ApiDiff.props" />-->
<Import Project="..\..\build\NullableEnable.props" /> <Import Project="..\..\build\NullableEnable.props" />
<Import Project="..\..\build\TrimmingEnable.props" /> <Import Project="..\..\build\TrimmingEnable.props" />
<Import Project="..\..\build\DevAnalyzers.props" /> <Import Project="..\..\build\DevAnalyzers.props" />

1
src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj

@ -14,7 +14,6 @@
</ItemGroup> </ItemGroup>
<Import Project="..\..\build\EmbedXaml.props" /> <Import Project="..\..\build\EmbedXaml.props" />
<Import Project="..\..\build\BuildTargets.targets" /> <Import Project="..\..\build\BuildTargets.targets" />
<Import Project="..\..\build\ApiDiff.props" />
<Import Project="..\..\build\DevAnalyzers.props" /> <Import Project="..\..\build\DevAnalyzers.props" />
<ItemGroup Label="InternalsVisibleTo"> <ItemGroup Label="InternalsVisibleTo">

1
src/Avalonia.Controls.ItemsRepeater/Avalonia.Controls.ItemsRepeater.csproj

@ -13,7 +13,6 @@
</ItemGroup> </ItemGroup>
<Import Project="..\..\build\EmbedXaml.props" /> <Import Project="..\..\build\EmbedXaml.props" />
<Import Project="..\..\build\BuildTargets.targets" /> <Import Project="..\..\build\BuildTargets.targets" />
<!--<Import Project="..\..\build\ApiDiff.props" />-->
<Import Project="..\..\build\NullableEnable.props" /> <Import Project="..\..\build\NullableEnable.props" />
<Import Project="..\..\build\TrimmingEnable.props" /> <Import Project="..\..\build\TrimmingEnable.props" />
<Import Project="..\..\build\DevAnalyzers.props" /> <Import Project="..\..\build\DevAnalyzers.props" />

1
src/Avalonia.Controls/Avalonia.Controls.csproj

@ -6,7 +6,6 @@
<ProjectReference Include="..\Avalonia.Base\Avalonia.Base.csproj" /> <ProjectReference Include="..\Avalonia.Base\Avalonia.Base.csproj" />
<ProjectReference Include="..\Avalonia.Remote.Protocol\Avalonia.Remote.Protocol.csproj" /> <ProjectReference Include="..\Avalonia.Remote.Protocol\Avalonia.Remote.Protocol.csproj" />
</ItemGroup> </ItemGroup>
<Import Project="..\..\build\ApiDiff.props" />
<Import Project="..\..\build\NullableEnable.props" /> <Import Project="..\..\build\NullableEnable.props" />
<Import Project="..\..\build\TrimmingEnable.props" /> <Import Project="..\..\build\TrimmingEnable.props" />
<Import Project="..\..\build\DevAnalyzers.props" /> <Import Project="..\..\build\DevAnalyzers.props" />

27
src/Avalonia.Controls/Documents/Inline.cs

@ -13,9 +13,10 @@ namespace Avalonia.Controls.Documents
/// <summary> /// <summary>
/// AvaloniaProperty for <see cref="TextDecorations" /> property. /// AvaloniaProperty for <see cref="TextDecorations" /> property.
/// </summary> /// </summary>
public static readonly StyledProperty<TextDecorationCollection?> TextDecorationsProperty = public static readonly AttachedProperty<TextDecorationCollection?> TextDecorationsProperty =
AvaloniaProperty.Register<Inline, TextDecorationCollection?>( AvaloniaProperty.RegisterAttached<Inline, Inline, TextDecorationCollection?>(
nameof(TextDecorations)); nameof(TextDecorations),
inherits: true);
/// <summary> /// <summary>
/// AvaloniaProperty for <see cref="BaselineAlignment" /> property. /// AvaloniaProperty for <see cref="BaselineAlignment" /> property.
@ -43,7 +44,27 @@ namespace Avalonia.Controls.Documents
get { return GetValue(BaselineAlignmentProperty); } get { return GetValue(BaselineAlignmentProperty); }
set { SetValue(BaselineAlignmentProperty, value); } set { SetValue(BaselineAlignmentProperty, value); }
} }
/// <summary>
/// Gets the value of the attached <see cref="TextDecorationsProperty"/> on a control.
/// </summary>
/// <param name="control">The control.</param>
/// <returns>The font style.</returns>
public static TextDecorationCollection? GetTextDecorations(Control control)
{
return control.GetValue(TextDecorationsProperty);
}
/// <summary>
/// Sets the value of the attached <see cref="TextDecorationsProperty"/> on a control.
/// </summary>
/// <param name="control">The control.</param>
/// <param name="value">The property value to set.</param>
public static void SetTextDecorations(Control control, TextDecorationCollection? value)
{
control.SetValue(TextDecorationsProperty, value);
}
internal abstract void BuildTextRun(IList<TextRun> textRuns); internal abstract void BuildTextRun(IList<TextRun> textRuns);
internal abstract void AppendText(StringBuilder stringBuilder); internal abstract void AppendText(StringBuilder stringBuilder);

2
src/Avalonia.Controls/Primitives/Track.cs

@ -52,7 +52,7 @@ namespace Avalonia.Controls.Primitives
ThumbProperty.Changed.AddClassHandler<Track>((x, e) => x.ThumbChanged(e)); ThumbProperty.Changed.AddClassHandler<Track>((x, e) => x.ThumbChanged(e));
IncreaseButtonProperty.Changed.AddClassHandler<Track>((x, e) => x.ButtonChanged(e)); IncreaseButtonProperty.Changed.AddClassHandler<Track>((x, e) => x.ButtonChanged(e));
DecreaseButtonProperty.Changed.AddClassHandler<Track>((x, e) => x.ButtonChanged(e)); DecreaseButtonProperty.Changed.AddClassHandler<Track>((x, e) => x.ButtonChanged(e));
AffectsArrange<Track>(MinimumProperty, MaximumProperty, ValueProperty, OrientationProperty); AffectsArrange<Track>(IsDirectionReversedProperty, MinimumProperty, MaximumProperty, ValueProperty, OrientationProperty);
} }
public Track() public Track()

4
src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs

@ -2,6 +2,7 @@
using Avalonia.Controls.Metadata; using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Styling; using Avalonia.Styling;
@ -36,8 +37,7 @@ namespace Avalonia.Controls
/// Defines the <see cref="IsChecked"/> property. /// Defines the <see cref="IsChecked"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<bool> IsCheckedProperty = public static readonly StyledProperty<bool> IsCheckedProperty =
AvaloniaProperty.Register<ToggleSplitButton, bool>( AvaloniaProperty.Register<ToggleSplitButton, bool>(nameof(IsChecked), false, defaultBindingMode: BindingMode.TwoWay);
nameof(IsChecked));
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ToggleSplitButton"/> class. /// Initializes a new instance of the <see cref="ToggleSplitButton"/> class.

2
src/Avalonia.Controls/TextBlock.cs

@ -135,7 +135,7 @@ namespace Avalonia.Controls
/// Defines the <see cref="TextDecorations"/> property. /// Defines the <see cref="TextDecorations"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<TextDecorationCollection?> TextDecorationsProperty = public static readonly StyledProperty<TextDecorationCollection?> TextDecorationsProperty =
AvaloniaProperty.Register<TextBlock, TextDecorationCollection?>(nameof(TextDecorations)); Inline.TextDecorationsProperty.AddOwner<TextBlock>();
/// <summary> /// <summary>
/// Defines the <see cref="Inlines"/> property. /// Defines the <see cref="Inlines"/> property.

34
src/Avalonia.Controls/TextBox.cs

@ -1879,6 +1879,37 @@ namespace Avalonia.Controls
return text.Substring(start, end - start); return text.Substring(start, end - start);
} }
/// <summary>
/// Returns the sum of any vertical whitespace added between the <see cref="ScrollViewer"/> and <see cref="TextPresenter"/> in the control template.
/// </summary>
/// <returns>The total vertical whitespace.</returns>
private double GetVerticalSpaceBetweenScrollViewerAndPresenter()
{
var verticalSpace = 0.0;
if (_presenter != null)
{
Visual? visual = _presenter;
while ((visual != null) && (visual != this))
{
if (visual == _scrollViewer)
{
// ScrollViewer is a stopping point and should only include the Padding
verticalSpace += _scrollViewer.Padding.Top + _scrollViewer.Padding.Bottom;
break;
}
var margin = visual.GetValue<Thickness>(Layoutable.MarginProperty);
var padding = visual.GetValue<Thickness>(Decorator.PaddingProperty);
verticalSpace += margin.Top + padding.Top + padding.Bottom + margin.Bottom;
visual = visual.VisualParent;
}
}
return verticalSpace;
}
/// <summary> /// <summary>
/// Raises both the <see cref="TextChanging"/> and <see cref="TextChanged"/> events. /// Raises both the <see cref="TextChanging"/> and <see cref="TextChanged"/> events.
/// </summary> /// </summary>
@ -2032,8 +2063,9 @@ namespace Avalonia.Controls
var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch); var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch);
var paragraphProperties = TextLayout.CreateTextParagraphProperties(typeface, fontSize, null, default, default, null, default, LineHeight, default); var paragraphProperties = TextLayout.CreateTextParagraphProperties(typeface, fontSize, null, default, default, null, default, LineHeight, default);
var textLayout = new TextLayout(new MaxLinesTextSource(MaxLines), paragraphProperties); var textLayout = new TextLayout(new MaxLinesTextSource(MaxLines), paragraphProperties);
var verticalSpace = GetVerticalSpaceBetweenScrollViewerAndPresenter();
maxHeight = Math.Ceiling(textLayout.Height); maxHeight = Math.Ceiling(textLayout.Height + verticalSpace);
} }
_scrollViewer.SetCurrentValue(MaxHeightProperty, maxHeight); _scrollViewer.SetCurrentValue(MaxHeightProperty, maxHeight);

1
src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj

@ -19,5 +19,4 @@
<ProjectReference Include="..\Avalonia.Base\Avalonia.Base.csproj" /> <ProjectReference Include="..\Avalonia.Base\Avalonia.Base.csproj" />
<ProjectReference Include="..\Avalonia.Controls\Avalonia.Controls.csproj" /> <ProjectReference Include="..\Avalonia.Controls\Avalonia.Controls.csproj" />
</ItemGroup> </ItemGroup>
<Import Project="..\..\build\ApiDiff.props" />
</Project> </Project>

1
src/Avalonia.Desktop/Avalonia.Desktop.csproj

@ -12,7 +12,6 @@
<ProjectReference Include="../Avalonia.X11/Avalonia.X11.csproj" /> <ProjectReference Include="../Avalonia.X11/Avalonia.X11.csproj" />
</ItemGroup> </ItemGroup>
<Import Project="..\..\build\ApiDiff.props" />
<Import Project="..\..\build\TrimmingEnable.props" /> <Import Project="..\..\build\TrimmingEnable.props" />
<Import Project="..\..\build\NullableEnable.props" /> <Import Project="..\..\build\NullableEnable.props" />
</Project> </Project>

1
src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj

@ -24,7 +24,6 @@
</ItemGroup> </ItemGroup>
<Import Project="..\..\build\EmbedXaml.props" /> <Import Project="..\..\build\EmbedXaml.props" />
<Import Project="..\..\build\BuildTargets.targets" /> <Import Project="..\..\build\BuildTargets.targets" />
<Import Project="..\..\build\ApiDiff.props" />
<Import Project="..\..\build\NullableEnable.props" /> <Import Project="..\..\build\NullableEnable.props" />
<Import Project="..\..\build\DevAnalyzers.props" /> <Import Project="..\..\build\DevAnalyzers.props" />
</Project> </Project>

1
src/Avalonia.Dialogs/Avalonia.Dialogs.csproj

@ -19,7 +19,6 @@
<InternalsVisibleTo Include="ControlCatalog, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="ControlCatalog, PublicKey=$(AvaloniaPublicKey)" />
</ItemGroup> </ItemGroup>
<Import Project="..\..\build\ApiDiff.props" />
<Import Project="..\..\build\DevAnalyzers.props" /> <Import Project="..\..\build\DevAnalyzers.props" />
<Import Project="..\..\build\TrimmingEnable.props" /> <Import Project="..\..\build\TrimmingEnable.props" />
</Project> </Project>

1
src/Avalonia.ReactiveUI/Avalonia.ReactiveUI.csproj

@ -9,7 +9,6 @@
</ItemGroup> </ItemGroup>
<Import Project="..\..\build\Rx.props" /> <Import Project="..\..\build\Rx.props" />
<Import Project="..\..\build\ReactiveUI.props" /> <Import Project="..\..\build\ReactiveUI.props" />
<Import Project="..\..\build\ApiDiff.props" />
<Import Project="..\..\build\NullableEnable.props" /> <Import Project="..\..\build\NullableEnable.props" />
<Import Project="..\..\build\TrimmingEnable.props" /> <Import Project="..\..\build\TrimmingEnable.props" />
<Import Project="..\..\build\DevAnalyzers.props" /> <Import Project="..\..\build\DevAnalyzers.props" />

3
src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj

@ -10,6 +10,5 @@
<Compile Include="..\Avalonia.Base\Input\Key.cs" /> <Compile Include="..\Avalonia.Base\Input\Key.cs" />
<Compile Include="..\Avalonia.Base\Compatibility\TrimmingAttributes.cs" Visible="False" /> <Compile Include="..\Avalonia.Base\Compatibility\TrimmingAttributes.cs" Visible="False" />
</ItemGroup> </ItemGroup>
<Import Project="..\..\build\ApiDiff.props" />
<Import Project="..\..\build\TrimmingEnable.props" /> <Import Project="..\..\build\TrimmingEnable.props" />
</Project> </Project>

1
src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj

@ -11,7 +11,6 @@
</ItemGroup> </ItemGroup>
<Import Project="..\..\build\NullableEnable.props" /> <Import Project="..\..\build\NullableEnable.props" />
<Import Project="..\..\build\BuildTargets.targets" /> <Import Project="..\..\build\BuildTargets.targets" />
<Import Project="..\..\build\ApiDiff.props" />
<Import Project="..\..\build\TrimmingEnable.props" /> <Import Project="..\..\build\TrimmingEnable.props" />
<Import Project="..\..\build\DevAnalyzers.props" /> <Import Project="..\..\build\DevAnalyzers.props" />
</Project> </Project>

1
src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj

@ -11,7 +11,6 @@
</ItemGroup> </ItemGroup>
<Import Project="..\..\build\NullableEnable.props" /> <Import Project="..\..\build\NullableEnable.props" />
<Import Project="..\..\build\BuildTargets.targets" /> <Import Project="..\..\build\BuildTargets.targets" />
<Import Project="..\..\build\ApiDiff.props" />
<Import Project="..\..\build\TrimmingEnable.props" /> <Import Project="..\..\build\TrimmingEnable.props" />
<Import Project="..\..\build\DevAnalyzers.props" /> <Import Project="..\..\build\DevAnalyzers.props" />
</Project> </Project>

3
src/Headless/Avalonia.Headless.NUnit/Avalonia.Headless.NUnit.csproj

@ -12,8 +12,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Avalonia.Headless\Avalonia.Headless.csproj" /> <ProjectReference Include="..\Avalonia.Headless\Avalonia.Headless.csproj" />
</ItemGroup> </ItemGroup>
<Import Project="..\..\..\build\ApiDiff.props" />
<Import Project="..\..\..\build\DevAnalyzers.props" /> <Import Project="..\..\..\build\DevAnalyzers.props" />
<Import Project="..\..\..\build\NullableEnable.props" /> <Import Project="..\..\..\build\NullableEnable.props" />
</Project> </Project>

1
src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj

@ -10,7 +10,6 @@
<PackageReference Include="Quamotion.RemoteViewing" Version="1.1.21" /> <PackageReference Include="Quamotion.RemoteViewing" Version="1.1.21" />
</ItemGroup> </ItemGroup>
<Import Project="..\..\..\build\ApiDiff.props" />
<Import Project="..\..\..\build\DevAnalyzers.props" /> <Import Project="..\..\..\build\DevAnalyzers.props" />
<Import Project="..\..\..\build\TrimmingEnable.props" /> <Import Project="..\..\..\build\TrimmingEnable.props" />
<Import Project="..\..\..\build\NullableEnable.props" /> <Import Project="..\..\..\build\NullableEnable.props" />

1
src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj

@ -13,7 +13,6 @@
<ProjectReference Include="..\Avalonia.Headless\Avalonia.Headless.csproj" /> <ProjectReference Include="..\Avalonia.Headless\Avalonia.Headless.csproj" />
</ItemGroup> </ItemGroup>
<Import Project="..\..\..\build\ApiDiff.props" />
<Import Project="..\..\..\build\DevAnalyzers.props" /> <Import Project="..\..\..\build\DevAnalyzers.props" />
<Import Project="..\..\..\build\NullableEnable.props" /> <Import Project="..\..\..\build\NullableEnable.props" />
</Project> </Project>

1
src/Headless/Avalonia.Headless/Avalonia.Headless.csproj

@ -7,7 +7,6 @@
<ProjectReference Include="..\..\..\packages\Avalonia\Avalonia.csproj" /> <ProjectReference Include="..\..\..\packages\Avalonia\Avalonia.csproj" />
</ItemGroup> </ItemGroup>
<Import Project="..\..\..\build\ApiDiff.props" />
<Import Project="..\..\..\build\DevAnalyzers.props" /> <Import Project="..\..\..\build\DevAnalyzers.props" />
<Import Project="..\..\..\build\TrimmingEnable.props" /> <Import Project="..\..\..\build\TrimmingEnable.props" />
<Import Project="..\..\..\build\NullableEnable.props" /> <Import Project="..\..\..\build\NullableEnable.props" />

1
src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj

@ -65,7 +65,6 @@
<ProjectReference Include="..\..\Avalonia.Controls\Avalonia.Controls.csproj" /> <ProjectReference Include="..\..\Avalonia.Controls\Avalonia.Controls.csproj" />
<ProjectReference Include="..\Avalonia.Markup\Avalonia.Markup.csproj" /> <ProjectReference Include="..\Avalonia.Markup\Avalonia.Markup.csproj" />
</ItemGroup> </ItemGroup>
<Import Project="..\..\..\build\ApiDiff.props" />
<Import Project="..\..\..\build\DevAnalyzers.props" /> <Import Project="..\..\..\build\DevAnalyzers.props" />
<Import Project="..\..\..\build\TrimmingEnable.props" /> <Import Project="..\..\..\build\TrimmingEnable.props" />
<Import Project="..\..\..\build\NullableEnable.props" /> <Import Project="..\..\..\build\NullableEnable.props" />

1
src/Markup/Avalonia.Markup/Avalonia.Markup.csproj

@ -13,7 +13,6 @@
<ProjectReference Include="..\..\Avalonia.Base\Avalonia.Base.csproj" /> <ProjectReference Include="..\..\Avalonia.Base\Avalonia.Base.csproj" />
</ItemGroup> </ItemGroup>
<Import Project="..\..\..\build\System.Memory.props" /> <Import Project="..\..\..\build\System.Memory.props" />
<Import Project="..\..\..\build\ApiDiff.props" />
<Import Project="..\..\..\build\NullableEnable.props" /> <Import Project="..\..\..\build\NullableEnable.props" />
<Import Project="..\..\..\build\DevAnalyzers.props" /> <Import Project="..\..\..\build\DevAnalyzers.props" />
<Import Project="..\..\..\build\TrimmingEnable.props" /> <Import Project="..\..\..\build\TrimmingEnable.props" />

55
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@ -189,7 +189,8 @@ namespace Avalonia.Skia
var d = destRect.ToSKRect(); var d = destRect.ToSKRect();
var paint = SKPaintCache.Shared.Get(); var paint = SKPaintCache.Shared.Get();
paint.Color = new SKColor(255, 255, 255, (byte)(255 * opacity * (_useOpacitySaveLayer ? 1 : _currentOpacity)));
paint.Color = new SKColor(255, 255, 255, (byte)(255 * opacity * _currentOpacity));
paint.FilterQuality = RenderOptions.BitmapInterpolationMode.ToSKFilterQuality(); paint.FilterQuality = RenderOptions.BitmapInterpolationMode.ToSKFilterQuality();
paint.BlendMode = RenderOptions.BitmapBlendingMode.ToSKBlendMode(); paint.BlendMode = RenderOptions.BitmapBlendingMode.ToSKBlendMode();
@ -375,7 +376,7 @@ namespace Avalonia.Skia
{ {
if (boxShadow != default && !boxShadow.IsInset) if (boxShadow != default && !boxShadow.IsInset)
{ {
using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _useOpacitySaveLayer ? 1 : _currentOpacity)) using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _currentOpacity))
{ {
var spread = (float)boxShadow.Spread; var spread = (float)boxShadow.Spread;
if (boxShadow.IsInset) if (boxShadow.IsInset)
@ -432,7 +433,7 @@ namespace Avalonia.Skia
{ {
if (boxShadow != default && boxShadow.IsInset) if (boxShadow != default && boxShadow.IsInset)
{ {
using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _useOpacitySaveLayer ? 1 : _currentOpacity)) using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _currentOpacity))
{ {
var spread = (float)boxShadow.Spread; var spread = (float)boxShadow.Spread;
var offsetX = (float)boxShadow.OffsetX; var offsetX = (float)boxShadow.OffsetX;
@ -592,8 +593,16 @@ namespace Avalonia.Skia
{ {
CheckLease(); CheckLease();
if(_useOpacitySaveLayer) _opacityStack.Push(_currentOpacity);
var useOpacitySaveLayer = _useOpacitySaveLayer || RenderOptions.RequiresFullOpacityHandling == true;
if (useOpacitySaveLayer)
{ {
opacity = _currentOpacity * opacity; //Take current multiplied opacity
_currentOpacity = 1; //Opacity is applied via layering
if (bounds.HasValue) if (bounds.HasValue)
{ {
var rect = bounds.Value.ToSKRect(); var rect = bounds.Value.ToSKRect();
@ -606,7 +615,6 @@ namespace Avalonia.Skia
} }
else else
{ {
_opacityStack.Push(_currentOpacity);
_currentOpacity *= opacity; _currentOpacity *= opacity;
} }
} }
@ -616,14 +624,14 @@ namespace Avalonia.Skia
{ {
CheckLease(); CheckLease();
if(_useOpacitySaveLayer) var useOpacitySaveLayer = _useOpacitySaveLayer || RenderOptions.RequiresFullOpacityHandling == true;
if (useOpacitySaveLayer)
{ {
Canvas.Restore(); Canvas.Restore();
} }
else
{ _currentOpacity = _opacityStack.Pop();
_currentOpacity = _opacityStack.Pop();
}
} }
/// <inheritdoc /> /// <inheritdoc />
@ -1239,31 +1247,8 @@ namespace Avalonia.Skia
// https://docs.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/graphics/skiasharp/paths/dots // https://docs.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/graphics/skiasharp/paths/dots
// TODO: Still something is off, dashes are now present, but don't look the same as D2D ones. // TODO: Still something is off, dashes are now present, but don't look the same as D2D ones.
switch (pen.LineCap) paint.StrokeCap = pen.LineCap.ToSKStrokeCap();
{ paint.StrokeJoin = pen.LineJoin.ToSKStrokeJoin();
case PenLineCap.Round:
paint.StrokeCap = SKStrokeCap.Round;
break;
case PenLineCap.Square:
paint.StrokeCap = SKStrokeCap.Square;
break;
default:
paint.StrokeCap = SKStrokeCap.Butt;
break;
}
switch (pen.LineJoin)
{
case PenLineJoin.Miter:
paint.StrokeJoin = SKStrokeJoin.Miter;
break;
case PenLineJoin.Round:
paint.StrokeJoin = SKStrokeJoin.Round;
break;
default:
paint.StrokeJoin = SKStrokeJoin.Bevel;
break;
}
paint.StrokeMiter = (float) pen.MiterLimit; paint.StrokeMiter = (float) pen.MiterLimit;

151
src/Skia/Avalonia.Skia/GeometryImpl.cs

@ -43,46 +43,11 @@ namespace Avalonia.Skia
/// <inheritdoc /> /// <inheritdoc />
public bool StrokeContains(IPen? pen, Point point) public bool StrokeContains(IPen? pen, Point point)
{ {
// Skia requires to compute stroke path to check for point containment. _pathCache.UpdateIfNeeded(StrokePath, pen);
// Due to that we are caching using stroke width.
// Usually this function is being called with same stroke width per path, so this saves a lot of Skia traffic.
var strokeWidth = (float)(pen?.Thickness ?? 0); return PathContainsCore(_pathCache.ExpandedPath, point);
if (!_pathCache.HasCacheFor(strokeWidth))
{
UpdatePathCache(strokeWidth);
}
return PathContainsCore(_pathCache.CachedStrokePath, point);
}
/// <summary>
/// Update path cache for given stroke width.
/// </summary>
/// <param name="strokeWidth">Stroke width.</param>
private void UpdatePathCache(float strokeWidth)
{
var strokePath = new SKPath();
// For stroke widths close to 0 simply use empty path. Render bounds are cached from fill path.
if (Math.Abs(strokeWidth) < float.Epsilon)
{
_pathCache.Cache(strokePath, strokeWidth, Bounds);
}
else
{
var paint = SKPaintCache.Shared.Get();
paint.IsStroke = true;
paint.StrokeWidth = strokeWidth;
paint.GetFillPath(StrokePath, strokePath);
SKPaintCache.Shared.ReturnReset(paint);
_pathCache.Cache(strokePath, strokeWidth, strokePath.TightBounds.ToAvaloniaRect());
}
} }
/// <summary> /// <summary>
/// Check Skia path if it contains a point. /// Check Skia path if it contains a point.
/// </summary> /// </summary>
@ -106,14 +71,8 @@ namespace Avalonia.Skia
/// <inheritdoc /> /// <inheritdoc />
public Rect GetRenderBounds(IPen? pen) public Rect GetRenderBounds(IPen? pen)
{ {
var strokeWidth = (float)(pen?.Thickness ?? 0); _pathCache.UpdateIfNeeded(StrokePath, pen);
return _pathCache.RenderBounds;
if (!_pathCache.HasCacheFor(strokeWidth))
{
UpdatePathCache(strokeWidth);
}
return _pathCache.CachedGeometryRenderBounds;
} }
/// <inheritdoc /> /// <inheritdoc />
@ -180,66 +139,70 @@ namespace Avalonia.Skia
/// </summary> /// </summary>
protected void InvalidateCaches() protected void InvalidateCaches()
{ {
_pathCache.Invalidate(); _pathCache.Dispose();
_pathCache = default;
} }
private struct PathCache private struct PathCache
{ {
private float _cachedStrokeWidth; private double _width, _miterLimit;
private PenLineCap _cap;
/// <summary> private PenLineJoin _join;
/// Tolerance for two stroke widths to be deemed equal private SKPath? _path, _cachedFor;
/// </summary> private Rect? _renderBounds;
public const float Tolerance = float.Epsilon; private static readonly SKPath s_emptyPath = new();
/// <summary>
/// Cached contour path. public Rect RenderBounds => _renderBounds ??= (_path ?? _cachedFor ?? s_emptyPath).Bounds.ToAvaloniaRect();
/// </summary> public SKPath ExpandedPath => _path ?? s_emptyPath;
public SKPath? CachedStrokePath { get; private set; }
public void UpdateIfNeeded(SKPath? strokePath, IPen? pen)
/// <summary>
/// Cached geometry render bounds.
/// </summary>
public Rect CachedGeometryRenderBounds { get; private set; }
/// <summary>
/// Is cached valid for given stroke width.
/// </summary>
/// <param name="strokeWidth">Stroke width to check.</param>
/// <returns>True, if CachedStrokePath can be used for given stroke width.</returns>
public bool HasCacheFor(float strokeWidth)
{ {
return CachedStrokePath != null && Math.Abs(_cachedStrokeWidth - strokeWidth) < Tolerance; var strokeWidth = pen?.Thickness ?? 0;
} var miterLimit = pen?.MiterLimit ?? 0;
var cap = pen?.LineCap ?? default;
/// <summary> var join = pen?.LineJoin ?? default;
/// Cache path for given stroke width. Takes ownership of a passed path.
/// </summary> if (_cachedFor == strokePath
/// <param name="path">Path to cache.</param> && _path != null
/// <param name="strokeWidth">Stroke width to cache.</param> && cap == _cap
/// <param name="geometryRenderBounds">Render bounds to use.</param> && join == _join
public void Cache(SKPath path, float strokeWidth, Rect geometryRenderBounds) && Math.Abs(_width - strokeWidth) < float.Epsilon
{ && (join != PenLineJoin.Miter || Math.Abs(_miterLimit - miterLimit) > float.Epsilon))
if (CachedStrokePath != path) // We are up to date
return;
_renderBounds = null;
_cachedFor = strokePath;
_width = strokeWidth;
_cap = cap;
_join = join;
_miterLimit = miterLimit;
if (strokePath == null || Math.Abs(strokeWidth) < float.Epsilon)
{ {
CachedStrokePath?.Dispose(); _path = null;
return;
} }
CachedStrokePath = path; var paint = SKPaintCache.Shared.Get();
CachedGeometryRenderBounds = geometryRenderBounds; paint.IsStroke = true;
_cachedStrokeWidth = strokeWidth; paint.StrokeWidth = (float)_width;
paint.StrokeCap = cap.ToSKStrokeCap();
paint.StrokeJoin = join.ToSKStrokeJoin();
paint.StrokeMiter = (float)miterLimit;
_path = new SKPath();
paint.GetFillPath(strokePath, _path);
SKPaintCache.Shared.ReturnReset(paint);
} }
/// <summary> public void Dispose()
/// Invalidate cache state.
/// </summary>
public void Invalidate()
{ {
CachedStrokePath?.Dispose(); _path?.Dispose();
CachedStrokePath = null; _path = null;
CachedGeometryRenderBounds = default;
_cachedStrokeWidth = default;
} }
} }
} }
} }

20
src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs

@ -196,6 +196,26 @@ namespace Avalonia.Skia
} }
} }
public static SKStrokeCap ToSKStrokeCap(this PenLineCap cap)
{
return cap switch
{
PenLineCap.Round => SKStrokeCap.Round,
PenLineCap.Square => SKStrokeCap.Square,
_ => SKStrokeCap.Butt
};
}
public static SKStrokeJoin ToSKStrokeJoin(this PenLineJoin join)
{
return join switch
{
PenLineJoin.Bevel => SKStrokeJoin.Bevel,
PenLineJoin.Round => SKStrokeJoin.Round,
_ => SKStrokeJoin.Miter
};
}
public static TextAlignment ToAvalonia(this SKTextAlign a) public static TextAlignment ToAvalonia(this SKTextAlign a)
{ {
switch (a) switch (a)

19
src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs

@ -1,6 +1,10 @@
using System;
using Avalonia.Logging; using Avalonia.Logging;
using Avalonia.Media;
using Avalonia.Platform; using Avalonia.Platform;
using SharpDX.Direct2D1; using SharpDX.Direct2D1;
using Geometry = SharpDX.Direct2D1.Geometry;
using PathGeometry = SharpDX.Direct2D1.PathGeometry;
namespace Avalonia.Direct2D1.Media namespace Avalonia.Direct2D1.Media
{ {
@ -27,7 +31,20 @@ namespace Avalonia.Direct2D1.Media
/// <inheritdoc/> /// <inheritdoc/>
public Rect GetRenderBounds(Avalonia.Media.IPen pen) public Rect GetRenderBounds(Avalonia.Media.IPen pen)
{ {
return Geometry.GetWidenedBounds((float)(pen?.Thickness ?? 0)).ToAvalonia(); if (pen == null || Math.Abs(pen.Thickness) < float.Epsilon)
return Geometry.GetBounds().ToAvalonia();
var originalBounds = Geometry.GetWidenedBounds((float)pen.Thickness).ToAvalonia();
switch (pen.LineCap)
{
case PenLineCap.Flat:
return originalBounds;
case PenLineCap.Round:
return originalBounds.Inflate(pen.Thickness / 2);
case PenLineCap.Square:
return originalBounds.Inflate(pen.Thickness);
default:
throw new ArgumentOutOfRangeException();
}
} }
/// <inheritdoc/> /// <inheritdoc/>

3
src/Windows/Avalonia.Win32/WindowImpl.cs

@ -683,9 +683,10 @@ namespace Avalonia.Win32
if (parentHwnd == IntPtr.Zero && !_windowProperties.ShowInTaskbar) if (parentHwnd == IntPtr.Zero && !_windowProperties.ShowInTaskbar)
{ {
parentHwnd = OffscreenParentWindow.Handle; parentHwnd = OffscreenParentWindow.Handle;
_hiddenWindowIsParent = true;
} }
_hiddenWindowIsParent = parentHwnd == OffscreenParentWindow.Handle;
SetWindowLongPtr(_hwnd, (int)WindowLongParam.GWL_HWNDPARENT, parentHwnd); SetWindowLongPtr(_hwnd, (int)WindowLongParam.GWL_HWNDPARENT, parentHwnd);
} }

68
tests/Avalonia.Base.UnitTests/Rendering/CompositorInvalidationClippingTests.cs

@ -0,0 +1,68 @@
using Avalonia.Controls;
using Avalonia.Media;
using Xunit;
namespace Avalonia.Base.UnitTests.Rendering;
/// <summary>
/// Test class that verifies how clipping influences rendering in the compositor
/// </summary>
public class CompositorInvalidationClippingTests : CompositorTestsBase
{
[Fact]
// Test case: When the ClipToBounds is false, all visuals should be rendered
public void Siblings_Should_Be_Rendered_On_Invalidate_Without_ClipToBounds()
{
AssertRenderedVisuals(clipToBounds: false, clipGeometry: false, expectedRenderedVisualsCount: 4);
}
[Fact]
// Test case: When the ClipToBounds is true, only visuals within the clipped boundary should be rendered
public void Siblings_Should_Not_Be_Rendered_On_Invalidate_With_ClipToBounds()
{
AssertRenderedVisuals(clipToBounds: true, clipGeometry: false, expectedRenderedVisualsCount: 3);
}
[Fact]
// Test case: When the Clip is used, only visuals within the clip geometry should be rendered
public void Siblings_Should_Not_Be_Rendered_On_Invalidate_With_Clip()
{
AssertRenderedVisuals(clipToBounds: false, clipGeometry: true, expectedRenderedVisualsCount: 3);
}
private void AssertRenderedVisuals(bool clipToBounds, bool clipGeometry, int expectedRenderedVisualsCount)
{
using (var s = new CompositorCanvas())
{
//#1 visual is top level
//#2 visual is s.Canvas
//#3 visual is border1
s.Canvas.Children.Add(new Border()
{
[Canvas.LeftProperty] = 0, [Canvas.TopProperty] = 0,
Width = 20, Height = 10,
Background = Brushes.Red,
ClipToBounds = clipToBounds,
Clip = clipGeometry ? new RectangleGeometry(new Rect(new Size(20, 10))) : null
});
//#4 visual is border2
s.Canvas.Children.Add(new Border()
{
[Canvas.LeftProperty] = 30, [Canvas.TopProperty] = 50,
Width = 20, Height = 10,
Background = Brushes.Red,
ClipToBounds = clipToBounds,
Clip = clipGeometry ? new RectangleGeometry(new Rect(new Size(20, 10))) : null
});
s.RunJobs();
s.Events.Reset();
//invalidate border1
s.Canvas.Children[0].IsVisible = false;
s.RunJobs();
s.AssertRenderedVisuals(expectedRenderedVisualsCount);
}
}
}

21
tests/Avalonia.Controls.UnitTests/TextBlockTests.cs

@ -202,5 +202,26 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(0, target.Inlines.Count); Assert.Equal(0, target.Inlines.Count);
} }
} }
[Fact]
public void Setting_TextDecorations_Should_Update_Inlines()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var target = new TextBlock();
target.Inlines.Add(new Run("Hello World"));
Assert.Equal(1, target.Inlines.Count);
Assert.Null(target.Inlines[0].TextDecorations);
var underline = TextDecorations.Underline;
target.TextDecorations = underline;
Assert.Equal(underline, target.Inlines[0].TextDecorations);
}
}
} }
} }

79
tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

@ -15,6 +15,7 @@ using Avalonia.Platform;
using Avalonia.Rendering; using Avalonia.Rendering;
using Avalonia.Rendering.Composition; using Avalonia.Rendering.Composition;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Moq; using Moq;
using Xunit; using Xunit;
@ -916,6 +917,82 @@ namespace Avalonia.Controls.UnitTests
} }
} }
[Theory]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
public void MaxLines_Sets_ScrollViewer_MaxHeight(int maxLines)
{
using (UnitTestApplication.Start(Services))
{
var target = new TextBox
{
Template = CreateTemplate(),
MaxLines = maxLines,
// Define explicit whole number line height for predictable calculations
LineHeight = 20
};
var impl = CreateMockTopLevelImpl();
var topLevel = new TestTopLevel(impl.Object)
{
Template = CreateTopLevelTemplate(),
Content = target
};
topLevel.ApplyTemplate();
topLevel.LayoutManager.ExecuteInitialLayoutPass();
var textPresenter = target.FindDescendantOfType<TextPresenter>();
Assert.Equal("PART_TextPresenter", textPresenter.Name);
Assert.Equal(new Thickness(0), textPresenter.Margin); // Test assumes no margin on TextPresenter
var scrollViewer = target.FindDescendantOfType<ScrollViewer>();
Assert.Equal("PART_ScrollViewer", scrollViewer.Name);
Assert.Equal(maxLines * target.LineHeight, scrollViewer.MaxHeight);
}
}
[Theory]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
public void MaxLines_Sets_ScrollViewer_MaxHeight_With_TextPresenter_Margin(int maxLines)
{
using (UnitTestApplication.Start(Services))
{
var target = new TextBox
{
Template = CreateTemplate(),
MaxLines = maxLines,
// Define explicit whole number line height for predictable calculations
LineHeight = 20
};
var impl = CreateMockTopLevelImpl();
var topLevel = new TestTopLevel(impl.Object)
{
Template = CreateTopLevelTemplate(),
Content = target
};
topLevel.ApplyTemplate();
topLevel.LayoutManager.ExecuteInitialLayoutPass();
var textPresenter = target.FindDescendantOfType<TextPresenter>();
Assert.Equal("PART_TextPresenter", textPresenter.Name);
var textPresenterMargin = new Thickness(horizontal: 0, vertical: 3);
textPresenter.Margin = textPresenterMargin;
target.InvalidateMeasure();
target.Measure(Size.Infinity);
var scrollViewer = target.FindDescendantOfType<ScrollViewer>();
Assert.Equal("PART_ScrollViewer", scrollViewer.Name);
Assert.Equal((maxLines * target.LineHeight) + textPresenterMargin.Top + textPresenterMargin.Bottom, scrollViewer.MaxHeight);
}
}
[Fact] [Fact]
public void CanUndo_CanRedo_Is_False_When_Initialized() public void CanUndo_CanRedo_Is_False_When_Initialized()
{ {
@ -1125,7 +1202,7 @@ namespace Avalonia.Controls.UnitTests
return new FuncControlTemplate<TextBox>((control, scope) => return new FuncControlTemplate<TextBox>((control, scope) =>
new ScrollViewer new ScrollViewer
{ {
Name = "Part_ScrollViewer", Name = "PART_ScrollViewer",
Template = new FuncControlTemplate<ScrollViewer>(ScrollViewerTests.CreateTemplate), Template = new FuncControlTemplate<ScrollViewer>(ScrollViewerTests.CreateTemplate),
Content = new TextPresenter Content = new TextPresenter
{ {

43
tests/Avalonia.Skia.UnitTests/RenderBoundsTests.cs

@ -0,0 +1,43 @@
using System;
using Avalonia.Controls.Shapes;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Skia.UnitTests
{
public class RenderBoundsTests
{
[Theory,
InlineData("M10 20 L 20 10 L 30 20", PenLineCap.Round, PenLineJoin.Miter, 2, 10,
8.585786819458008, 8.585786819458008, 22.828428268432617, 12.828428268432617),
InlineData("M10 10 L 20 10", PenLineCap.Round, PenLineJoin.Miter,2, 10,
9,9,12,2),
InlineData("M10 10 L 20 15 L 10 20", PenLineCap.Flat, PenLineJoin.Miter, 2, 20,
9.552786827087402, 9.105572700500488, 12.683281898498535, 11.788853645324707)
]
public void RenderBoundsAreCorrectlyCalculated(string path, PenLineCap cap, PenLineJoin join, double thickness, double miterLimit, double x, double y, double width, double height)
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
.With(renderInterface: new PlatformRenderInterface())))
{
var geo = PathGeometry.Parse(path);
var pen = new Pen(Brushes.Black, thickness, null, cap, join, miterLimit);
var bounds = geo.GetRenderBounds(pen);
var tolerance = 0.001;
if (
Math.Abs(bounds.X - x) > tolerance
|| Math.Abs(bounds.Y - y) > tolerance
|| Math.Abs(bounds.Width - width) > tolerance
|| Math.Abs(bounds.Height - height) > tolerance)
Assert.Fail($"Expected {x}:{y}:{width}:{height}, got {bounds}");
Assert.Equal(new Rect(x, y, width, height), bounds);
}
}
}
}

17
tests/Avalonia.UnitTests/CompositorTestServices.cs

@ -89,6 +89,13 @@ public class CompositorTestServices : IDisposable
Events.Rects.Clear(); Events.Rects.Clear();
} }
public void AssertRenderedVisuals(int renderVisuals)
{
RunJobs();
Assert.Equal(Events.RenderedVisuals, renderVisuals);
Events.Rects.Clear();
}
public void AssertHitTest(double x, double y, Func<Visual, bool> filter, params object[] expected) public void AssertHitTest(double x, double y, Func<Visual, bool> filter, params object[] expected)
=> AssertHitTest(new Point(x, y), filter, expected); => AssertHitTest(new Point(x, y), filter, expected);
@ -110,6 +117,13 @@ public class CompositorTestServices : IDisposable
{ {
public List<Rect> Rects = new(); public List<Rect> Rects = new();
public int RenderedVisuals { get; private set; }
public void IncrementRenderedVisuals()
{
RenderedVisuals++;
}
public void RectInvalidated(Rect rc) public void RectInvalidated(Rect rc)
{ {
Rects.Add(rc); Rects.Add(rc);
@ -118,6 +132,7 @@ public class CompositorTestServices : IDisposable
public void Reset() public void Reset()
{ {
Rects.Clear(); Rects.Clear();
RenderedVisuals = 0;
} }
} }
@ -218,4 +233,4 @@ public class DispatcherCompositorScheduler : ICompositorScheduler
{ {
Dispatcher.UIThread.Post(() => compositor.Commit(), DispatcherPriority.UiThreadRender); Dispatcher.UIThread.Post(() => compositor.Commit(), DispatcherPriority.UiThreadRender);
} }
} }

Loading…
Cancel
Save