diff --git a/src/Avalonia.Base/Utilities/MathUtilities.cs b/src/Avalonia.Base/Utilities/MathUtilities.cs index dcb3ef4a2b..546133bb03 100644 --- a/src/Avalonia.Base/Utilities/MathUtilities.cs +++ b/src/Avalonia.Base/Utilities/MathUtilities.cs @@ -1,6 +1,9 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System; +using System.Runtime.InteropServices; + namespace Avalonia.Utilities { /// @@ -8,6 +11,86 @@ namespace Avalonia.Utilities /// public static class MathUtilities { + /// + /// AreClose - Returns whether or not two doubles are "close". That is, whether or + /// not they are within epsilon of each other. + /// + /// The first double to compare. + /// The second double to compare. + public static bool AreClose(double value1, double value2) + { + //in case they are Infinities (then epsilon check does not work) + if (value1 == value2) return true; + double eps = (Math.Abs(value1) + Math.Abs(value2) + 10.0) * double.Epsilon; + double delta = value1 - value2; + return (-eps < delta) && (eps > delta); + } + + /// + /// LessThan - Returns whether or not the first double is less than the second double. + /// That is, whether or not the first is strictly less than *and* not within epsilon of + /// the other number. + /// The first double to compare. + /// The second double to compare. + public static bool LessThan(double value1, double value2) + { + return (value1 < value2) && !AreClose(value1, value2); + } + + /// + /// GreaterThan - Returns whether or not the first double is greater than the second double. + /// That is, whether or not the first is strictly greater than *and* not within epsilon of + /// the other number. + /// The first double to compare. + /// The second double to compare. + public static bool GreaterThan(double value1, double value2) + { + return (value1 > value2) && !AreClose(value1, value2); + } + + /// + /// LessThanOrClose - Returns whether or not the first double is less than or close to + /// the second double. That is, whether or not the first is strictly less than or within + /// epsilon of the other number. + /// The first double to compare. + /// The second double to compare. + public static bool LessThanOrClose(double value1, double value2) + { + return (value1 < value2) || AreClose(value1, value2); + } + + /// + /// GreaterThanOrClose - Returns whether or not the first double is greater than or close to + /// the second double. That is, whether or not the first is strictly greater than or within + /// epsilon of the other number. + /// + /// The first double to compare. + /// The second double to compare. + public static bool GreaterThanOrClose(double value1, double value2) + { + return (value1 > value2) || AreClose(value1, value2); + } + + /// + /// IsOne - Returns whether or not the double is "close" to 1. Same as AreClose(double, 1), + /// but this is faster. + /// + /// The double to compare to 1. + public static bool IsOne(double value) + { + return Math.Abs(value - 1.0) < 10.0 * double.Epsilon; + } + + /// + /// IsZero - Returns whether or not the double is "close" to 0. Same as AreClose(double, 0), + /// but this is faster. + /// + /// The double to compare to 0. + public static bool IsZero(double value) + { + return Math.Abs(value) < 10.0 * double.Epsilon; + } + /// /// Clamps a value between a minimum and maximum value. /// @@ -31,6 +114,39 @@ namespace Avalonia.Utilities } } + /// + /// Calculates the value to be used for layout rounding at high DPI. + /// + /// Input value to be rounded. + /// Ratio of screen's DPI to layout DPI + /// Adjusted value that will produce layout rounding on screen at high dpi. + /// This is a layout helper method. It takes DPI into account and also does not return + /// the rounded value if it is unacceptable for layout, e.g. Infinity or NaN. It's a helper associated with + /// UseLayoutRounding property and should not be used as a general rounding utility. + public static double RoundLayoutValue(double value, double dpiScale) + { + double newValue; + + // If DPI == 1, don't use DPI-aware rounding. + if (!MathUtilities.AreClose(dpiScale, 1.0)) + { + 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; + } + /// /// Clamps a value between a minimum and maximum value. /// @@ -54,4 +170,4 @@ namespace Avalonia.Utilities } } } -} +} \ No newline at end of file diff --git a/src/Avalonia.Controls/WrapPanel.cs b/src/Avalonia.Controls/WrapPanel.cs index 597734d400..4df1b39400 100644 --- a/src/Avalonia.Controls/WrapPanel.cs +++ b/src/Avalonia.Controls/WrapPanel.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using System.Linq; using Avalonia.Input; +using Avalonia.Utilities; using static System.Math; @@ -92,109 +93,127 @@ namespace Avalonia.Controls } } - private UVSize CreateUVSize(Size size) => new UVSize(Orientation, size); - - private UVSize CreateUVSize() => new UVSize(Orientation); - /// - protected override Size MeasureOverride(Size availableSize) + protected override Size MeasureOverride(Size constraint) { - var desiredSize = CreateUVSize(); - var lineSize = CreateUVSize(); - var uvAvailableSize = CreateUVSize(availableSize); + var curLineSize = new UVSize(Orientation); + var panelSize = new UVSize(Orientation); + var uvConstraint = new UVSize(Orientation, constraint.Width, constraint.Height); - foreach (var child in Children) + var childConstraint = new Size(constraint.Width, constraint.Height); + + for (int i = 0, count = Children.Count; i < count; i++) { - child.Measure(availableSize); - var childSize = CreateUVSize(child.DesiredSize); - if (lineSize.U + childSize.U <= uvAvailableSize.U) // same line + var child = Children[i]; + if (child == null) continue; + + //Flow passes its own constrint to children + child.Measure(childConstraint); + + //this is the size of the child in UV space + var sz = new UVSize(Orientation, child.DesiredSize.Width, child.DesiredSize.Height); + + if (MathUtilities.GreaterThan(curLineSize.U + sz.U, uvConstraint.U)) //need to switch to another line { - lineSize.U += childSize.U; - lineSize.V = Max(lineSize.V, childSize.V); + panelSize.U = Max(curLineSize.U, panelSize.U); + panelSize.V += curLineSize.V; + curLineSize = sz; + + if (MathUtilities.GreaterThan(sz.U, uvConstraint.U)) //the element is wider then the constrint - give it a separate line + { + panelSize.U = Max(sz.U, panelSize.U); + panelSize.V += sz.V; + curLineSize = new UVSize(Orientation); + } } - else // moving to next line + else //continue to accumulate a line { - desiredSize.U = Max(lineSize.U, uvAvailableSize.U); - desiredSize.V += lineSize.V; - lineSize = childSize; + curLineSize.U += sz.U; + curLineSize.V = Max(sz.V, curLineSize.V); } } - // last element - desiredSize.U = Max(lineSize.U, desiredSize.U); - desiredSize.V += lineSize.V; - return desiredSize.ToSize(); + //the last line size, if any should be added + panelSize.U = Max(curLineSize.U, panelSize.U); + panelSize.V += curLineSize.V; + + //go from UV space to W/H space + return new Size(panelSize.Width, panelSize.Height); } /// protected override Size ArrangeOverride(Size finalSize) { + int firstInLine = 0; double accumulatedV = 0; - var uvFinalSize = CreateUVSize(finalSize); - var lineSize = CreateUVSize(); - int firstChildInLineIndex = 0; - for (int index = 0; index < Children.Count; index++) + UVSize curLineSize = new UVSize(Orientation); + UVSize uvFinalSize = new UVSize(Orientation, finalSize.Width, finalSize.Height); + + for (int i = 0; i < Children.Count; i++) { - var child = Children[index]; - var childSize = CreateUVSize(child.DesiredSize); - if (lineSize.U + childSize.U <= uvFinalSize.U) // same line + var child = Children[i]; + if (child == null) continue; + + var sz = new UVSize(Orientation, child.DesiredSize.Width, child.DesiredSize.Height); + + if (MathUtilities.GreaterThan(curLineSize.U + sz.U, uvFinalSize.U)) //need to switch to another line { - lineSize.U += childSize.U; - lineSize.V = Max(lineSize.V, childSize.V); + arrangeLine(accumulatedV, curLineSize.V, firstInLine, i); + + accumulatedV += curLineSize.V; + curLineSize = sz; + + if (MathUtilities.GreaterThan(sz.U, uvFinalSize.U)) //the element is wider then the constraint - give it a separate line + { + //switch to next line which only contain one element + arrangeLine(accumulatedV, sz.V, i, ++i); + + accumulatedV += sz.V; + curLineSize = new UVSize(Orientation); + } + firstInLine = i; } - else // moving to next line + else //continue to accumulate a line { - var controlsInLine = GetControlsBetween(firstChildInLineIndex, index); - ArrangeLine(accumulatedV, lineSize.V, controlsInLine); - accumulatedV += lineSize.V; - lineSize = childSize; - firstChildInLineIndex = index; + curLineSize.U += sz.U; + curLineSize.V = Max(sz.V, curLineSize.V); } } - if (firstChildInLineIndex < Children.Count) + //arrange the last line, if any + if (firstInLine < Children.Count) { - var controlsInLine = GetControlsBetween(firstChildInLineIndex, Children.Count); - ArrangeLine(accumulatedV, lineSize.V, controlsInLine); + arrangeLine(accumulatedV, curLineSize.V, firstInLine, Children.Count); } - return finalSize; - } - private IEnumerable GetControlsBetween(int first, int last) - { - return Children.Skip(first).Take(last - first); + return finalSize; } - private void ArrangeLine(double v, double lineV, IEnumerable controls) + private void arrangeLine(double v, double lineV, int start, int end) { double u = 0; bool isHorizontal = (Orientation == Orientation.Horizontal); - foreach (var child in controls) + + for (int i = start; i < end; i++) { - var childSize = CreateUVSize(child.DesiredSize); - var x = isHorizontal ? u : v; - var y = isHorizontal ? v : u; - var width = isHorizontal ? childSize.U : lineV; - var height = isHorizontal ? lineV : childSize.U; - child.Arrange(new Rect(x, y, width, height)); - u += childSize.U; + var child = Children[i]; + if (child != null) + { + UVSize childSize = new UVSize(Orientation, child.DesiredSize.Width, child.DesiredSize.Height); + double layoutSlotU = childSize.U; + child.Arrange(new Rect( + (isHorizontal ? u : v), + (isHorizontal ? v : u), + (isHorizontal ? layoutSlotU : lineV), + (isHorizontal ? lineV : layoutSlotU))); + u += layoutSlotU; + } } } - /// - /// Used to not not write separate code for horizontal and vertical orientation. - /// U is direction in line. (x if orientation is horizontal) - /// V is direction of lines. (y if orientation is horizontal) - /// - [DebuggerDisplay("U = {U} V = {V}")] + private struct UVSize { - private readonly Orientation _orientation; - - internal double U; - - internal double V; - - private UVSize(Orientation orientation, double width, double height) + internal UVSize(Orientation orientation, double width, double height) { U = V = 0d; _orientation = orientation; @@ -202,52 +221,25 @@ namespace Avalonia.Controls Height = height; } - internal UVSize(Orientation orientation, Size size) - : this(orientation, size.Width, size.Height) - { - } - internal UVSize(Orientation orientation) { U = V = 0d; _orientation = orientation; } - private double Width + internal double U; + internal double V; + private Orientation _orientation; + + internal double Width { get { return (_orientation == Orientation.Horizontal ? U : V); } - set - { - if (_orientation == Orientation.Horizontal) - { - U = value; - } - else - { - V = value; - } - } + set { if (_orientation == Orientation.Horizontal) U = value; else V = value; } } - - private double Height + internal double Height { get { return (_orientation == Orientation.Horizontal ? V : U); } - set - { - if (_orientation == Orientation.Horizontal) - { - V = value; - } - else - { - U = value; - } - } - } - - public Size ToSize() - { - return new Size(Width, Height); + set { if (_orientation == Orientation.Horizontal) V = value; else U = value; } } } }