diff --git a/src/Avalonia.Base/Layout/LayoutHelper.cs b/src/Avalonia.Base/Layout/LayoutHelper.cs
index c6cbda62557..a342c654f9c 100644
--- a/src/Avalonia.Base/Layout/LayoutHelper.cs
+++ b/src/Avalonia.Base/Layout/LayoutHelper.cs
@@ -1,4 +1,6 @@
using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
using Avalonia.Utilities;
using Avalonia.VisualTree;
@@ -25,21 +27,20 @@ public static class LayoutHelper
/// The space available for the control.
/// The control's size.
public static Size ApplyLayoutConstraints(Layoutable control, Size constraints)
- {
- var minmax = new MinMax(control);
+ => ApplyLayoutConstraints(new MinMax(control), constraints);
- return new Size(
- MathUtilities.Clamp(constraints.Width, minmax.MinWidth, minmax.MaxWidth),
- MathUtilities.Clamp(constraints.Height, minmax.MinHeight, minmax.MaxHeight));
- }
+ internal static Size ApplyLayoutConstraints(MinMax minMax, Size constraints)
+ => new(
+ MathUtilities.Clamp(constraints.Width, minMax.MinWidth, minMax.MaxWidth),
+ MathUtilities.Clamp(constraints.Height, minMax.MinHeight, minMax.MaxHeight));
public static Size MeasureChild(Layoutable? control, Size availableSize, Thickness padding,
Thickness borderThickness)
{
if (IsParentLayoutRounded(control, out double scale))
{
- padding = RoundLayoutThickness(padding, scale, scale);
- borderThickness = RoundLayoutThickness(borderThickness, scale, scale);
+ padding = RoundLayoutThickness(padding, scale);
+ borderThickness = RoundLayoutThickness(borderThickness, scale);
}
if (control != null)
@@ -55,7 +56,7 @@ public static Size MeasureChild(Layoutable? control, Size availableSize, Thickne
{
if (IsParentLayoutRounded(control, out double scale))
{
- padding = RoundLayoutThickness(padding, scale, scale);
+ padding = RoundLayoutThickness(padding, scale);
}
if (control != null)
@@ -71,8 +72,8 @@ public static Size ArrangeChild(Layoutable? child, Size availableSize, Thickness
{
if (IsParentLayoutRounded(child, out double scale))
{
- padding = RoundLayoutThickness(padding, scale, scale);
- borderThickness = RoundLayoutThickness(borderThickness, scale, scale);
+ padding = RoundLayoutThickness(padding, scale);
+ borderThickness = RoundLayoutThickness(borderThickness, scale);
}
return ArrangeChildInternal(child, availableSize, padding + borderThickness);
@@ -81,7 +82,7 @@ public static Size ArrangeChild(Layoutable? child, Size availableSize, Thickness
public static Size ArrangeChild(Layoutable? child, Size availableSize, Thickness padding)
{
if(IsParentLayoutRounded(child, out double scale))
- padding = RoundLayoutThickness(padding, scale, scale);
+ padding = RoundLayoutThickness(padding, scale);
return ArrangeChildInternal(child, availableSize, padding);
}
@@ -140,18 +141,7 @@ void InnerInvalidateMeasure(Visual target)
/// The control.
/// Thrown when control has no root or returned layout scaling is invalid.
public static double GetLayoutScale(Layoutable control)
- {
- var visualRoot = (control as Visual)?.VisualRoot;
-
- var result = (visualRoot as ILayoutRoot)?.LayoutScaling ?? 1.0;
-
- if (result == 0 || double.IsNaN(result) || double.IsInfinity(result))
- {
- throw new Exception($"Invalid LayoutScaling returned from {visualRoot!.GetType()}");
- }
-
- return result;
- }
+ => control.VisualRoot is ILayoutRoot layoutRoot ? layoutRoot.LayoutScaling : 1.0;
///
/// Rounds a size to integer values for layout purposes, compensating for high DPI screen
@@ -172,6 +162,20 @@ public static Size RoundLayoutSizeUp(Size size, double dpiScaleX, double dpiScal
return new Size(RoundLayoutValueUp(size.Width, dpiScaleX), RoundLayoutValueUp(size.Height, dpiScaleY));
}
+ [SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator", Justification = "The DPI scale should have been normalized.")]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static Size RoundLayoutSizeUp(Size size, double dpiScale)
+ {
+ // If DPI == 1, don't use DPI-aware rounding.
+ return dpiScale == 1.0 ?
+ new Size(
+ Math.Ceiling(size.Width),
+ Math.Ceiling(size.Height)) :
+ new Size(
+ Math.Ceiling(RoundTo8Digits(size.Width) * dpiScale) / dpiScale,
+ Math.Ceiling(RoundTo8Digits(size.Height) * dpiScale) / dpiScale);
+ }
+
///
/// Rounds a thickness to integer values for layout purposes, compensating for high DPI screen
/// coordinates.
@@ -196,6 +200,38 @@ public static Thickness RoundLayoutThickness(Thickness thickness, double dpiScal
);
}
+ [SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator", Justification = "The DPI scale should have been normalized.")]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static Thickness RoundLayoutThickness(Thickness thickness, double dpiScale)
+ {
+ // If DPI == 1, don't use DPI-aware rounding.
+ return dpiScale == 1.0 ?
+ new Thickness(
+ Math.Round(thickness.Left),
+ Math.Round(thickness.Top),
+ Math.Round(thickness.Right),
+ Math.Round(thickness.Bottom)) :
+ new Thickness(
+ Math.Round(thickness.Left * dpiScale) / dpiScale,
+ Math.Round(thickness.Top * dpiScale) / dpiScale,
+ Math.Round(thickness.Right * dpiScale) / dpiScale,
+ Math.Round(thickness.Bottom * dpiScale) / dpiScale);
+ }
+
+ [SuppressMessage("ReSharper", "CompareOfFloatsByEqualityOperator", Justification = "The DPI scale should have been normalized.")]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static Point RoundLayoutPoint(Point point, double dpiScale)
+ {
+ // If DPI == 1, don't use DPI-aware rounding.
+ return dpiScale == 1.0 ?
+ new Point(
+ Math.Round(point.X),
+ Math.Round(point.Y)) :
+ new Point(
+ Math.Round(point.X * dpiScale) / dpiScale,
+ Math.Round(point.Y * dpiScale) / dpiScale);
+ }
+
///
/// Calculates the value to be used for layout rounding at high DPI by rounding the value
/// up or down to the nearest pixel.
@@ -211,28 +247,10 @@ public static Thickness RoundLayoutThickness(Thickness thickness, double dpiScal
///
public static double RoundLayoutValue(double value, double dpiScale)
{
- double newValue;
-
// If DPI == 1, don't use DPI-aware rounding.
- if (!MathUtilities.IsOne(dpiScale))
- {
- newValue = Math.Round(value * dpiScale) / dpiScale;
-
- // If rounding produces a value unacceptable to layout (NaN, Infinity or MaxValue),
- // use the original value.
- if (double.IsNaN(newValue) ||
- double.IsInfinity(newValue) ||
- MathUtilities.AreClose(newValue, double.MaxValue))
- {
- newValue = value;
- }
- }
- else
- {
- newValue = Math.Round(value);
- }
-
- return newValue;
+ return MathUtilities.IsOne(dpiScale) ?
+ Math.Round(value) :
+ Math.Round(value * dpiScale) / dpiScale;
}
///
@@ -250,73 +268,25 @@ public static double RoundLayoutValue(double value, double dpiScale)
///
public static double RoundLayoutValueUp(double value, double dpiScale)
{
- double newValue;
+ // If DPI == 1, don't use DPI-aware rounding.
+ return MathUtilities.IsOne(dpiScale) ?
+ Math.Ceiling(value) :
+ Math.Ceiling(RoundTo8Digits(value) * dpiScale) / dpiScale;
+ }
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static double RoundTo8Digits(double value)
+ {
// Round the value to avoid FP errors. This is needed because if `value` has a floating
// point precision error (e.g. 79.333333333333343) then when it's multiplied by
// `dpiScale` and rounded up, it will be rounded up to a value one greater than it
// should be.
#if NET6_0_OR_GREATER
- value = Math.Round(value, 8, MidpointRounding.ToZero);
+ return Math.Round(value, 8, MidpointRounding.ToZero);
#else
// MidpointRounding.ToZero isn't available in netstandard2.0.
- value = Math.Truncate(value * 1e8) / 1e8;
+ return Math.Truncate(value * 1e8) / 1e8;
#endif
-
- // If DPI == 1, don't use DPI-aware rounding.
- if (!MathUtilities.IsOne(dpiScale))
- {
- newValue = Math.Ceiling(value * dpiScale) / dpiScale;
-
- // If rounding produces a value unacceptable to layout (NaN, Infinity or MaxValue),
- // use the original value.
- if (double.IsNaN(newValue) ||
- double.IsInfinity(newValue) ||
- MathUtilities.AreClose(newValue, double.MaxValue))
- {
- newValue = value;
- }
- }
- else
- {
- newValue = Math.Ceiling(value);
- }
-
- return newValue;
- }
-
- ///
- /// Calculates the min and max height for a control. Ported from WPF.
- ///
- private readonly struct MinMax
- {
- public MinMax(Layoutable e)
- {
- MaxHeight = e.MaxHeight;
- MinHeight = e.MinHeight;
- double l = e.Height;
-
- double height = (double.IsNaN(l) ? double.PositiveInfinity : l);
- MaxHeight = Math.Max(Math.Min(height, MaxHeight), MinHeight);
-
- height = (double.IsNaN(l) ? 0 : l);
- MinHeight = Math.Max(Math.Min(MaxHeight, height), MinHeight);
-
- MaxWidth = e.MaxWidth;
- MinWidth = e.MinWidth;
- l = e.Width;
-
- double width = (double.IsNaN(l) ? double.PositiveInfinity : l);
- MaxWidth = Math.Max(Math.Min(width, MaxWidth), MinWidth);
-
- width = (double.IsNaN(l) ? 0 : l);
- MinWidth = Math.Max(Math.Min(MaxWidth, width), MinWidth);
- }
-
- public double MinWidth { get; }
- public double MaxWidth { get; }
- public double MinHeight { get; }
- public double MaxHeight { get; }
}
}
}
diff --git a/src/Avalonia.Base/Layout/Layoutable.cs b/src/Avalonia.Base/Layout/Layoutable.cs
index a71ceb76afa..ad39127cf48 100644
--- a/src/Avalonia.Base/Layout/Layoutable.cs
+++ b/src/Avalonia.Base/Layout/Layoutable.cs
@@ -2,6 +2,7 @@
using Avalonia.Diagnostics;
using Avalonia.Logging;
using Avalonia.Reactive;
+using Avalonia.Utilities;
using Avalonia.VisualTree;
#nullable enable
@@ -544,53 +545,43 @@ protected virtual Size MeasureCore(Size availableSize)
if (useLayoutRounding)
{
scale = LayoutHelper.GetLayoutScale(this);
- margin = LayoutHelper.RoundLayoutThickness(margin, scale, scale);
+ margin = LayoutHelper.RoundLayoutThickness(margin, scale);
}
ApplyStyling();
ApplyTemplate();
+ var minMax = new MinMax(this);
+
var constrained = LayoutHelper.ApplyLayoutConstraints(
- this,
+ minMax,
availableSize.Deflate(margin));
var measured = MeasureOverride(constrained);
- var width = measured.Width;
- var height = measured.Height;
+ var width = MathUtilities.Clamp(measured.Width, minMax.MinWidth, minMax.MaxWidth);
+ var height = MathUtilities.Clamp(measured.Height, minMax.MinHeight, minMax.MaxHeight);
+ if (useLayoutRounding)
{
- double widthCache = Width;
-
- if (!double.IsNaN(widthCache))
- {
- width = widthCache;
- }
+ (width, height) = LayoutHelper.RoundLayoutSizeUp(new Size(width, height), scale);
}
- width = Math.Min(width, MaxWidth);
- width = Math.Max(width, MinWidth);
+ if (width > availableSize.Width)
+ width = availableSize.Width;
- {
- double heightCache = Height;
+ if (height > availableSize.Height)
+ height = availableSize.Height;
- if (!double.IsNaN(heightCache))
- {
- height = heightCache;
- }
- }
+ width += margin.Left + margin.Right;
+ height += margin.Top + margin.Bottom;
- height = Math.Min(height, MaxHeight);
- height = Math.Max(height, MinHeight);
+ if (width < 0)
+ width = 0;
- if (useLayoutRounding)
- {
- (width, height) = LayoutHelper.RoundLayoutSizeUp(new Size(width, height), scale, scale);
- }
-
- width = Math.Min(width, availableSize.Width);
- height = Math.Min(height, availableSize.Height);
+ if (height < 0)
+ height = 0;
- return NonNegative(new Size(width, height).Inflate(margin));
+ return new Size(width, height);
}
else
{
@@ -618,8 +609,13 @@ protected virtual Size MeasureOverride(Size availableSize)
if (visual is Layoutable layoutable)
{
layoutable.Measure(availableSize);
- width = Math.Max(width, layoutable.DesiredSize.Width);
- height = Math.Max(height, layoutable.DesiredSize.Height);
+ var childSize = layoutable.DesiredSize;
+
+ if (childSize.Width > width)
+ width = childSize.Width;
+
+ if (childSize.Height > height)
+ height = childSize.Height;
}
}
@@ -650,12 +646,19 @@ protected virtual void ArrangeCore(Rect finalRect)
// If the margin isn't pre-rounded some sizes will be offset by 1 pixel in certain scales.
if (useLayoutRounding)
{
- margin = LayoutHelper.RoundLayoutThickness(margin, scale, scale);
+ margin = LayoutHelper.RoundLayoutThickness(margin, scale);
}
- var availableSizeMinusMargins = new Size(
- Math.Max(0, finalRect.Width - margin.Left - margin.Right),
- Math.Max(0, finalRect.Height - margin.Top - margin.Bottom));
+
+ var availableWidthMinusMargins = finalRect.Width - margin.Left - margin.Right;
+ if (availableWidthMinusMargins < 0)
+ availableWidthMinusMargins = 0;
+
+ var availableHeightMinusMargins = finalRect.Height - margin.Top - margin.Bottom;
+ if (availableHeightMinusMargins < 0)
+ availableHeightMinusMargins = 0;
+
+ var availableSizeMinusMargins = new Size(availableWidthMinusMargins, availableHeightMinusMargins);
var horizontalAlignment = HorizontalAlignment;
var verticalAlignment = VerticalAlignment;
var size = availableSizeMinusMargins;
@@ -670,12 +673,12 @@ protected virtual void ArrangeCore(Rect finalRect)
size = size.WithHeight(Math.Min(size.Height, DesiredSize.Height - margin.Top - margin.Bottom));
}
- size = LayoutHelper.ApplyLayoutConstraints(this, size);
+ size = LayoutHelper.ApplyLayoutConstraints(new MinMax(this), size);
if (useLayoutRounding)
{
- size = LayoutHelper.RoundLayoutSizeUp(size, scale, scale);
- availableSizeMinusMargins = LayoutHelper.RoundLayoutSizeUp(availableSizeMinusMargins, scale, scale);
+ size = LayoutHelper.RoundLayoutSizeUp(size, scale);
+ availableSizeMinusMargins = LayoutHelper.RoundLayoutSizeUp(availableSizeMinusMargins, scale);
}
size = ArrangeOverride(size).Constrain(size);
@@ -702,13 +705,14 @@ protected virtual void ArrangeCore(Rect finalRect)
break;
}
+ var origin = new Point(originX, originY);
+
if (useLayoutRounding)
{
- originX = LayoutHelper.RoundLayoutValue(originX, scale);
- originY = LayoutHelper.RoundLayoutValue(originY, scale);
+ origin = LayoutHelper.RoundLayoutPoint(origin, scale);
}
- Bounds = new Rect(originX, originY, size.Width, size.Height);
+ Bounds = new Rect(origin, size);
}
}
@@ -887,11 +891,10 @@ private void AncestorBecameVisible(ILayoutManager layoutManager)
/// True if the rect is invalid; otherwise false.
private static bool IsInvalidRect(Rect rect)
{
- return rect.Width < 0 || rect.Height < 0 ||
- double.IsInfinity(rect.X) || double.IsInfinity(rect.Y) ||
- double.IsInfinity(rect.Width) || double.IsInfinity(rect.Height) ||
- double.IsNaN(rect.X) || double.IsNaN(rect.Y) ||
- double.IsNaN(rect.Width) || double.IsNaN(rect.Height);
+ return MathUtilities.IsNegativeOrNonFinite(rect.Width) ||
+ MathUtilities.IsNegativeOrNonFinite(rect.Height) ||
+ !MathUtilities.IsFinite(rect.X) ||
+ !MathUtilities.IsFinite(rect.Y);
}
///
@@ -902,9 +905,8 @@ private static bool IsInvalidRect(Rect rect)
/// True if the size is invalid; otherwise false.
private static bool IsInvalidSize(Size size)
{
- return size.Width < 0 || size.Height < 0 ||
- double.IsInfinity(size.Width) || double.IsInfinity(size.Height) ||
- double.IsNaN(size.Width) || double.IsNaN(size.Height);
+ return MathUtilities.IsNegativeOrNonFinite(size.Width) ||
+ MathUtilities.IsNegativeOrNonFinite(size.Height);
}
///
diff --git a/src/Avalonia.Base/Layout/MinMax.cs b/src/Avalonia.Base/Layout/MinMax.cs
new file mode 100644
index 00000000000..aa482442553
--- /dev/null
+++ b/src/Avalonia.Base/Layout/MinMax.cs
@@ -0,0 +1,51 @@
+using System.Runtime.CompilerServices;
+
+namespace Avalonia.Layout;
+
+internal struct MinMax
+{
+ public double MinWidth;
+ public double MaxWidth;
+ public double MinHeight;
+ public double MaxHeight;
+
+ public MinMax(Layoutable e)
+ {
+ (MinWidth, MaxWidth) = CalcMinMax(e.Width, e.MinWidth, e.MaxWidth);
+ (MinHeight, MaxHeight) = CalcMinMax(e.Height, e.MinHeight, e.MaxHeight);
+ }
+
+ private static (double Min, double Max) CalcMinMax(double value, double min, double max)
+ {
+ double v0, v1;
+
+ if (double.IsNaN(value))
+ {
+ v0 = 0.0;
+ v1 = double.PositiveInfinity;
+ }
+ else
+ {
+ v0 = v1 = value;
+ }
+
+ max = ClampUnchecked(v1, min, max);
+ min = ClampUnchecked(v0, min, max);
+
+ return (min, max);
+ }
+
+ // Don't use Math.Clamp, it's possible for min to be greater than max here
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private static double ClampUnchecked(double value, double min, double max)
+ {
+ if (value > max)
+ value = max;
+
+ if (value < min)
+ value = min;
+
+ return value;
+ }
+
+}
diff --git a/src/Avalonia.Base/Size.cs b/src/Avalonia.Base/Size.cs
index 587d5a53e46..5ac9b7f9dec 100644
--- a/src/Avalonia.Base/Size.cs
+++ b/src/Avalonia.Base/Size.cs
@@ -1,5 +1,6 @@
using System;
using System.Globalization;
+using System.Runtime.CompilerServices;
#if !BUILDTASK
using Avalonia.Animation.Animators;
#endif
@@ -187,11 +188,18 @@ public Size Constrain(Size constraint)
/// The thickness.
/// The deflated size.
/// The deflated size cannot be less than 0.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public Size Deflate(Thickness thickness)
{
- return new Size(
- Math.Max(0, _width - thickness.Left - thickness.Right),
- Math.Max(0, _height - thickness.Top - thickness.Bottom));
+ var width = _width - thickness.Left - thickness.Right;
+ if (width < 0)
+ width = 0;
+
+ var height = _height - thickness.Top - thickness.Bottom;
+ if (height < 0)
+ height = 0;
+
+ return new Size(width, height);
}
///
@@ -247,6 +255,7 @@ public override int GetHashCode()
///
/// The thickness.
/// The inflated size.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public Size Inflate(Thickness thickness)
{
return new Size(
diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs
index 1f0c00ef639..8d8a093ac4f 100644
--- a/src/Avalonia.Base/StyledElement.cs
+++ b/src/Avalonia.Base/StyledElement.cs
@@ -84,7 +84,7 @@ public class StyledElement : Animatable,
private string? _name;
private Classes? _classes;
private ILogicalRoot? _logicalRoot;
- private IAvaloniaList? _logicalChildren;
+ private AvaloniaList? _logicalChildren;
private IResourceDictionary? _resources;
private Styles? _styles;
private bool _stylesApplied;
diff --git a/src/Avalonia.Base/Threading/Dispatcher.cs b/src/Avalonia.Base/Threading/Dispatcher.cs
index 07e09c3d588..8253c2fed25 100644
--- a/src/Avalonia.Base/Threading/Dispatcher.cs
+++ b/src/Avalonia.Base/Threading/Dispatcher.cs
@@ -44,9 +44,18 @@ internal Dispatcher(IDispatcherImpl impl)
_exceptionFilterEventArgs = new DispatcherUnhandledExceptionFilterEventArgs(this);
}
- public static Dispatcher UIThread => s_uiThread ??= CreateUIThreadDispatcher();
+ public static Dispatcher UIThread
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get
+ {
+ return s_uiThread ??= CreateUIThreadDispatcher();
+ }
+ }
+
public bool SupportsRunLoops => _controlledImpl != null;
+ [MethodImpl(MethodImplOptions.NoInlining)]
private static Dispatcher CreateUIThreadDispatcher()
{
var impl = AvaloniaLocator.Current.GetService();
diff --git a/src/Avalonia.Base/Utilities/MathUtilities.cs b/src/Avalonia.Base/Utilities/MathUtilities.cs
index 1c729593127..01f041c5eec 100644
--- a/src/Avalonia.Base/Utilities/MathUtilities.cs
+++ b/src/Avalonia.Base/Utilities/MathUtilities.cs
@@ -208,6 +208,7 @@ public static bool IsZero(float value)
/// The minimum value.
/// The maximum value.
/// The clamped value.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static double Clamp(double val, double min, double max)
{
if (min > max)
@@ -363,6 +364,28 @@ public static (double min, double max) GetMinMaxFromDelta(double initialValue, d
{
return GetMinMax(initialValue, initialValue + delta);
}
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static bool IsNegativeOrNonFinite(double d)
+ {
+#if NET6_0_OR_GREATER
+ ulong bits = BitConverter.DoubleToUInt64Bits(d);
+ return bits >= 0x7FF0_0000_0000_0000;
+#else
+ return d < 0 || !IsFinite(d);
+#endif
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static bool IsFinite(double d)
+ {
+#if NET6_0_OR_GREATER
+ return double.IsFinite(d);
+#else
+ long bits = BitConverter.DoubleToInt64Bits(d);
+ return (bits & 0x7FFF_FFFF_FFFF_FFFF) < 0x7FF0_0000_0000_0000;
+#endif
+ }
#if !BUILDTASK
internal static int WhichPolygonSideIntersects(
@@ -451,7 +474,7 @@ internal static bool IsEntirelyContained(
return true;
}
#endif
-
+
private static void ThrowCannotBeGreaterThanException(T min, T max)
{
throw new ArgumentException($"{min} cannot be greater than {max}.");
diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs
index 52e5251addc..6b10fa228c4 100644
--- a/src/Avalonia.Base/Visual.cs
+++ b/src/Avalonia.Base/Visual.cs
@@ -148,6 +148,8 @@ static Visual()
///
public Visual()
{
+ _visualRoot = this as IRenderRoot;
+
// Disable transitions until we're added to the visual tree.
DisableTransitions();
@@ -317,16 +319,12 @@ public int ZIndex
///
/// Gets the control's child visuals.
///
- protected internal IAvaloniaList VisualChildren
- {
- get;
- private set;
- }
+ protected internal IAvaloniaList VisualChildren { get; }
///
/// Gets the root of the visual tree, if the control is attached to a visual tree.
///
- protected internal IRenderRoot? VisualRoot => _visualRoot ?? (this as IRenderRoot);
+ protected internal IRenderRoot? VisualRoot => _visualRoot;
internal RenderOptions RenderOptions { get; set; }
@@ -544,7 +542,7 @@ protected virtual void OnDetachedFromVisualTreeCore(VisualTreeAttachmentEventArg
{
Logger.TryGet(LogEventLevel.Verbose, LogArea.Visual)?.Log(this, "Detached from visual tree");
- _visualRoot = null;
+ _visualRoot = this as IRenderRoot;
RootedVisualChildrenCount--;
if (RenderTransform is IMutableTransform mutableTransform)
@@ -683,9 +681,9 @@ private void SetVisualParent(Visual? value)
var old = _visualParent;
_visualParent = value;
- if (_visualRoot != null)
+ if (_visualRoot is not null && old is not null)
{
- var e = new VisualTreeAttachmentEventArgs(old!, _visualRoot);
+ var e = new VisualTreeAttachmentEventArgs(old, _visualRoot);
OnDetachedFromVisualTreeCore(e);
}
diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs
index 6353f7ebb93..a1df0c41805 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs
@@ -899,7 +899,7 @@ internal void ComputeLayoutRoundedWidth(double leftEdge)
if (OwningGrid != null && OwningGrid.UseLayoutRounding)
{
var scale = LayoutHelper.GetLayoutScale(HeaderCell);
- var roundSize = LayoutHelper.RoundLayoutSizeUp(new Size(leftEdge + ActualWidth, 1), scale, scale);
+ var roundSize = LayoutHelper.RoundLayoutSizeUp(new Size(leftEdge + ActualWidth, 1), scale);
LayoutRoundedWidth = roundSize.Width - leftEdge;
}
else
diff --git a/src/Avalonia.Controls/Border.cs b/src/Avalonia.Controls/Border.cs
index 4eb0274ec72..dc82c50a392 100644
--- a/src/Avalonia.Controls/Border.cs
+++ b/src/Avalonia.Controls/Border.cs
@@ -156,7 +156,7 @@ private Thickness LayoutThickness
var borderThickness = BorderThickness;
if (UseLayoutRounding)
- borderThickness = LayoutHelper.RoundLayoutThickness(borderThickness, _scale, _scale);
+ borderThickness = LayoutHelper.RoundLayoutThickness(borderThickness, _scale);
_layoutThickness = borderThickness;
}
diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs
index e57acba5cfe..aacdb266674 100644
--- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs
@@ -525,7 +525,7 @@ private Thickness LayoutThickness
var borderThickness = BorderThickness;
if (UseLayoutRounding)
- borderThickness = LayoutHelper.RoundLayoutThickness(borderThickness, _scale, _scale);
+ borderThickness = LayoutHelper.RoundLayoutThickness(borderThickness, _scale);
_layoutThickness = borderThickness;
}
@@ -618,8 +618,8 @@ internal Size ArrangeOverrideImpl(Size finalSize, Vector offset)
if (useLayoutRounding)
{
- padding = LayoutHelper.RoundLayoutThickness(padding, scale, scale);
- borderThickness = LayoutHelper.RoundLayoutThickness(borderThickness, scale, scale);
+ padding = LayoutHelper.RoundLayoutThickness(padding, scale);
+ borderThickness = LayoutHelper.RoundLayoutThickness(borderThickness, scale);
}
padding += borderThickness;
@@ -642,8 +642,8 @@ internal Size ArrangeOverrideImpl(Size finalSize, Vector offset)
if (useLayoutRounding)
{
- sizeForChild = LayoutHelper.RoundLayoutSizeUp(sizeForChild, scale, scale);
- availableSize = LayoutHelper.RoundLayoutSizeUp(availableSize, scale, scale);
+ sizeForChild = LayoutHelper.RoundLayoutSizeUp(sizeForChild, scale);
+ availableSize = LayoutHelper.RoundLayoutSizeUp(availableSize, scale);
}
switch (horizontalContentAlignment)
@@ -666,14 +666,14 @@ internal Size ArrangeOverrideImpl(Size finalSize, Vector offset)
break;
}
+ var origin = new Point(originX, originY);
+
if (useLayoutRounding)
{
- originX = LayoutHelper.RoundLayoutValue(originX, scale);
- originY = LayoutHelper.RoundLayoutValue(originY, scale);
+ origin = LayoutHelper.RoundLayoutPoint(origin, scale);
}
- var boundsForChild =
- new Rect(originX, originY, sizeForChild.Width, sizeForChild.Height).Deflate(padding);
+ var boundsForChild = new Rect(origin, sizeForChild).Deflate(padding);
Child.Arrange(boundsForChild);
diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
index 88e10c3ba35..e8503886813 100644
--- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
@@ -486,7 +486,7 @@ private Size ComputeExtent(Size viewportSize)
if (Child.UseLayoutRounding)
{
var scale = LayoutHelper.GetLayoutScale(Child);
- childMargin = LayoutHelper.RoundLayoutThickness(childMargin, scale, scale);
+ childMargin = LayoutHelper.RoundLayoutThickness(childMargin, scale);
}
var extent = Child!.Bounds.Size.Inflate(childMargin);
diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs
index 12b631aeb16..9ae941f6d12 100644
--- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs
+++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs
@@ -291,7 +291,6 @@ public static void SetIsTemplateFocusTarget(Control control, bool value)
public sealed override void ApplyTemplate()
{
var template = Template;
- var logical = (ILogical)this;
// Apply the template if it is not the same as the template already applied - except
// for in the case that the template is null and we're not attached to the logical
@@ -299,7 +298,7 @@ public sealed override void ApplyTemplate()
// the template has been detached, so we want to wait until it's re-attached to the
// logical tree as if it's re-attached to the same tree the template will be the same
// and we don't need to do anything.
- if (_appliedTemplate != template && (template != null || logical.IsAttachedToLogicalTree))
+ if (_appliedTemplate != template && (template != null || ((ILogical)this).IsAttachedToLogicalTree))
{
if (VisualChildren.Count > 0)
{
diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs
index a8324b2a5a2..5de3d76ed1a 100644
--- a/src/Avalonia.Controls/TextBlock.cs
+++ b/src/Avalonia.Controls/TextBlock.cs
@@ -605,7 +605,7 @@ private protected virtual void RenderCore(DrawingContext context)
}
var scale = LayoutHelper.GetLayoutScale(this);
- var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale);
+ var padding = LayoutHelper.RoundLayoutThickness(Padding, scale);
var top = padding.Top;
var textHeight = TextLayout.Height;
@@ -709,7 +709,7 @@ protected override void OnMeasureInvalidated()
protected override Size MeasureOverride(Size availableSize)
{
var scale = LayoutHelper.GetLayoutScale(this);
- var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale);
+ var padding = LayoutHelper.RoundLayoutThickness(Padding, scale);
var deflatedSize = availableSize.Deflate(padding);
if (_constraint != deflatedSize)
@@ -740,7 +740,7 @@ protected override Size MeasureOverride(Size availableSize)
//This implicitly recreated the TextLayout with a new constraint if we previously reset it.
var textLayout = TextLayout;
- var size = LayoutHelper.RoundLayoutSizeUp(new Size(textLayout.MinTextWidth, textLayout.Height).Inflate(padding), 1, 1);
+ var size = LayoutHelper.RoundLayoutSizeUp(new Size(textLayout.MinTextWidth, textLayout.Height).Inflate(padding), 1);
return size;
}
@@ -748,7 +748,7 @@ protected override Size MeasureOverride(Size availableSize)
protected override Size ArrangeOverride(Size finalSize)
{
var scale = LayoutHelper.GetLayoutScale(this);
- var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale);
+ var padding = LayoutHelper.RoundLayoutThickness(Padding, scale);
var availableSize = finalSize.Deflate(padding);
diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs
index fa03cc7be83..6331268f978 100644
--- a/src/Avalonia.Controls/TopLevel.cs
+++ b/src/Avalonia.Controls/TopLevel.cs
@@ -131,6 +131,7 @@ private static readonly WeakEvent
private readonly IDisposable? _pointerOverPreProcessorSubscription;
private readonly IDisposable? _backGestureSubscription;
private readonly Dictionary _platformImplBindings = new();
+ private double _scaling;
private Size _clientSize;
private Size? _frameSize;
private WindowTransparencyLevel _actualTransparencyLevel;
@@ -211,6 +212,7 @@ public TopLevel(ITopLevelImpl impl, IAvaloniaDependencyResolver? dependencyResol
PlatformImpl = impl ?? throw new InvalidOperationException(
"Could not create window implementation: maybe no windowing subsystem was initialized?");
+ _scaling = ValidateScaling(impl.RenderScaling);
_actualTransparencyLevel = PlatformImpl.TransparencyLevel;
dependencyResolver ??= AvaloniaLocator.Current;
@@ -536,11 +538,10 @@ public static bool GetAutoSafeAreaPadding(Control control)
return control.GetValue(AutoSafeAreaPaddingProperty);
}
- ///
- double ILayoutRoot.LayoutScaling => PlatformImpl?.RenderScaling ?? 1;
+ double ILayoutRoot.LayoutScaling => _scaling;
///
- public double RenderScaling => PlatformImpl?.RenderScaling ?? 1;
+ public double RenderScaling => _scaling;
IStyleHost IStyleHost.StylingParent => _globalStyles!;
@@ -698,6 +699,7 @@ private protected virtual void HandleClosed()
Debug.Assert(PlatformImpl != null);
// The PlatformImpl is completely invalid at this point
PlatformImpl = null;
+ _scaling = 1.0;
if (_globalStyles is object)
{
@@ -749,6 +751,7 @@ internal virtual void HandleResized(Size clientSize, WindowResizeReason reason)
/// The window scaling.
private void HandleScalingChanged(double scaling)
{
+ _scaling = ValidateScaling(scaling);
LayoutHelper.InvalidateSelfAndChildrenMeasure(this);
Dispatcher.UIThread.Send(_ => ScalingChanged?.Invoke(this, EventArgs.Empty));
}
@@ -952,6 +955,24 @@ protected internal override void InvalidateMirrorTransform()
ITextInputMethodImpl? ITextInputMethodRoot.InputMethod => PlatformImpl?.TryGetFeature();
+ private double ValidateScaling(double scaling)
+ {
+ if (MathUtilities.IsNegativeOrNonFinite(scaling) || MathUtilities.IsZero(scaling))
+ {
+ throw new InvalidOperationException(
+ $"Invalid {nameof(ITopLevelImpl.RenderScaling)} value {scaling} returned from {PlatformImpl?.GetType()}");
+ }
+
+ if (MathUtilities.IsOne(scaling))
+ {
+ // Ensure we've got exactly 1.0 and not an approximation,
+ // so we don't have to use MathUtilities.IsOne in various layout hot paths.
+ return 1.0;
+ }
+
+ return scaling;
+ }
+
///
/// Provides layout pass timing from the layout manager to the renderer, for diagnostics purposes.
///
diff --git a/tests/Avalonia.Benchmarks/Layout/Measure.cs b/tests/Avalonia.Benchmarks/Layout/Measure.cs
index fce2cddec9d..98e5538b5d4 100644
--- a/tests/Avalonia.Benchmarks/Layout/Measure.cs
+++ b/tests/Avalonia.Benchmarks/Layout/Measure.cs
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Avalonia.Controls;
+using Avalonia.Layout;
using Avalonia.UnitTests;
using BenchmarkDotNet.Attributes;
@@ -31,12 +32,23 @@ public Measure()
[Benchmark, MethodImpl(MethodImplOptions.NoInlining)]
public void Remeasure()
{
+ _root.InvalidateMeasure();
+
foreach (var control in _controls)
{
- control.InvalidateMeasure();
+ // Use an unsafe accessor instead of InvalidateMeasure, otherwise a lot of time is spent invalidating
+ // controls, which we don't want: this benchmark is supposed to be focused on Measure/Arrange.
+ SetIsMeasureValid(control, false);
+ SetIsArrangeValid(control, false);
}
_root.LayoutManager.ExecuteLayoutPass();
}
+
+ [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "set_" + nameof(Layoutable.IsMeasureValid))]
+ private static extern void SetIsMeasureValid(Layoutable layoutable, bool value);
+
+ [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "set_" + nameof(Layoutable.IsArrangeValid))]
+ private static extern void SetIsArrangeValid(Layoutable layoutable, bool value);
}
}
diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs
index e70ab520439..27d55fb77d9 100644
--- a/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs
@@ -300,7 +300,10 @@ public void Extent_Should_Be_Rounded_To_Viewport_When_Close()
target.Measure(new Size(1000, 1000));
target.Arrange(new Rect(0, 0, 1000, 1000));
- Assert.Equal(new Size(176.00000000000003, 176.00000000000003), target.Child!.DesiredSize);
+ var nonRoundedVieViewport = target.Child!.Bounds.Size.Inflate(
+ LayoutHelper.RoundLayoutThickness(target.Child.Margin, root.LayoutScaling));
+
+ Assert.Equal(new Size(176.00000000000003, 176.00000000000003), nonRoundedVieViewport);
Assert.Equal(new Size(176, 176), target.Viewport);
Assert.Equal(new Size(176, 176), target.Extent);
}
diff --git a/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs b/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs
index f1399d83bc5..1237d0f2f10 100644
--- a/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs
@@ -66,7 +66,7 @@ public void Should_Measure_MinTextWith()
var textLayout = textBlock.TextLayout;
- var constraint = LayoutHelper.RoundLayoutSizeUp(new Size(textLayout.MinTextWidth, textLayout.Height), 1, 1);
+ var constraint = LayoutHelper.RoundLayoutSizeUp(new Size(textLayout.MinTextWidth, textLayout.Height), 1);
Assert.Equal(textBlock.DesiredSize, constraint);
}
@@ -83,7 +83,7 @@ public void Calling_Arrange_With_Different_Size_Should_Update_Constraint_And_Tex
var textLayout = textBlock.TextLayout;
- var constraint = LayoutHelper.RoundLayoutSizeUp(new Size(textLayout.WidthIncludingTrailingWhitespace, textLayout.Height), 1, 1);
+ var constraint = LayoutHelper.RoundLayoutSizeUp(new Size(textLayout.WidthIncludingTrailingWhitespace, textLayout.Height), 1);
textBlock.Arrange(new Rect(constraint));
@@ -118,7 +118,7 @@ public void Calling_Measure_With_Infinite_Space_Should_Set_DesiredSize()
var textLayout = textBlock.TextLayout;
- var constraint = LayoutHelper.RoundLayoutSizeUp(new Size(textLayout.WidthIncludingTrailingWhitespace, textLayout.Height), 1, 1);
+ var constraint = LayoutHelper.RoundLayoutSizeUp(new Size(textLayout.WidthIncludingTrailingWhitespace, textLayout.Height), 1);
Assert.Equal(constraint, textBlock.DesiredSize);
}
diff --git a/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs b/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs
index 1c6495359a4..a4c12744ab8 100644
--- a/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs
@@ -239,6 +239,7 @@ private static Mock CreateMockWindowBaseImpl(bool setupAllPrope
var renderer = new Mock();
if (setupAllProperties)
renderer.SetupAllProperties();
+ renderer.Setup(x => x.RenderScaling).Returns(1.0);
renderer.Setup(x => x.Compositor).Returns(RendererMocks.CreateDummyCompositor());
return renderer;
}
@@ -248,18 +249,10 @@ private class TestWindowBase : WindowBase
public bool IsClosed { get; private set; }
public TestWindowBase()
- : base(CreateWindowsBaseImplMock())
+ : base(CreateMockWindowBaseImpl().Object)
{
}
- private static IWindowBaseImpl CreateWindowsBaseImplMock()
- {
- var compositor = RendererMocks.CreateDummyCompositor();
- return Mock.Of(x =>
- x.RenderScaling == 1 &&
- x.Compositor == compositor);
- }
-
public TestWindowBase(IWindowBaseImpl impl)
: base(impl)
{
diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs
index cd43282dc78..55b9a6de66b 100644
--- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs
@@ -18,6 +18,7 @@ public class WindowTests
public void Setting_Title_Should_Set_Impl_Title()
{
var windowImpl = new Mock();
+ windowImpl.Setup(r => r.RenderScaling).Returns(1.0);
windowImpl.Setup(r => r.Compositor).Returns(RendererMocks.CreateDummyCompositor());
var windowingPlatform = new MockWindowingPlatform(() => windowImpl.Object);