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((x, e) => x.ThumbChanged(e)); IncreaseButtonProperty.Changed.AddClassHandler((x, e) => x.ButtonChanged(e)); DecreaseButtonProperty.Changed.AddClassHandler((x, e) => x.ButtonChanged(e)); - AffectsArrange(MinimumProperty, MaximumProperty, ValueProperty, OrientationProperty); + AffectsArrange(IsDirectionReversedProperty, MinimumProperty, MaximumProperty, ValueProperty, OrientationProperty); } public Track() diff --git a/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs b/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs index ff8bd5dc0b..c493445ba1 100644 --- a/src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs +++ b/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 property. /// 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 +}