diff --git a/src/Avalonia.Base/Properties/AssemblyInfo.cs b/src/Avalonia.Base/Properties/AssemblyInfo.cs index 0664f22dcb..692982cdc6 100644 --- a/src/Avalonia.Base/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Base/Properties/AssemblyInfo.cs @@ -8,3 +8,4 @@ using Avalonia.Metadata; [assembly: InternalsVisibleTo("Avalonia.Base.UnitTests")] [assembly: InternalsVisibleTo("Avalonia.UnitTests")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] +[assembly: InternalsVisibleTo("Avalonia.Controls.DataGrid")] diff --git a/src/Avalonia.Base/Utilities/MathUtilities.cs b/src/Avalonia.Base/Utilities/MathUtilities.cs index 7949a62949..f16af16ed5 100644 --- a/src/Avalonia.Base/Utilities/MathUtilities.cs +++ b/src/Avalonia.Base/Utilities/MathUtilities.cs @@ -1,5 +1,4 @@ using System; -using System.Runtime.InteropServices; namespace Avalonia.Utilities { @@ -9,7 +8,7 @@ namespace Avalonia.Utilities public static class MathUtilities { // smallest such that 1.0+DoubleEpsilon != 1.0 - private const double DoubleEpsilon = 2.2204460492503131e-016; + internal static readonly double DoubleEpsilon = 2.2204460492503131e-016; private const float FloatEpsilon = 1.192092896e-07F; @@ -188,6 +187,11 @@ namespace Avalonia.Utilities /// The clamped value. public static double Clamp(double val, double min, double max) { + if (min > max) + { + ThrowCannotBeGreaterThanException(min, max); + } + if (val < min) { return min; @@ -216,7 +220,7 @@ namespace Avalonia.Utilities double newValue; // If DPI == 1, don't use DPI-aware rounding. - if (!MathUtilities.AreClose(dpiScale, 1.0)) + 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. @@ -246,7 +250,7 @@ namespace Avalonia.Utilities { if (min > max) { - throw new ArgumentException($"{min} cannot be greater than {max}."); + ThrowCannotBeGreaterThanException(min, max); } if (val < min) @@ -262,5 +266,10 @@ namespace Avalonia.Utilities return val; } } + + private static void ThrowCannotBeGreaterThanException(double min, double max) + { + throw new ArgumentException($"{min} cannot be greater than {max}."); + } } } diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index cfe47a09d5..7e893d131e 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -2681,7 +2681,7 @@ namespace Avalonia.Controls { return; } - Debug.Assert(DoubleUtil.LessThanOrClose(_vScrollBar.Value, _vScrollBar.Maximum)); + Debug.Assert(MathUtilities.LessThanOrClose(_vScrollBar.Value, _vScrollBar.Maximum)); _verticalScrollChangesIgnored++; try @@ -2698,7 +2698,7 @@ namespace Avalonia.Controls } else if (scrollEventType == ScrollEventType.SmallDecrement) { - if (DoubleUtil.GreaterThan(NegVerticalOffset, 0)) + if (MathUtilities.GreaterThan(NegVerticalOffset, 0)) { DisplayData.PendingVerticalScrollHeight -= NegVerticalOffset; } @@ -2717,7 +2717,7 @@ namespace Avalonia.Controls DisplayData.PendingVerticalScrollHeight = _vScrollBar.Value - _verticalOffset; } - if (!DoubleUtil.IsZero(DisplayData.PendingVerticalScrollHeight)) + if (!MathUtilities.IsZero(DisplayData.PendingVerticalScrollHeight)) { // Invalidate so the scroll happens on idle InvalidateRowsMeasure(invalidateIndividualElements: false); @@ -3346,22 +3346,22 @@ namespace Avalonia.Controls bool needHorizScrollbarWithoutVertScrollbar = false; if (allowHorizScrollbar && - DoubleUtil.GreaterThan(totalVisibleWidth, cellsWidth) && - DoubleUtil.LessThan(totalVisibleFrozenWidth, cellsWidth) && - DoubleUtil.LessThanOrClose(horizScrollBarHeight, cellsHeight)) + MathUtilities.GreaterThan(totalVisibleWidth, cellsWidth) && + MathUtilities.LessThan(totalVisibleFrozenWidth, cellsWidth) && + MathUtilities.LessThanOrClose(horizScrollBarHeight, cellsHeight)) { double oldDataHeight = cellsHeight; cellsHeight -= horizScrollBarHeight; Debug.Assert(cellsHeight >= 0); needHorizScrollbarWithoutVertScrollbar = needHorizScrollbar = true; - if (allowVertScrollbar && (DoubleUtil.LessThanOrClose(totalVisibleWidth - cellsWidth, vertScrollBarWidth) || - DoubleUtil.LessThanOrClose(cellsWidth - totalVisibleFrozenWidth, vertScrollBarWidth))) + if (allowVertScrollbar && (MathUtilities.LessThanOrClose(totalVisibleWidth - cellsWidth, vertScrollBarWidth) || + MathUtilities.LessThanOrClose(cellsWidth - totalVisibleFrozenWidth, vertScrollBarWidth))) { // Would we still need a horizontal scrollbar without the vertical one? UpdateDisplayedRows(DisplayData.FirstScrollingSlot, cellsHeight); if (DisplayData.NumTotallyDisplayedScrollingElements != VisibleSlotCount) { - needHorizScrollbar = DoubleUtil.LessThan(totalVisibleFrozenWidth, cellsWidth - vertScrollBarWidth); + needHorizScrollbar = MathUtilities.LessThan(totalVisibleFrozenWidth, cellsWidth - vertScrollBarWidth); } } @@ -3374,8 +3374,8 @@ namespace Avalonia.Controls UpdateDisplayedRows(DisplayData.FirstScrollingSlot, cellsHeight); if (allowVertScrollbar && - DoubleUtil.GreaterThan(cellsHeight, 0) && - DoubleUtil.LessThanOrClose(vertScrollBarWidth, cellsWidth) && + MathUtilities.GreaterThan(cellsHeight, 0) && + MathUtilities.LessThanOrClose(vertScrollBarWidth, cellsWidth) && DisplayData.NumTotallyDisplayedScrollingElements != VisibleSlotCount) { cellsWidth -= vertScrollBarWidth; @@ -3389,9 +3389,9 @@ namespace Avalonia.Controls if (allowHorizScrollbar && needVertScrollbar && !needHorizScrollbar && - DoubleUtil.GreaterThan(totalVisibleWidth, cellsWidth) && - DoubleUtil.LessThan(totalVisibleFrozenWidth, cellsWidth) && - DoubleUtil.LessThanOrClose(horizScrollBarHeight, cellsHeight)) + MathUtilities.GreaterThan(totalVisibleWidth, cellsWidth) && + MathUtilities.LessThan(totalVisibleFrozenWidth, cellsWidth) && + MathUtilities.LessThanOrClose(horizScrollBarHeight, cellsHeight)) { cellsWidth += vertScrollBarWidth; cellsHeight -= horizScrollBarHeight; @@ -3422,7 +3422,7 @@ namespace Avalonia.Controls if (allowVertScrollbar) { if (cellsHeight > 0 && - DoubleUtil.LessThanOrClose(vertScrollBarWidth, cellsWidth) && + MathUtilities.LessThanOrClose(vertScrollBarWidth, cellsWidth) && DisplayData.NumTotallyDisplayedScrollingElements != VisibleSlotCount) { cellsWidth -= vertScrollBarWidth; @@ -3439,9 +3439,9 @@ namespace Avalonia.Controls if (allowHorizScrollbar) { if (cellsWidth > 0 && - DoubleUtil.LessThanOrClose(horizScrollBarHeight, cellsHeight) && - DoubleUtil.GreaterThan(totalVisibleWidth, cellsWidth) && - DoubleUtil.LessThan(totalVisibleFrozenWidth, cellsWidth)) + MathUtilities.LessThanOrClose(horizScrollBarHeight, cellsHeight) && + MathUtilities.GreaterThan(totalVisibleWidth, cellsWidth) && + MathUtilities.LessThan(totalVisibleFrozenWidth, cellsWidth)) { cellsHeight -= horizScrollBarHeight; Debug.Assert(cellsHeight >= 0); @@ -5387,7 +5387,7 @@ namespace Avalonia.Controls private void SetVerticalOffset(double newVerticalOffset) { _verticalOffset = newVerticalOffset; - if (_vScrollBar != null && !DoubleUtil.AreClose(newVerticalOffset, _vScrollBar.Value)) + if (_vScrollBar != null && !MathUtilities.AreClose(newVerticalOffset, _vScrollBar.Value)) { _vScrollBar.Value = _verticalOffset; } diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs index df2b03798a..e6cc7e5e40 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs @@ -301,7 +301,7 @@ namespace Avalonia.Controls private static bool CanResizeColumn(DataGridColumn column) { if (column.OwningGrid != null && column.OwningGrid.ColumnsInternal != null && column.OwningGrid.UsesStarSizing && - (column.OwningGrid.ColumnsInternal.LastVisibleColumn == column || !DoubleUtil.AreClose(column.OwningGrid.ColumnsInternal.VisibleEdgedColumnsWidth, column.OwningGrid.CellsWidth))) + (column.OwningGrid.ColumnsInternal.LastVisibleColumn == column || !MathUtilities.AreClose(column.OwningGrid.ColumnsInternal.VisibleEdgedColumnsWidth, column.OwningGrid.CellsWidth))) { return false; } diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumns.cs b/src/Avalonia.Controls.DataGrid/DataGridColumns.cs index 16b63ad696..5b75bc73f9 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridColumns.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridColumns.cs @@ -44,7 +44,7 @@ namespace Avalonia.Controls /// The remaining amount of adjustment. internal double AdjustColumnWidths(int displayIndex, double amount, bool userInitiated) { - if (!DoubleUtil.IsZero(amount)) + if (!MathUtilities.IsZero(amount)) { if (amount < 0) { @@ -777,7 +777,7 @@ namespace Avalonia.Controls private double AdjustStarColumnWidths(int displayIndex, double adjustment, bool userInitiated) { double remainingAdjustment = adjustment; - if (DoubleUtil.IsZero(remainingAdjustment)) + if (MathUtilities.IsZero(remainingAdjustment)) { return remainingAdjustment; } @@ -843,7 +843,7 @@ namespace Avalonia.Controls /// The remaining amount of adjustment. private double AdjustStarColumnWidths(int displayIndex, double remainingAdjustment, bool userInitiated, Func targetWidth) { - if (DoubleUtil.IsZero(remainingAdjustment)) + if (MathUtilities.IsZero(remainingAdjustment)) { return remainingAdjustment; } @@ -1244,7 +1244,7 @@ namespace Avalonia.Controls Debug.Assert(amount < 0); Debug.Assert(column.Width.UnitType != DataGridLengthUnitType.Star); - if (DoubleUtil.GreaterThanOrClose(targetWidth, column.Width.DisplayValue)) + if (MathUtilities.GreaterThanOrClose(targetWidth, column.Width.DisplayValue)) { return amount; } @@ -1271,7 +1271,7 @@ namespace Avalonia.Controls /// The remaining amount of adjustment. private double DecreaseNonStarColumnWidths(int displayIndex, Func targetWidth, double amount, bool reverse, bool affectNewColumns) { - if (DoubleUtil.GreaterThanOrClose(amount, 0)) + if (MathUtilities.GreaterThanOrClose(amount, 0)) { return amount; } @@ -1285,7 +1285,7 @@ namespace Avalonia.Controls (affectNewColumns || column.IsInitialDesiredWidthDetermined))) { amount = DecreaseNonStarColumnWidth(column, Math.Max(column.ActualMinWidth, targetWidth(column)), amount); - if (DoubleUtil.IsZero(amount)) + if (MathUtilities.IsZero(amount)) { break; } @@ -1392,7 +1392,7 @@ namespace Avalonia.Controls /// The remaining amount of adjustment. private double IncreaseNonStarColumnWidths(int displayIndex, Func targetWidth, double amount, bool reverse, bool affectNewColumns) { - if (DoubleUtil.LessThanOrClose(amount, 0)) + if (MathUtilities.LessThanOrClose(amount, 0)) { return amount; } @@ -1406,7 +1406,7 @@ namespace Avalonia.Controls (affectNewColumns || column.IsInitialDesiredWidthDetermined))) { amount = IncreaseNonStarColumnWidth(column, Math.Min(column.ActualMaxWidth, targetWidth(column)), amount); - if (DoubleUtil.IsZero(amount)) + if (MathUtilities.IsZero(amount)) { break; } diff --git a/src/Avalonia.Controls.DataGrid/DataGridLength.cs b/src/Avalonia.Controls.DataGrid/DataGridLength.cs index 6a545a35ec..4841ddd494 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridLength.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridLength.cs @@ -529,7 +529,7 @@ namespace Avalonia.Controls // in this case drop value part and print only "Star" case DataGridLengthUnitType.Star: return ( - DoubleUtil.AreClose(1.0, dataGridLength.Value.Value) + MathUtilities.AreClose(1.0, dataGridLength.Value.Value) ? _starSuffix : Convert.ToString(dataGridLength.Value.Value, culture ?? CultureInfo.CurrentCulture) + DataGridLengthConverter._starSuffix); diff --git a/src/Avalonia.Controls.DataGrid/DataGridRow.cs b/src/Avalonia.Controls.DataGrid/DataGridRow.cs index df200240ff..830eff1102 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRow.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRow.cs @@ -879,7 +879,7 @@ namespace Avalonia.Controls && (double.IsNaN(_detailsContent.Height)) && (AreDetailsVisible) && (!double.IsNaN(_detailsDesiredHeight)) - && !DoubleUtil.AreClose(_detailsContent.Bounds.Inflate(_detailsContent.Margin).Height, _detailsDesiredHeight) + && !MathUtilities.AreClose(_detailsContent.Bounds.Inflate(_detailsContent.Margin).Height, _detailsDesiredHeight) && Slot != -1) { _detailsDesiredHeight = _detailsContent.Bounds.Inflate(_detailsContent.Margin).Height; diff --git a/src/Avalonia.Controls.DataGrid/DataGridRows.cs b/src/Avalonia.Controls.DataGrid/DataGridRows.cs index fda14cf5b4..924156f5f4 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRows.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRows.cs @@ -329,7 +329,7 @@ namespace Avalonia.Controls internal void OnRowsMeasure() { - if (!DoubleUtil.IsZero(DisplayData.PendingVerticalScrollHeight)) + if (!MathUtilities.IsZero(DisplayData.PendingVerticalScrollHeight)) { ScrollSlotsByHeight(DisplayData.PendingVerticalScrollHeight); DisplayData.PendingVerticalScrollHeight = 0; @@ -432,7 +432,7 @@ namespace Avalonia.Controls } else if (DisplayData.FirstScrollingSlot == slot && slot != -1) { - if (!DoubleUtil.IsZero(NegVerticalOffset)) + if (!MathUtilities.IsZero(NegVerticalOffset)) { // First displayed row is partially scrolled of. Let's scroll it so that NegVerticalOffset becomes 0. DisplayData.PendingVerticalScrollHeight = -NegVerticalOffset; @@ -447,7 +447,7 @@ namespace Avalonia.Controls { // Scroll up to the new row so it becomes the first displayed row firstFullSlot = DisplayData.FirstScrollingSlot - 1; - if (DoubleUtil.GreaterThan(NegVerticalOffset, 0)) + if (MathUtilities.GreaterThan(NegVerticalOffset, 0)) { deltaY = -NegVerticalOffset; } @@ -470,7 +470,7 @@ namespace Avalonia.Controls // Figure out how much of the last row is cut off double rowHeight = GetExactSlotElementHeight(DisplayData.LastScrollingSlot); double availableHeight = AvailableSlotElementRoom + rowHeight; - if (DoubleUtil.AreClose(rowHeight, availableHeight)) + if (MathUtilities.AreClose(rowHeight, availableHeight)) { if (DisplayData.LastScrollingSlot == slot) { @@ -499,7 +499,7 @@ namespace Avalonia.Controls { ResetDisplayedRows(); } - if (DoubleUtil.GreaterThanOrClose(GetExactSlotElementHeight(slot), CellsHeight)) + if (MathUtilities.GreaterThanOrClose(GetExactSlotElementHeight(slot), CellsHeight)) { // The entire row won't fit in the DataGrid so we start showing it from the top NegVerticalOffset = 0; @@ -519,7 +519,7 @@ namespace Avalonia.Controls } // - Debug.Assert(DoubleUtil.LessThanOrClose(NegVerticalOffset, _verticalOffset)); + Debug.Assert(MathUtilities.LessThanOrClose(NegVerticalOffset, _verticalOffset)); SetVerticalOffset(_verticalOffset); @@ -1660,7 +1660,7 @@ namespace Avalonia.Controls private void ScrollSlotsByHeight(double height) { Debug.Assert(DisplayData.FirstScrollingSlot >= 0); - Debug.Assert(!DoubleUtil.IsZero(height)); + Debug.Assert(!MathUtilities.IsZero(height)); _scrollingByHeight = true; try @@ -1672,7 +1672,7 @@ namespace Avalonia.Controls { // Scrolling Down int lastVisibleSlot = GetPreviousVisibleSlot(SlotCount); - if (_vScrollBar != null && DoubleUtil.AreClose(_vScrollBar.Maximum, newVerticalOffset)) + if (_vScrollBar != null && MathUtilities.AreClose(_vScrollBar.Maximum, newVerticalOffset)) { // We've scrolled to the bottom of the ScrollBar, automatically place the user at the very bottom // of the DataGrid. If this produces very odd behavior, evaluate the coping strategy used by @@ -1684,7 +1684,7 @@ namespace Avalonia.Controls else { deltaY = GetSlotElementHeight(newFirstScrollingSlot) - NegVerticalOffset; - if (DoubleUtil.LessThan(height, deltaY)) + if (MathUtilities.LessThan(height, deltaY)) { // We've merely covered up more of the same row we're on NegVerticalOffset += height; @@ -1707,7 +1707,7 @@ namespace Avalonia.Controls } else { - while (DoubleUtil.LessThanOrClose(deltaY, height)) + while (MathUtilities.LessThanOrClose(deltaY, height)) { if (newFirstScrollingSlot < lastVisibleSlot) { @@ -1727,7 +1727,7 @@ namespace Avalonia.Controls double rowHeight = GetExactSlotElementHeight(newFirstScrollingSlot); double remainingHeight = height - deltaY; - if (DoubleUtil.LessThanOrClose(rowHeight, remainingHeight)) + if (MathUtilities.LessThanOrClose(rowHeight, remainingHeight)) { deltaY += rowHeight; } @@ -1744,7 +1744,7 @@ namespace Avalonia.Controls else { // Scrolling Up - if (DoubleUtil.GreaterThanOrClose(height + NegVerticalOffset, 0)) + if (MathUtilities.GreaterThanOrClose(height + NegVerticalOffset, 0)) { // We've merely exposing more of the row we're on NegVerticalOffset += height; @@ -1778,7 +1778,7 @@ namespace Avalonia.Controls else { int lastScrollingSlot = DisplayData.LastScrollingSlot; - while (DoubleUtil.GreaterThan(deltaY, height)) + while (MathUtilities.GreaterThan(deltaY, height)) { if (newFirstScrollingSlot > 0) { @@ -1797,7 +1797,7 @@ namespace Avalonia.Controls } double rowHeight = GetExactSlotElementHeight(newFirstScrollingSlot); double remainingHeight = height - deltaY; - if (DoubleUtil.LessThanOrClose(rowHeight + remainingHeight, 0)) + if (MathUtilities.LessThanOrClose(rowHeight + remainingHeight, 0)) { deltaY -= rowHeight; } @@ -1809,7 +1809,7 @@ namespace Avalonia.Controls } } } - if (DoubleUtil.GreaterThanOrClose(0, newVerticalOffset) && newFirstScrollingSlot != 0) + if (MathUtilities.GreaterThanOrClose(0, newVerticalOffset) && newFirstScrollingSlot != 0) { // We've scrolled to the top of the ScrollBar, automatically place the user at the very top // of the DataGrid. If this produces very odd behavior, evaluate the RowHeight estimate. @@ -1822,7 +1822,7 @@ namespace Avalonia.Controls } double firstRowHeight = GetExactSlotElementHeight(newFirstScrollingSlot); - if (DoubleUtil.LessThan(firstRowHeight, NegVerticalOffset)) + if (MathUtilities.LessThan(firstRowHeight, NegVerticalOffset)) { // We've scrolled off more of the first row than what's possible. This can happen // if the first row got shorter (Ex: Collpasing RowDetails) or if the user has a recycling @@ -1838,11 +1838,11 @@ namespace Avalonia.Controls UpdateDisplayedRows(newFirstScrollingSlot, CellsHeight); double firstElementHeight = GetExactSlotElementHeight(DisplayData.FirstScrollingSlot); - if (DoubleUtil.GreaterThan(NegVerticalOffset, firstElementHeight)) + if (MathUtilities.GreaterThan(NegVerticalOffset, firstElementHeight)) { int firstElementSlot = DisplayData.FirstScrollingSlot; // We filled in some rows at the top and now we have a NegVerticalOffset that's greater than the first element - while (newFirstScrollingSlot > 0 && DoubleUtil.GreaterThan(NegVerticalOffset, firstElementHeight)) + while (newFirstScrollingSlot > 0 && MathUtilities.GreaterThan(NegVerticalOffset, firstElementHeight)) { int previousSlot = GetPreviousVisibleSlot(firstElementSlot); if (previousSlot == -1) @@ -1872,7 +1872,7 @@ namespace Avalonia.Controls { _verticalOffset = NegVerticalOffset; } - else if (DoubleUtil.GreaterThan(NegVerticalOffset, newVerticalOffset)) + else if (MathUtilities.GreaterThan(NegVerticalOffset, newVerticalOffset)) { // The scrolled-in row was larger than anticipated. Adjust the DataGrid so the ScrollBar thumb // can stay in the same place @@ -1890,8 +1890,8 @@ namespace Avalonia.Controls DisplayData.FullyRecycleElements(); - Debug.Assert(DoubleUtil.GreaterThanOrClose(NegVerticalOffset, 0)); - Debug.Assert(DoubleUtil.GreaterThanOrClose(_verticalOffset, NegVerticalOffset)); + Debug.Assert(MathUtilities.GreaterThanOrClose(NegVerticalOffset, 0)); + Debug.Assert(MathUtilities.GreaterThanOrClose(_verticalOffset, NegVerticalOffset)); } finally { @@ -2032,7 +2032,7 @@ namespace Avalonia.Controls double deltaY = -NegVerticalOffset; int visibleScrollingRows = 0; - if (DoubleUtil.LessThanOrClose(displayHeight, 0) || SlotCount == 0 || ColumnsItemsInternal.Count == 0) + if (MathUtilities.LessThanOrClose(displayHeight, 0) || SlotCount == 0 || ColumnsItemsInternal.Count == 0) { return; } @@ -2044,7 +2044,7 @@ namespace Avalonia.Controls } int slot = firstDisplayedScrollingSlot; - while (slot < SlotCount && !DoubleUtil.GreaterThanOrClose(deltaY, displayHeight)) + while (slot < SlotCount && !MathUtilities.GreaterThanOrClose(deltaY, displayHeight)) { deltaY += GetExactSlotElementHeight(slot); visibleScrollingRows++; @@ -2052,7 +2052,7 @@ namespace Avalonia.Controls slot = GetNextVisibleSlot(slot); } - while (DoubleUtil.LessThan(deltaY, displayHeight) && slot >= 0) + while (MathUtilities.LessThan(deltaY, displayHeight) && slot >= 0) { slot = GetPreviousVisibleSlot(firstDisplayedScrollingSlot); if (slot >= 0) @@ -2063,14 +2063,14 @@ namespace Avalonia.Controls } } // If we're up to the first row, and we still have room left, uncover as much of the first row as we can - if (firstDisplayedScrollingSlot == 0 && DoubleUtil.LessThan(deltaY, displayHeight)) + if (firstDisplayedScrollingSlot == 0 && MathUtilities.LessThan(deltaY, displayHeight)) { double newNegVerticalOffset = Math.Max(0, NegVerticalOffset - displayHeight + deltaY); deltaY += NegVerticalOffset - newNegVerticalOffset; NegVerticalOffset = newNegVerticalOffset; } - if (DoubleUtil.GreaterThan(deltaY, displayHeight) || (DoubleUtil.AreClose(deltaY, displayHeight) && DoubleUtil.GreaterThan(NegVerticalOffset, 0))) + if (MathUtilities.GreaterThan(deltaY, displayHeight) || (MathUtilities.AreClose(deltaY, displayHeight) && MathUtilities.GreaterThan(NegVerticalOffset, 0))) { DisplayData.NumTotallyDisplayedScrollingElements = visibleScrollingRows - 1; } @@ -2108,7 +2108,7 @@ namespace Avalonia.Controls double deltaY = 0; int visibleScrollingRows = 0; - if (DoubleUtil.LessThanOrClose(displayHeight, 0) || SlotCount == 0 || ColumnsItemsInternal.Count == 0) + if (MathUtilities.LessThanOrClose(displayHeight, 0) || SlotCount == 0 || ColumnsItemsInternal.Count == 0) { ResetDisplayedRows(); return; @@ -2120,7 +2120,7 @@ namespace Avalonia.Controls } int slot = lastDisplayedScrollingRow; - while (DoubleUtil.LessThan(deltaY, displayHeight) && slot >= 0) + while (MathUtilities.LessThan(deltaY, displayHeight) && slot >= 0) { deltaY += GetExactSlotElementHeight(slot); visibleScrollingRows++; @@ -2542,7 +2542,7 @@ namespace Avalonia.Controls double heightChange = UpdateRowGroupVisibility(rowGroupInfo, isVisible, isDisplayed: false); // Use epsilon instead of 0 here so that in the off chance that our estimates put the vertical offset negative // the user can still scroll to the top since the offset is non-zero - SetVerticalOffset(Math.Max(DoubleUtil.DBL_EPSILON, _verticalOffset + heightChange)); + SetVerticalOffset(Math.Max(MathUtilities.DoubleEpsilon, _verticalOffset + heightChange)); } else { @@ -3024,4 +3024,4 @@ namespace Avalonia.Controls } #endif } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs index 0f513e7f42..6e0703c90f 100644 --- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs +++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs @@ -310,9 +310,9 @@ namespace Avalonia.Controls.Primitives double leftEdge = column.IsFrozen ? frozenLeftEdge : scrollingLeftEdge; double rightEdge = leftEdge + column.ActualWidth; return - DoubleUtil.GreaterThan(rightEdge, 0) && - DoubleUtil.LessThanOrClose(leftEdge, OwningGrid.CellsWidth) && - DoubleUtil.GreaterThan(rightEdge, frozenLeftEdge); // scrolling column covered up by frozen column(s) + MathUtilities.GreaterThan(rightEdge, 0) && + MathUtilities.LessThanOrClose(leftEdge, OwningGrid.CellsWidth) && + MathUtilities.GreaterThan(rightEdge, frozenLeftEdge); // scrolling column covered up by frozen column(s) } } } diff --git a/src/Avalonia.Controls.DataGrid/Utils/DoubleUtil.cs b/src/Avalonia.Controls.DataGrid/Utils/DoubleUtil.cs deleted file mode 100644 index ec0e8836d7..0000000000 --- a/src/Avalonia.Controls.DataGrid/Utils/DoubleUtil.cs +++ /dev/null @@ -1,136 +0,0 @@ -// (c) Copyright Microsoft Corporation. -// This source is subject to the Microsoft Public License (Ms-PL). -// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. -// All other rights reserved. - -using System; - -namespace Avalonia.Controls.Utils -{ - internal static class DoubleUtil - { - internal const double DBL_EPSILON = 1e-6; - - /// - /// AreClose - Returns whether or not two doubles are "close". That is, whether or - /// not they are within epsilon of each other. Note that this epsilon is proportional - /// to the numbers themselves to that AreClose survives scalar multiplication. - /// There are plenty of ways for this to return false even for numbers which - /// are theoretically identical, so no code calling this should fail to work if this - /// returns false. This is important enough to repeat: - /// NB: NO CODE CALLING THIS FUNCTION SHOULD DEPEND ON ACCURATE RESULTS - this should be - /// used for optimizations *only*. - /// - /// - /// bool - the result of the AreClose comparison. - /// - /// 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; - // This computes (|value1-value2| / (|value1| + |value2| + 10.0)) < DBL_EPSILON - double eps = (Math.Abs(value1) + Math.Abs(value2) + 10.0) * DBL_EPSILON; - double delta = value1 - value2; - return (-eps < delta) && (eps > delta); - } - - /// - /// 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. Note that this epsilon is proportional to the numbers themselves - /// to that AreClose survives scalar multiplication. Note, - /// There are plenty of ways for this to return false even for numbers which - /// are theoretically identical, so no code calling this should fail to work if this - /// returns false. This is important enough to repeat: - /// NB: NO CODE CALLING THIS FUNCTION SHOULD DEPEND ON ACCURATE RESULTS - this should be - /// used for optimizations *only*. - /// - /// - /// bool - the result of the GreaterThan comparison. - /// - /// The first double to compare. - /// The second double to compare. - public static bool GreaterThan(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. Note that this epsilon is proportional to the numbers - /// themselves to that AreClose survives scalar multiplication. Note, - /// There are plenty of ways for this to return false even for numbers which - /// are theoretically identical, so no code calling this should fail to work if this - /// returns false. This is important enough to repeat: - /// NB: NO CODE CALLING THIS FUNCTION SHOULD DEPEND ON ACCURATE RESULTS - this should be - /// used for optimizations *only*. - /// - /// - /// bool - the result of the GreaterThanOrClose comparison. - /// - /// The first double to compare. - /// The second double to compare. - public static bool GreaterThanOrClose(double value1, double value2) - { - return (value1 > value2) || AreClose(value1, value2); - } - - /// - /// IsZero - Returns whether or not the double is "close" to 0. Same as AreClose(double, 0), - /// but this is faster. - /// - /// - /// bool - the result of the IsZero comparison. - /// - /// The double to compare to 0. - public static bool IsZero(double value) - { - return Math.Abs(value) < 10.0 * DBL_EPSILON; - } - - /// - /// 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. Note that this epsilon is proportional to the numbers themselves - /// to that AreClose survives scalar multiplication. Note, - /// There are plenty of ways for this to return false even for numbers which - /// are theoretically identical, so no code calling this should fail to work if this - /// returns false. This is important enough to repeat: - /// NB: NO CODE CALLING THIS FUNCTION SHOULD DEPEND ON ACCURATE RESULTS - this should be - /// used for optimizations *only*. - /// - /// - /// bool - the result of the LessThan comparison. - /// - /// The first double to compare. - /// The second double to compare. - public static bool LessThan(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. Note that this epsilon is proportional to the numbers - /// themselves to that AreClose survives scalar multiplication. Note, - /// There are plenty of ways for this to return false even for numbers which - /// are theoretically identical, so no code calling this should fail to work if this - /// returns false. This is important enough to repeat: - /// NB: NO CODE CALLING THIS FUNCTION SHOULD DEPEND ON ACCURATE RESULTS - this should be - /// used for optimizations *only*. - /// - /// - /// bool - the result of the LessThanOrClose comparison. - /// - /// The first double to compare. - /// The second double to compare. - public static bool LessThanOrClose(double value1, double value2) - { - return (value1 < value2) || AreClose(value1, value2); - } - } -} diff --git a/tests/Avalonia.Base.UnitTests/Utilities/MathUtilitiesTests.cs b/tests/Avalonia.Base.UnitTests/Utilities/MathUtilitiesTests.cs index 0378a5b017..a12d07b8ef 100644 --- a/tests/Avalonia.Base.UnitTests/Utilities/MathUtilitiesTests.cs +++ b/tests/Avalonia.Base.UnitTests/Utilities/MathUtilitiesTests.cs @@ -92,14 +92,14 @@ namespace Avalonia.Base.UnitTests.Utilities } [Fact] - public void Clamp_Input_NaN_Return_NaN() + public void Float_Clamp_Input_NaN_Return_NaN() { var clamp = MathUtilities.Clamp(double.NaN, 0.0, 1.0); Assert.True(double.IsNaN(clamp)); } [Fact] - public void Clamp_Input_NegativeInfinity_Return_Min() + public void Float_Clamp_Input_NegativeInfinity_Return_Min() { const double min = 0.0; const double max = 1.0; @@ -108,12 +108,115 @@ namespace Avalonia.Base.UnitTests.Utilities } [Fact] - public void Clamp_Input_PositiveInfinity_Return_Max() + public void Float_Clamp_Input_PositiveInfinity_Return_Max() { const double min = 0.0; const double max = 1.0; var actual = MathUtilities.Clamp(double.PositiveInfinity, min, max); Assert.Equal(max, actual); } + + [Fact] + public void Double_Float_Zero_Less_Than_One() + { + var actual = MathUtilities.LessThan(0d, 1d); + Assert.True(actual); + } + + [Fact] + public void Single_Float_Zero_Less_Than_One() + { + var actual = MathUtilities.LessThan(0f, 1f); + Assert.True(actual); + } + + [Fact] + public void Double_Float_One_Not_Less_Than_Zero() + { + var actual = MathUtilities.LessThan(1d, 0d); + Assert.False(actual); + } + + [Fact] + public void Single_Float_One_Not_Less_Than_Zero() + { + var actual = MathUtilities.LessThan(1f, 0f); + Assert.False(actual); + } + + [Fact] + public void Double_Float_Zero_Not_Greater_Than_One() + { + var actual = MathUtilities.GreaterThan(0d, 1d); + Assert.False(actual); + } + + [Fact] + public void Single_Float_Zero_Not_Greater_Than_One() + { + var actual = MathUtilities.GreaterThan(0f, 1f); + Assert.False(actual); + } + + [Fact] + public void Double_Float_One_Greater_Than_Zero() + { + var actual = MathUtilities.GreaterThan(1d, 0d); + Assert.True(actual); + } + + [Fact] + public void Single_Float_One_Greater_Than_Zero() + { + var actual = MathUtilities.GreaterThan(1f, 0f); + Assert.True(actual); + } + + [Fact] + public void Double_Float_One_Less_Than_Or_Close_One() + { + var actual = MathUtilities.LessThanOrClose(1d, 1d); + Assert.True(actual); + } + + [Fact] + public void Single_Float_One_Less_Than_Or_Close_One() + { + var actual = MathUtilities.LessThanOrClose(1f, 1f); + Assert.True(actual); + } + + [Fact] + public void Double_Float_One_Greater_Than_Or_Close_One() + { + var actual = MathUtilities.GreaterThanOrClose(1d, 1d); + Assert.True(actual); + } + + [Fact] + public void Single_Float_One_Greater_Than_Or_Close_One() + { + var actual = MathUtilities.GreaterThanOrClose(1f, 1f); + Assert.True(actual); + } + + [Fact] + public void Round_Layout_Value_Without_DPI_Aware() + { + const double value = 42.5; + var expectedValue = Math.Round(value); + var actualValue = MathUtilities.RoundLayoutValue(value, 1.0); + Assert.Equal(expectedValue, actualValue); + } + + [Fact] + public void Round_Layout_Value_With_DPI_Aware() + { + const double dpiScale = 1.25; + const double value = 42.5; + var expectedValue = Math.Round(value * dpiScale) / dpiScale; + var actualValue = MathUtilities.RoundLayoutValue(value, dpiScale); + Assert.Equal(expectedValue, actualValue); + } } }