Layout performance improvements (#18315)
This commit is contained in:
committed by
GitHub
parent
14c9b27ef9
commit
aac83dcd53
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Avalonia.Utilities;
|
||||
using Avalonia.VisualTree;
|
||||
|
||||
@@ -25,21 +27,20 @@ namespace Avalonia.Layout
|
||||
/// <param name="constraints">The space available for the control.</param>
|
||||
/// <returns>The control's size.</returns>
|
||||
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 @@ namespace Avalonia.Layout
|
||||
{
|
||||
if (IsParentLayoutRounded(control, out double scale))
|
||||
{
|
||||
padding = RoundLayoutThickness(padding, scale, scale);
|
||||
padding = RoundLayoutThickness(padding, scale);
|
||||
}
|
||||
|
||||
if (control != null)
|
||||
@@ -71,8 +72,8 @@ namespace Avalonia.Layout
|
||||
{
|
||||
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 @@ namespace Avalonia.Layout
|
||||
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 @@ namespace Avalonia.Layout
|
||||
/// <param name="control">The control.</param>
|
||||
/// <exception cref="Exception">Thrown when control has no root or returned layout scaling is invalid.</exception>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Rounds a size to integer values for layout purposes, compensating for high DPI screen
|
||||
@@ -172,6 +162,20 @@ namespace Avalonia.Layout
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rounds a thickness to integer values for layout purposes, compensating for high DPI screen
|
||||
/// coordinates.
|
||||
@@ -196,6 +200,38 @@ namespace Avalonia.Layout
|
||||
);
|
||||
}
|
||||
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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 @@ namespace Avalonia.Layout
|
||||
/// </remarks>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -250,73 +268,25 @@ namespace Avalonia.Layout
|
||||
/// </remarks>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the min and max height for a control. Ported from WPF.
|
||||
/// </summary>
|
||||
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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using Avalonia.Diagnostics;
|
||||
using Avalonia.Logging;
|
||||
using Avalonia.Reactive;
|
||||
using Avalonia.Utilities;
|
||||
using Avalonia.VisualTree;
|
||||
|
||||
#nullable enable
|
||||
@@ -544,53 +545,43 @@ namespace Avalonia.Layout
|
||||
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;
|
||||
|
||||
{
|
||||
double widthCache = Width;
|
||||
|
||||
if (!double.IsNaN(widthCache))
|
||||
{
|
||||
width = widthCache;
|
||||
}
|
||||
}
|
||||
|
||||
width = Math.Min(width, MaxWidth);
|
||||
width = Math.Max(width, MinWidth);
|
||||
|
||||
{
|
||||
double heightCache = Height;
|
||||
|
||||
if (!double.IsNaN(heightCache))
|
||||
{
|
||||
height = heightCache;
|
||||
}
|
||||
}
|
||||
|
||||
height = Math.Min(height, MaxHeight);
|
||||
height = Math.Max(height, MinHeight);
|
||||
var width = MathUtilities.Clamp(measured.Width, minMax.MinWidth, minMax.MaxWidth);
|
||||
var height = MathUtilities.Clamp(measured.Height, minMax.MinHeight, minMax.MaxHeight);
|
||||
|
||||
if (useLayoutRounding)
|
||||
{
|
||||
(width, height) = LayoutHelper.RoundLayoutSizeUp(new Size(width, height), scale, scale);
|
||||
(width, height) = LayoutHelper.RoundLayoutSizeUp(new Size(width, height), scale);
|
||||
}
|
||||
|
||||
width = Math.Min(width, availableSize.Width);
|
||||
height = Math.Min(height, availableSize.Height);
|
||||
if (width > availableSize.Width)
|
||||
width = availableSize.Width;
|
||||
|
||||
return NonNegative(new Size(width, height).Inflate(margin));
|
||||
if (height > availableSize.Height)
|
||||
height = availableSize.Height;
|
||||
|
||||
width += margin.Left + margin.Right;
|
||||
height += margin.Top + margin.Bottom;
|
||||
|
||||
if (width < 0)
|
||||
width = 0;
|
||||
|
||||
if (height < 0)
|
||||
height = 0;
|
||||
|
||||
return new Size(width, height);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -618,8 +609,13 @@ namespace Avalonia.Layout
|
||||
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 @@ namespace Avalonia.Layout
|
||||
// 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 @@ namespace Avalonia.Layout
|
||||
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 @@ namespace Avalonia.Layout
|
||||
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 @@ namespace Avalonia.Layout
|
||||
/// <returns>True if the rect is invalid; otherwise false.</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -902,9 +905,8 @@ namespace Avalonia.Layout
|
||||
/// <returns>True if the size is invalid; otherwise false.</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Runtime.CompilerServices;
|
||||
#if !BUILDTASK
|
||||
using Avalonia.Animation.Animators;
|
||||
#endif
|
||||
@@ -187,11 +188,18 @@ namespace Avalonia
|
||||
/// <param name="thickness">The thickness.</param>
|
||||
/// <returns>The deflated size.</returns>
|
||||
/// <remarks>The deflated size cannot be less than 0.</remarks>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -247,6 +255,7 @@ namespace Avalonia
|
||||
/// </summary>
|
||||
/// <param name="thickness">The thickness.</param>
|
||||
/// <returns>The inflated size.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Size Inflate(Thickness thickness)
|
||||
{
|
||||
return new Size(
|
||||
|
||||
@@ -85,7 +85,7 @@ namespace Avalonia
|
||||
private string? _name;
|
||||
private Classes? _classes;
|
||||
private ILogicalRoot? _logicalRoot;
|
||||
private IAvaloniaList<ILogical>? _logicalChildren;
|
||||
private AvaloniaList<ILogical>? _logicalChildren;
|
||||
private IResourceDictionary? _resources;
|
||||
private Styles? _styles;
|
||||
private bool _stylesApplied;
|
||||
|
||||
@@ -44,9 +44,18 @@ public partial class Dispatcher : IDispatcher
|
||||
_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<IDispatcherImpl>();
|
||||
|
||||
@@ -208,6 +208,7 @@ namespace Avalonia.Utilities
|
||||
/// <param name="min">The minimum value.</param>
|
||||
/// <param name="max">The maximum value.</param>
|
||||
/// <returns>The clamped value.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static double Clamp(double val, double min, double max)
|
||||
{
|
||||
if (min > max)
|
||||
@@ -363,6 +364,28 @@ namespace Avalonia.Utilities
|
||||
{
|
||||
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 @@ namespace Avalonia.Utilities
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
private static void ThrowCannotBeGreaterThanException<T>(T min, T max)
|
||||
{
|
||||
throw new ArgumentException($"{min} cannot be greater than {max}.");
|
||||
|
||||
@@ -148,6 +148,8 @@ namespace Avalonia
|
||||
/// </summary>
|
||||
public Visual()
|
||||
{
|
||||
_visualRoot = this as IRenderRoot;
|
||||
|
||||
// Disable transitions until we're added to the visual tree.
|
||||
DisableTransitions();
|
||||
|
||||
@@ -317,16 +319,12 @@ namespace Avalonia
|
||||
/// <summary>
|
||||
/// Gets the control's child visuals.
|
||||
/// </summary>
|
||||
protected internal IAvaloniaList<Visual> VisualChildren
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
protected internal IAvaloniaList<Visual> VisualChildren { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the root of the visual tree, if the control is attached to a visual tree.
|
||||
/// </summary>
|
||||
protected internal IRenderRoot? VisualRoot => _visualRoot ?? (this as IRenderRoot);
|
||||
protected internal IRenderRoot? VisualRoot => _visualRoot;
|
||||
|
||||
internal RenderOptions RenderOptions { get; set; }
|
||||
|
||||
@@ -544,7 +542,7 @@ namespace Avalonia
|
||||
{
|
||||
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 @@ namespace Avalonia
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -156,7 +156,7 @@ namespace Avalonia.Controls
|
||||
var borderThickness = BorderThickness;
|
||||
|
||||
if (UseLayoutRounding)
|
||||
borderThickness = LayoutHelper.RoundLayoutThickness(borderThickness, _scale, _scale);
|
||||
borderThickness = LayoutHelper.RoundLayoutThickness(borderThickness, _scale);
|
||||
|
||||
_layoutThickness = borderThickness;
|
||||
}
|
||||
|
||||
@@ -525,7 +525,7 @@ namespace Avalonia.Controls.Presenters
|
||||
var borderThickness = BorderThickness;
|
||||
|
||||
if (UseLayoutRounding)
|
||||
borderThickness = LayoutHelper.RoundLayoutThickness(borderThickness, _scale, _scale);
|
||||
borderThickness = LayoutHelper.RoundLayoutThickness(borderThickness, _scale);
|
||||
|
||||
_layoutThickness = borderThickness;
|
||||
}
|
||||
@@ -618,8 +618,8 @@ namespace Avalonia.Controls.Presenters
|
||||
|
||||
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 @@ namespace Avalonia.Controls.Presenters
|
||||
|
||||
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 @@ namespace Avalonia.Controls.Presenters
|
||||
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);
|
||||
|
||||
|
||||
@@ -511,7 +511,7 @@ namespace Avalonia.Controls.Presenters
|
||||
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);
|
||||
|
||||
@@ -291,7 +291,6 @@ namespace Avalonia.Controls.Primitives
|
||||
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 @@ namespace Avalonia.Controls.Primitives
|
||||
// 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)
|
||||
{
|
||||
|
||||
@@ -605,7 +605,7 @@ namespace Avalonia.Controls
|
||||
}
|
||||
|
||||
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 @@ namespace Avalonia.Controls
|
||||
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 @@ namespace Avalonia.Controls
|
||||
//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 @@ namespace Avalonia.Controls
|
||||
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);
|
||||
|
||||
|
||||
@@ -137,6 +137,7 @@ namespace Avalonia.Controls
|
||||
private readonly IDisposable? _pointerOverPreProcessorSubscription;
|
||||
private readonly IDisposable? _backGestureSubscription;
|
||||
private readonly Dictionary<AvaloniaProperty, Action> _platformImplBindings = new();
|
||||
private double _scaling;
|
||||
private Size _clientSize;
|
||||
private Size? _frameSize;
|
||||
private WindowTransparencyLevel _actualTransparencyLevel;
|
||||
@@ -218,6 +219,7 @@ namespace Avalonia.Controls
|
||||
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;
|
||||
@@ -555,11 +557,10 @@ namespace Avalonia.Controls
|
||||
return control.GetValue(AutoSafeAreaPaddingProperty);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
double ILayoutRoot.LayoutScaling => PlatformImpl?.RenderScaling ?? 1;
|
||||
double ILayoutRoot.LayoutScaling => _scaling;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public double RenderScaling => PlatformImpl?.RenderScaling ?? 1;
|
||||
public double RenderScaling => _scaling;
|
||||
|
||||
IStyleHost IStyleHost.StylingParent => _globalStyles!;
|
||||
|
||||
@@ -717,6 +718,7 @@ namespace Avalonia.Controls
|
||||
Debug.Assert(PlatformImpl != null);
|
||||
// The PlatformImpl is completely invalid at this point
|
||||
PlatformImpl = null;
|
||||
_scaling = 1.0;
|
||||
|
||||
if (_globalStyles is object)
|
||||
{
|
||||
@@ -768,6 +770,7 @@ namespace Avalonia.Controls
|
||||
/// <param name="scaling">The window scaling.</param>
|
||||
private void HandleScalingChanged(double scaling)
|
||||
{
|
||||
_scaling = ValidateScaling(scaling);
|
||||
LayoutHelper.InvalidateSelfAndChildrenMeasure(this);
|
||||
Dispatcher.UIThread.Send(_ => ScalingChanged?.Invoke(this, EventArgs.Empty));
|
||||
}
|
||||
@@ -971,6 +974,24 @@ namespace Avalonia.Controls
|
||||
|
||||
ITextInputMethodImpl? ITextInputMethodRoot.InputMethod => PlatformImpl?.TryGetFeature<ITextInputMethodImpl>();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides layout pass timing from the layout manager to the renderer, for diagnostics purposes.
|
||||
/// </summary>
|
||||
|
||||
@@ -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 @@ namespace Avalonia.Benchmarks.Layout
|
||||
[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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,7 +301,10 @@ namespace Avalonia.Controls.UnitTests.Presenters
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ namespace Avalonia.Controls.UnitTests
|
||||
|
||||
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 @@ namespace Avalonia.Controls.UnitTests
|
||||
|
||||
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 @@ namespace Avalonia.Controls.UnitTests
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -239,6 +239,7 @@ namespace Avalonia.Controls.UnitTests
|
||||
var renderer = new Mock<IWindowBaseImpl>();
|
||||
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 @@ namespace Avalonia.Controls.UnitTests
|
||||
public bool IsClosed { get; private set; }
|
||||
|
||||
public TestWindowBase()
|
||||
: base(CreateWindowsBaseImplMock())
|
||||
: base(CreateMockWindowBaseImpl().Object)
|
||||
{
|
||||
}
|
||||
|
||||
private static IWindowBaseImpl CreateWindowsBaseImplMock()
|
||||
{
|
||||
var compositor = RendererMocks.CreateDummyCompositor();
|
||||
return Mock.Of<IWindowBaseImpl>(x =>
|
||||
x.RenderScaling == 1 &&
|
||||
x.Compositor == compositor);
|
||||
}
|
||||
|
||||
public TestWindowBase(IWindowBaseImpl impl)
|
||||
: base(impl)
|
||||
{
|
||||
|
||||
@@ -18,6 +18,7 @@ namespace Avalonia.Controls.UnitTests
|
||||
public void Setting_Title_Should_Set_Impl_Title()
|
||||
{
|
||||
var windowImpl = new Mock<IWindowImpl>();
|
||||
windowImpl.Setup(r => r.RenderScaling).Returns(1.0);
|
||||
windowImpl.Setup(r => r.Compositor).Returns(RendererMocks.CreateDummyCompositor());
|
||||
var windowingPlatform = new MockWindowingPlatform(() => windowImpl.Object);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user