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
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1-27F5-4255-9AFC-04ABFD11683A}"
ProjectSection(SolutionItems) = preProject
build\ApiDiff.props = build\ApiDiff.props
build\AvaloniaPublicKey.props = build\AvaloniaPublicKey.props
build\Base.props = build\Base.props
build\Binding.props = build\Binding.props

3
NuGet.Config

@ -3,8 +3,7 @@
<configuration>
<packageSources>
<clear />
<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="api.nuget.org" value="https://api.nuget.org/v3/index.json" />
<add key="skiasharp" value="https://aka.ms/skiasharp-eap/index.json" />
</packageSources>
</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;
[[self inputContext] handleEvent:event];
[self keyboardEvent:event withType:KeyDown];
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;
if(Running)
{
Running = false;
if(![NSThread isMainThread])
{
AddRef();
@ -28,22 +27,22 @@ public:
});
return;
};
Running = false;
if(IsApp)
[NSApp stop:nil];
else
{
// Wakeup the event loop
NSEvent* event = [NSEvent otherEventWithType:NSEventTypeApplicationDefined
location:NSMakePoint(0, 0)
modifierFlags:0
timestamp:0
windowNumber:0
context:nil
subtype:0
data1:0
data2:0];
[NSApp postEvent:event atStart:YES];
}
// Wakeup the event loop
NSEvent* event = [NSEvent otherEventWithType:NSEventTypeApplicationDefined
location:NSMakePoint(0, 0)
modifierFlags:0
timestamp:0
windowNumber:0
context:nil
subtype:0
data1:0
data2:0];
[NSApp postEvent:event atStart:YES];
}
};
};

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

@ -15,7 +15,6 @@
<Import Project="..\..\build\Base.props" />
<Import Project="..\..\build\Binding.props" />
<Import Project="..\..\build\System.Memory.props" />
<Import Project="..\..\build\ApiDiff.props" />
<Import Project="..\..\build\NullableEnable.props" />
<Import Project="..\..\build\TrimmingEnable.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 TextRenderingMode TextRenderingMode { get; init; }
public BitmapBlendingMode BitmapBlendingMode { get; init; }
public bool? RequiresFullOpacityHandling { get; init; }
/// <summary>
/// Gets the value of the BitmapInterpolationMode attached property for a visual.
/// </summary>
/// <param name="visual">The control.</param>
/// <returns>The control's left coordinate.</returns>
/// <returns>The value.</returns>
public static BitmapInterpolationMode GetBitmapInterpolationMode(Visual visual)
{
return visual.RenderOptions.BitmapInterpolationMode;
@ -23,7 +24,7 @@ namespace Avalonia.Media
/// Sets the value of the BitmapInterpolationMode attached property for a visual.
/// </summary>
/// <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)
{
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.
/// </summary>
/// <param name="visual">The control.</param>
/// <returns>The control's left coordinate.</returns>
/// <returns>The value.</returns>
public static BitmapBlendingMode GetBitmapBlendingMode(Visual visual)
{
return visual.RenderOptions.BitmapBlendingMode;
@ -53,7 +54,7 @@ namespace Avalonia.Media
/// Gets the value of the EdgeMode attached property for a visual.
/// </summary>
/// <param name="visual">The control.</param>
/// <returns>The control's left coordinate.</returns>
/// <returns>The value.</returns>
public static EdgeMode GetEdgeMode(Visual visual)
{
return visual.RenderOptions.EdgeMode;
@ -63,7 +64,7 @@ namespace Avalonia.Media
/// Sets the value of the EdgeMode attached property for a visual.
/// </summary>
/// <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)
{
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.
/// </summary>
/// <param name="visual">The control.</param>
/// <returns>The control's left coordinate.</returns>
/// <returns>The value.</returns>
public static TextRenderingMode GetTextRenderingMode(Visual visual)
{
return visual.RenderOptions.TextRenderingMode;
@ -83,12 +84,32 @@ namespace Avalonia.Media
/// Sets the value of the TextRenderingMode attached property for a visual.
/// </summary>
/// <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)
{
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)
{
var bitmapInterpolationMode = BitmapInterpolationMode;
@ -119,12 +140,20 @@ namespace Avalonia.Media
bitmapBlendingMode = other.BitmapBlendingMode;
}
var requiresFullOpacityHandling = RequiresFullOpacityHandling;
if (requiresFullOpacityHandling == null)
{
requiresFullOpacityHandling = other.RequiresFullOpacityHandling;
}
return new RenderOptions
{
BitmapInterpolationMode = bitmapInterpolationMode,
EdgeMode = edgeMode,
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!);
}
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
{
int RenderedVisuals { get; }
void IncrementRenderedVisuals();
void RectInvalidated(Rect rc);
}

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

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

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

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

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>
<Import Project="..\..\build\EmbedXaml.props" />
<Import Project="..\..\build\BuildTargets.targets" />
<!--<Import Project="..\..\build\ApiDiff.props" />-->
<Import Project="..\..\build\NullableEnable.props" />
<Import Project="..\..\build\TrimmingEnable.props" />
<Import Project="..\..\build\DevAnalyzers.props" />

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

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

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

@ -13,7 +13,6 @@
</ItemGroup>
<Import Project="..\..\build\EmbedXaml.props" />
<Import Project="..\..\build\BuildTargets.targets" />
<!--<Import Project="..\..\build\ApiDiff.props" />-->
<Import Project="..\..\build\NullableEnable.props" />
<Import Project="..\..\build\TrimmingEnable.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.Remote.Protocol\Avalonia.Remote.Protocol.csproj" />
</ItemGroup>
<Import Project="..\..\build\ApiDiff.props" />
<Import Project="..\..\build\NullableEnable.props" />
<Import Project="..\..\build\TrimmingEnable.props" />
<Import Project="..\..\build\DevAnalyzers.props" />

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

@ -13,9 +13,10 @@ namespace Avalonia.Controls.Documents
/// <summary>
/// AvaloniaProperty for <see cref="TextDecorations" /> property.
/// </summary>
public static readonly StyledProperty<TextDecorationCollection?> TextDecorationsProperty =
AvaloniaProperty.Register<Inline, TextDecorationCollection?>(
nameof(TextDecorations));
public static readonly AttachedProperty<TextDecorationCollection?> TextDecorationsProperty =
AvaloniaProperty.RegisterAttached<Inline, Inline, TextDecorationCollection?>(
nameof(TextDecorations),
inherits: true);
/// <summary>
/// AvaloniaProperty for <see cref="BaselineAlignment" /> property.
@ -43,7 +44,27 @@ namespace Avalonia.Controls.Documents
get { return GetValue(BaselineAlignmentProperty); }
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 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));
IncreaseButtonProperty.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()

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

@ -2,6 +2,7 @@
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Interactivity;
using Avalonia.Styling;
@ -36,8 +37,7 @@ namespace Avalonia.Controls
/// Defines the <see cref="IsChecked"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsCheckedProperty =
AvaloniaProperty.Register<ToggleSplitButton, bool>(
nameof(IsChecked));
AvaloniaProperty.Register<ToggleSplitButton, bool>(nameof(IsChecked), false, defaultBindingMode: BindingMode.TwoWay);
/// <summary>
/// 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.
/// </summary>
public static readonly StyledProperty<TextDecorationCollection?> TextDecorationsProperty =
AvaloniaProperty.Register<TextBlock, TextDecorationCollection?>(nameof(TextDecorations));
Inline.TextDecorationsProperty.AddOwner<TextBlock>();
/// <summary>
/// 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);
}
/// <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>
/// Raises both the <see cref="TextChanging"/> and <see cref="TextChanged"/> events.
/// </summary>
@ -2032,8 +2063,9 @@ namespace Avalonia.Controls
var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch);
var paragraphProperties = TextLayout.CreateTextParagraphProperties(typeface, fontSize, null, default, default, null, default, LineHeight, default);
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);

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

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

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

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

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

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

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

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

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

@ -9,7 +9,6 @@
</ItemGroup>
<Import Project="..\..\build\Rx.props" />
<Import Project="..\..\build\ReactiveUI.props" />
<Import Project="..\..\build\ApiDiff.props" />
<Import Project="..\..\build\NullableEnable.props" />
<Import Project="..\..\build\TrimmingEnable.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\Compatibility\TrimmingAttributes.cs" Visible="False" />
</ItemGroup>
<Import Project="..\..\build\ApiDiff.props" />
<Import Project="..\..\build\TrimmingEnable.props" />
</Project>
</Project>

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

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

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

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

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

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

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

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

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

@ -7,7 +7,6 @@
<ProjectReference Include="..\..\..\packages\Avalonia\Avalonia.csproj" />
</ItemGroup>
<Import Project="..\..\..\build\ApiDiff.props" />
<Import Project="..\..\..\build\DevAnalyzers.props" />
<Import Project="..\..\..\build\TrimmingEnable.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.Markup\Avalonia.Markup.csproj" />
</ItemGroup>
<Import Project="..\..\..\build\ApiDiff.props" />
<Import Project="..\..\..\build\DevAnalyzers.props" />
<Import Project="..\..\..\build\TrimmingEnable.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" />
</ItemGroup>
<Import Project="..\..\..\build\System.Memory.props" />
<Import Project="..\..\..\build\ApiDiff.props" />
<Import Project="..\..\..\build\NullableEnable.props" />
<Import Project="..\..\..\build\DevAnalyzers.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 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.BlendMode = RenderOptions.BitmapBlendingMode.ToSKBlendMode();
@ -375,7 +376,7 @@ namespace Avalonia.Skia
{
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;
if (boxShadow.IsInset)
@ -432,7 +433,7 @@ namespace Avalonia.Skia
{
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 offsetX = (float)boxShadow.OffsetX;
@ -592,8 +593,16 @@ namespace Avalonia.Skia
{
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)
{
var rect = bounds.Value.ToSKRect();
@ -606,7 +615,6 @@ namespace Avalonia.Skia
}
else
{
_opacityStack.Push(_currentOpacity);
_currentOpacity *= opacity;
}
}
@ -616,14 +624,14 @@ namespace Avalonia.Skia
{
CheckLease();
if(_useOpacitySaveLayer)
var useOpacitySaveLayer = _useOpacitySaveLayer || RenderOptions.RequiresFullOpacityHandling == true;
if (useOpacitySaveLayer)
{
Canvas.Restore();
}
else
{
_currentOpacity = _opacityStack.Pop();
}
_currentOpacity = _opacityStack.Pop();
}
/// <inheritdoc />
@ -1239,31 +1247,8 @@ namespace Avalonia.Skia
// 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.
switch (pen.LineCap)
{
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.StrokeCap = pen.LineCap.ToSKStrokeCap();
paint.StrokeJoin = pen.LineJoin.ToSKStrokeJoin();
paint.StrokeMiter = (float) pen.MiterLimit;

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

@ -43,46 +43,11 @@ namespace Avalonia.Skia
/// <inheritdoc />
public bool StrokeContains(IPen? pen, Point point)
{
// Skia requires to compute stroke path to check for point containment.
// 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.
_pathCache.UpdateIfNeeded(StrokePath, pen);
var strokeWidth = (float)(pen?.Thickness ?? 0);
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());
}
return PathContainsCore(_pathCache.ExpandedPath, point);
}
/// <summary>
/// Check Skia path if it contains a point.
/// </summary>
@ -106,14 +71,8 @@ namespace Avalonia.Skia
/// <inheritdoc />
public Rect GetRenderBounds(IPen? pen)
{
var strokeWidth = (float)(pen?.Thickness ?? 0);
if (!_pathCache.HasCacheFor(strokeWidth))
{
UpdatePathCache(strokeWidth);
}
return _pathCache.CachedGeometryRenderBounds;
_pathCache.UpdateIfNeeded(StrokePath, pen);
return _pathCache.RenderBounds;
}
/// <inheritdoc />
@ -180,66 +139,70 @@ namespace Avalonia.Skia
/// </summary>
protected void InvalidateCaches()
{
_pathCache.Invalidate();
_pathCache.Dispose();
_pathCache = default;
}
private struct PathCache
{
private float _cachedStrokeWidth;
/// <summary>
/// Tolerance for two stroke widths to be deemed equal
/// </summary>
public const float Tolerance = float.Epsilon;
/// <summary>
/// Cached contour path.
/// </summary>
public SKPath? CachedStrokePath { get; private set; }
/// <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)
private double _width, _miterLimit;
private PenLineCap _cap;
private PenLineJoin _join;
private SKPath? _path, _cachedFor;
private Rect? _renderBounds;
private static readonly SKPath s_emptyPath = new();
public Rect RenderBounds => _renderBounds ??= (_path ?? _cachedFor ?? s_emptyPath).Bounds.ToAvaloniaRect();
public SKPath ExpandedPath => _path ?? s_emptyPath;
public void UpdateIfNeeded(SKPath? strokePath, IPen? pen)
{
return CachedStrokePath != null && Math.Abs(_cachedStrokeWidth - strokeWidth) < Tolerance;
}
/// <summary>
/// Cache path for given stroke width. Takes ownership of a passed path.
/// </summary>
/// <param name="path">Path to cache.</param>
/// <param name="strokeWidth">Stroke width to cache.</param>
/// <param name="geometryRenderBounds">Render bounds to use.</param>
public void Cache(SKPath path, float strokeWidth, Rect geometryRenderBounds)
{
if (CachedStrokePath != path)
var strokeWidth = pen?.Thickness ?? 0;
var miterLimit = pen?.MiterLimit ?? 0;
var cap = pen?.LineCap ?? default;
var join = pen?.LineJoin ?? default;
if (_cachedFor == strokePath
&& _path != null
&& cap == _cap
&& join == _join
&& Math.Abs(_width - strokeWidth) < float.Epsilon
&& (join != PenLineJoin.Miter || Math.Abs(_miterLimit - miterLimit) > float.Epsilon))
// 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;
CachedGeometryRenderBounds = geometryRenderBounds;
_cachedStrokeWidth = strokeWidth;
var paint = SKPaintCache.Shared.Get();
paint.IsStroke = true;
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>
/// Invalidate cache state.
/// </summary>
public void Invalidate()
public void Dispose()
{
CachedStrokePath?.Dispose();
CachedStrokePath = null;
CachedGeometryRenderBounds = default;
_cachedStrokeWidth = default;
_path?.Dispose();
_path = null;
}
}
}
}

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)
{
switch (a)

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

@ -1,6 +1,10 @@
using System;
using Avalonia.Logging;
using Avalonia.Media;
using Avalonia.Platform;
using SharpDX.Direct2D1;
using Geometry = SharpDX.Direct2D1.Geometry;
using PathGeometry = SharpDX.Direct2D1.PathGeometry;
namespace Avalonia.Direct2D1.Media
{
@ -27,7 +31,20 @@ namespace Avalonia.Direct2D1.Media
/// <inheritdoc/>
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/>

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

@ -683,9 +683,10 @@ namespace Avalonia.Win32
if (parentHwnd == IntPtr.Zero && !_windowProperties.ShowInTaskbar)
{
parentHwnd = OffscreenParentWindow.Handle;
_hiddenWindowIsParent = true;
}
_hiddenWindowIsParent = parentHwnd == OffscreenParentWindow.Handle;
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);
}
}
[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.Composition;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Moq;
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]
public void CanUndo_CanRedo_Is_False_When_Initialized()
{
@ -1125,7 +1202,7 @@ namespace Avalonia.Controls.UnitTests
return new FuncControlTemplate<TextBox>((control, scope) =>
new ScrollViewer
{
Name = "Part_ScrollViewer",
Name = "PART_ScrollViewer",
Template = new FuncControlTemplate<ScrollViewer>(ScrollViewerTests.CreateTemplate),
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();
}
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)
=> AssertHitTest(new Point(x, y), filter, expected);
@ -110,6 +117,13 @@ public class CompositorTestServices : IDisposable
{
public List<Rect> Rects = new();
public int RenderedVisuals { get; private set; }
public void IncrementRenderedVisuals()
{
RenderedVisuals++;
}
public void RectInvalidated(Rect rc)
{
Rects.Add(rc);
@ -118,6 +132,7 @@ public class CompositorTestServices : IDisposable
public void Reset()
{
Rects.Clear();
RenderedVisuals = 0;
}
}
@ -218,4 +233,4 @@ public class DispatcherCompositorScheduler : ICompositorScheduler
{
Dispatcher.UIThread.Post(() => compositor.Commit(), DispatcherPriority.UiThreadRender);
}
}
}

Loading…
Cancel
Save