diff --git a/Avalonia.sln b/Avalonia.sln
index d5419365ac..98ad0cadae 100644
--- a/Avalonia.sln
+++ b/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
diff --git a/NuGet.Config b/NuGet.Config
index 7d2bd8abd2..2042fea360 100644
--- a/NuGet.Config
+++ b/NuGet.Config
@@ -3,8 +3,7 @@
-
-
+
diff --git a/build/ApiDiff.props b/build/ApiDiff.props
deleted file mode 100644
index b0b1942f60..0000000000
--- a/build/ApiDiff.props
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
- 0.10.0
- $(PackageId)
- Avalonia
- false
-
-
-
-
-
-
-
diff --git a/native/Avalonia.Native/src/OSX/AvnView.mm b/native/Avalonia.Native/src/OSX/AvnView.mm
index a4999b8df3..ea6ba93fdb 100644
--- a/native/Avalonia.Native/src/OSX/AvnView.mm
+++ b/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];
}
}
diff --git a/native/Avalonia.Native/src/OSX/platformthreading.mm b/native/Avalonia.Native/src/OSX/platformthreading.mm
index d80df68fea..56b7ce97e0 100644
--- a/native/Avalonia.Native/src/OSX/platformthreading.mm
+++ b/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];
}
};
};
diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj
index 16eb09de65..9aeb71f649 100644
--- a/src/Avalonia.Base/Avalonia.Base.csproj
+++ b/src/Avalonia.Base/Avalonia.Base.csproj
@@ -15,7 +15,6 @@
-
diff --git a/src/Avalonia.Base/Media/RenderOptions.cs b/src/Avalonia.Base/Media/RenderOptions.cs
index 639498543b..1ac2520919 100644
--- a/src/Avalonia.Base/Media/RenderOptions.cs
+++ b/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; }
///
/// Gets the value of the BitmapInterpolationMode attached property for a visual.
///
/// The control.
- /// The control's left coordinate.
+ /// The value.
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.
///
/// The control.
- /// The left value.
+ /// The value.
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.
///
/// The control.
- /// The control's left coordinate.
+ /// The value.
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.
///
/// The control.
- /// The control's left coordinate.
+ /// The value.
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.
///
/// The control.
- /// The left value.
+ /// The value.
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.
///
/// The control.
- /// The control's left coordinate.
+ /// The value.
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.
///
/// The control.
- /// The left value.
+ /// The value.
public static void SetTextRenderingMode(Visual visual, TextRenderingMode value)
{
visual.RenderOptions = visual.RenderOptions with { TextRenderingMode = value };
}
+ ///
+ /// Gets the value of the RequiresFullOpacityHandling attached property for a visual.
+ ///
+ /// The control.
+ /// The value.
+ public static bool? GetRequiresFullOpacityHandling(Visual visual)
+ {
+ return visual.RenderOptions.RequiresFullOpacityHandling;
+ }
+
+ ///
+ /// Sets the value of the RequiresFullOpacityHandling attached property for a visual.
+ ///
+ /// The control.
+ /// The value.
+ 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
};
}
}
diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataGeometryNode.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataGeometryNode.cs
index 67231c9012..5a40a6b279 100644
--- a/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataGeometryNode.cs
+++ b/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;
}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Rendering/Composition/ICompositionTargetDebugEvents.cs b/src/Avalonia.Base/Rendering/Composition/ICompositionTargetDebugEvents.cs
index c830ca2c49..27aca436b8 100644
--- a/src/Avalonia.Base/Rendering/Composition/ICompositionTargetDebugEvents.cs
+++ b/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);
}
diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.DirtyProperties.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.DirtyProperties.cs
index c1037d5c67..51414c2250 100644
--- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.DirtyProperties.cs
+++ b/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)
diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
index 45515a37e2..aeb228282e 100644
--- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
+++ b/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;
}
diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GeometryBoundsHelper.cs b/src/Avalonia.Base/Rendering/SceneGraph/GeometryBoundsHelper.cs
deleted file mode 100644
index b1129e81c4..0000000000
--- a/src/Avalonia.Base/Rendering/SceneGraph/GeometryBoundsHelper.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-using System;
-using Avalonia.Media;
-using Avalonia.Utilities;
-
-namespace Avalonia.Rendering.SceneGraph;
-
-internal static class GeometryBoundsHelper
-{
- ///
- /// Calculates the bounds of a given geometry with respect to the pens
- ///
- /// The calculated bounds without s
- /// The pen with information about the s
- ///
- 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();
- }
- }
-}
diff --git a/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj b/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj
index 5a31053bdc..8a36e6900a 100644
--- a/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj
+++ b/src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj
@@ -17,7 +17,6 @@
-
diff --git a/src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj b/src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj
index 6556ce721e..6c1e019603 100644
--- a/src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj
+++ b/src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj
@@ -14,7 +14,6 @@
-
diff --git a/src/Avalonia.Controls.ItemsRepeater/Avalonia.Controls.ItemsRepeater.csproj b/src/Avalonia.Controls.ItemsRepeater/Avalonia.Controls.ItemsRepeater.csproj
index 1ec0ee33a7..b7739bda8d 100644
--- a/src/Avalonia.Controls.ItemsRepeater/Avalonia.Controls.ItemsRepeater.csproj
+++ b/src/Avalonia.Controls.ItemsRepeater/Avalonia.Controls.ItemsRepeater.csproj
@@ -13,7 +13,6 @@
-
diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj
index 304454777f..0dda861448 100644
--- a/src/Avalonia.Controls/Avalonia.Controls.csproj
+++ b/src/Avalonia.Controls/Avalonia.Controls.csproj
@@ -6,7 +6,6 @@
-
diff --git a/src/Avalonia.Controls/Documents/Inline.cs b/src/Avalonia.Controls/Documents/Inline.cs
index 23b806583e..a6fe57e66e 100644
--- a/src/Avalonia.Controls/Documents/Inline.cs
+++ b/src/Avalonia.Controls/Documents/Inline.cs
@@ -13,9 +13,10 @@ namespace Avalonia.Controls.Documents
///
/// AvaloniaProperty for property.
///
- public static readonly StyledProperty TextDecorationsProperty =
- AvaloniaProperty.Register(
- nameof(TextDecorations));
+ public static readonly AttachedProperty TextDecorationsProperty =
+ AvaloniaProperty.RegisterAttached(
+ nameof(TextDecorations),
+ inherits: true);
///
/// AvaloniaProperty for property.
@@ -43,7 +44,27 @@ namespace Avalonia.Controls.Documents
get { return GetValue(BaselineAlignmentProperty); }
set { SetValue(BaselineAlignmentProperty, value); }
}
+
+ ///
+ /// Gets the value of the attached on a control.
+ ///
+ /// The control.
+ /// The font style.
+ public static TextDecorationCollection? GetTextDecorations(Control control)
+ {
+ return control.GetValue(TextDecorationsProperty);
+ }
+ ///
+ /// Sets the value of the attached on a control.
+ ///
+ /// The control.
+ /// The property value to set.
+ public static void SetTextDecorations(Control control, TextDecorationCollection? value)
+ {
+ control.SetValue(TextDecorationsProperty, value);
+ }
+
internal abstract void BuildTextRun(IList textRuns);
internal abstract void AppendText(StringBuilder stringBuilder);
diff --git a/src/Avalonia.Controls/Primitives/Track.cs b/src/Avalonia.Controls/Primitives/Track.cs
index f9aa5fc7a0..d823455e13 100644
--- a/src/Avalonia.Controls/Primitives/Track.cs
+++ b/src/Avalonia.Controls/Primitives/Track.cs
@@ -52,7 +52,7 @@ namespace Avalonia.Controls.Primitives
ThumbProperty.Changed.AddClassHandler
public static readonly StyledProperty IsCheckedProperty =
- AvaloniaProperty.Register(
- nameof(IsChecked));
+ AvaloniaProperty.Register(nameof(IsChecked), false, defaultBindingMode: BindingMode.TwoWay);
///
/// Initializes a new instance of the class.
diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs
index ea420c7c45..cc92bdd752 100644
--- a/src/Avalonia.Controls/TextBlock.cs
+++ b/src/Avalonia.Controls/TextBlock.cs
@@ -135,7 +135,7 @@ namespace Avalonia.Controls
/// Defines the property.
///
public static readonly StyledProperty TextDecorationsProperty =
- AvaloniaProperty.Register(nameof(TextDecorations));
+ Inline.TextDecorationsProperty.AddOwner();
///
/// Defines the property.
diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs
index 98b3b13c17..8a5ac4e495 100644
--- a/src/Avalonia.Controls/TextBox.cs
+++ b/src/Avalonia.Controls/TextBox.cs
@@ -1879,6 +1879,37 @@ namespace Avalonia.Controls
return text.Substring(start, end - start);
}
+ ///
+ /// Returns the sum of any vertical whitespace added between the and in the control template.
+ ///
+ /// The total vertical whitespace.
+ 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(Layoutable.MarginProperty);
+ var padding = visual.GetValue(Decorator.PaddingProperty);
+
+ verticalSpace += margin.Top + padding.Top + padding.Bottom + margin.Bottom;
+
+ visual = visual.VisualParent;
+ }
+ }
+
+ return verticalSpace;
+ }
+
///
/// Raises both the and events.
///
@@ -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);
diff --git a/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj b/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj
index c5255b22cd..0c1b3e8a45 100644
--- a/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj
+++ b/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj
@@ -19,5 +19,4 @@
-
diff --git a/src/Avalonia.Desktop/Avalonia.Desktop.csproj b/src/Avalonia.Desktop/Avalonia.Desktop.csproj
index d180e6e34d..12cc977c84 100644
--- a/src/Avalonia.Desktop/Avalonia.Desktop.csproj
+++ b/src/Avalonia.Desktop/Avalonia.Desktop.csproj
@@ -12,7 +12,6 @@
-
diff --git a/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj b/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj
index 65d1bea298..135bc1b685 100644
--- a/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj
+++ b/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj
@@ -24,7 +24,6 @@
-
diff --git a/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj b/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj
index be4bae26f1..3a2c18ac24 100644
--- a/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj
+++ b/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj
@@ -19,7 +19,6 @@
-
diff --git a/src/Avalonia.ReactiveUI/Avalonia.ReactiveUI.csproj b/src/Avalonia.ReactiveUI/Avalonia.ReactiveUI.csproj
index 4cae8e82df..30b5d80fbc 100644
--- a/src/Avalonia.ReactiveUI/Avalonia.ReactiveUI.csproj
+++ b/src/Avalonia.ReactiveUI/Avalonia.ReactiveUI.csproj
@@ -9,7 +9,6 @@
-
diff --git a/src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj b/src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj
index baeb8820f8..13d1ff9651 100644
--- a/src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj
+++ b/src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj
@@ -10,6 +10,5 @@
-
-
\ No newline at end of file
+
diff --git a/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj b/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj
index 660661fc94..e62abff18b 100644
--- a/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj
+++ b/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj
@@ -11,7 +11,6 @@
-
diff --git a/src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj b/src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj
index 39da9a747f..9864cf24e0 100644
--- a/src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj
+++ b/src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj
@@ -11,7 +11,6 @@
-
diff --git a/src/Headless/Avalonia.Headless.NUnit/Avalonia.Headless.NUnit.csproj b/src/Headless/Avalonia.Headless.NUnit/Avalonia.Headless.NUnit.csproj
index 49f1de31f2..d176d74b5f 100644
--- a/src/Headless/Avalonia.Headless.NUnit/Avalonia.Headless.NUnit.csproj
+++ b/src/Headless/Avalonia.Headless.NUnit/Avalonia.Headless.NUnit.csproj
@@ -12,8 +12,7 @@
-
-
+
diff --git a/src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj b/src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj
index 1f06f28687..2020dca482 100644
--- a/src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj
+++ b/src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj
@@ -10,7 +10,6 @@
-
diff --git a/src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj b/src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj
index 4ab70eb07d..85879faa4c 100644
--- a/src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj
+++ b/src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj
@@ -13,7 +13,6 @@
-
diff --git a/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj b/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj
index 893cb0074c..fe071b594f 100644
--- a/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj
+++ b/src/Headless/Avalonia.Headless/Avalonia.Headless.csproj
@@ -7,7 +7,6 @@
-
diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
index 618379757b..d827530207 100644
--- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
+++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
@@ -65,7 +65,6 @@
-
diff --git a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj
index d041e7d2e6..29583b6cb8 100644
--- a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj
+++ b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj
@@ -13,7 +13,6 @@
-
diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
index fbff4ab4e7..76d236e18a 100644
--- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
+++ b/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();
}
///
@@ -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;
diff --git a/src/Skia/Avalonia.Skia/GeometryImpl.cs b/src/Skia/Avalonia.Skia/GeometryImpl.cs
index aee84d1346..c1ce4a661f 100644
--- a/src/Skia/Avalonia.Skia/GeometryImpl.cs
+++ b/src/Skia/Avalonia.Skia/GeometryImpl.cs
@@ -43,46 +43,11 @@ namespace Avalonia.Skia
///
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);
- }
-
- ///
- /// Update path cache for given stroke width.
- ///
- /// Stroke width.
- 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);
}
-
+
///
/// Check Skia path if it contains a point.
///
@@ -106,14 +71,8 @@ namespace Avalonia.Skia
///
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;
}
///
@@ -180,66 +139,70 @@ namespace Avalonia.Skia
///
protected void InvalidateCaches()
{
- _pathCache.Invalidate();
+ _pathCache.Dispose();
+ _pathCache = default;
}
private struct PathCache
{
- private float _cachedStrokeWidth;
-
- ///
- /// Tolerance for two stroke widths to be deemed equal
- ///
- public const float Tolerance = float.Epsilon;
-
- ///
- /// Cached contour path.
- ///
- public SKPath? CachedStrokePath { get; private set; }
-
- ///
- /// Cached geometry render bounds.
- ///
- public Rect CachedGeometryRenderBounds { get; private set; }
-
- ///
- /// Is cached valid for given stroke width.
- ///
- /// Stroke width to check.
- /// True, if CachedStrokePath can be used for given stroke width.
- 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;
- }
-
- ///
- /// Cache path for given stroke width. Takes ownership of a passed path.
- ///
- /// Path to cache.
- /// Stroke width to cache.
- /// Render bounds to use.
- 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);
}
- ///
- /// Invalidate cache state.
- ///
- public void Invalidate()
+ public void Dispose()
{
- CachedStrokePath?.Dispose();
- CachedStrokePath = null;
- CachedGeometryRenderBounds = default;
- _cachedStrokeWidth = default;
+ _path?.Dispose();
+ _path = null;
}
+
}
}
}
diff --git a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs
index 455e415ce1..44fe7aed89 100644
--- a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs
+++ b/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)
diff --git a/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs
index 9a93d1afd3..e1c08e0814 100644
--- a/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs
+++ b/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
///
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();
+ }
}
///
diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs
index 154b20ef84..e9156e553d 100644
--- a/src/Windows/Avalonia.Win32/WindowImpl.cs
+++ b/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);
}
diff --git a/tests/Avalonia.Base.UnitTests/Rendering/CompositorInvalidationClippingTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/CompositorInvalidationClippingTests.cs
new file mode 100644
index 0000000000..c158ff4e75
--- /dev/null
+++ b/tests/Avalonia.Base.UnitTests/Rendering/CompositorInvalidationClippingTests.cs
@@ -0,0 +1,68 @@
+using Avalonia.Controls;
+using Avalonia.Media;
+using Xunit;
+
+namespace Avalonia.Base.UnitTests.Rendering;
+///
+/// Test class that verifies how clipping influences rendering in the compositor
+///
+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);
+ }
+ }
+}
diff --git a/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs b/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs
index eb1d6f5ea4..fd3d80729a 100644
--- a/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs
+++ b/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);
+ }
+ }
}
}
diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
index d4558c9e04..3c7fb6f9c0 100644
--- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
+++ b/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();
+ Assert.Equal("PART_TextPresenter", textPresenter.Name);
+ Assert.Equal(new Thickness(0), textPresenter.Margin); // Test assumes no margin on TextPresenter
+
+ var scrollViewer = target.FindDescendantOfType();
+ 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();
+ 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();
+ 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((control, scope) =>
new ScrollViewer
{
- Name = "Part_ScrollViewer",
+ Name = "PART_ScrollViewer",
Template = new FuncControlTemplate(ScrollViewerTests.CreateTemplate),
Content = new TextPresenter
{
diff --git a/tests/Avalonia.Skia.UnitTests/RenderBoundsTests.cs b/tests/Avalonia.Skia.UnitTests/RenderBoundsTests.cs
new file mode 100644
index 0000000000..a659c2b8b2
--- /dev/null
+++ b/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);
+ }
+ }
+ }
+}
diff --git a/tests/Avalonia.UnitTests/CompositorTestServices.cs b/tests/Avalonia.UnitTests/CompositorTestServices.cs
index 53fd610a17..00645259a5 100644
--- a/tests/Avalonia.UnitTests/CompositorTestServices.cs
+++ b/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 filter, params object[] expected)
=> AssertHitTest(new Point(x, y), filter, expected);
@@ -110,6 +117,13 @@ public class CompositorTestServices : IDisposable
{
public List 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);
}
-}
\ No newline at end of file
+}