From bd0b8942714592fedfef1e84886d31cade346d37 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 19 May 2020 14:08:06 +0200 Subject: [PATCH] Fix UseLayoutRounding calculations. Ported `RoundLayoutSize` and `RoundLayoutValue` from WPF and use these to calculated layout rounding. Fixes #3467. --- .../Presenters/ContentPresenter.cs | 12 ++-- src/Avalonia.Layout/LayoutHelper.cs | 58 ++++++++++++++++++ src/Avalonia.Layout/Layoutable.cs | 17 +++-- .../Primitives/TrackTests.cs | 4 +- .../Path_Expander_With_Border.expected.png | Bin 1281 -> 1284 bytes .../Path_Expander_With_Border.expected.png | Bin 1281 -> 1222 bytes 6 files changed, 71 insertions(+), 20 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index 50aa8a9e71..f75184c686 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -365,12 +365,8 @@ namespace Avalonia.Controls.Presenters if (useLayoutRounding) { - sizeForChild = new Size( - Math.Ceiling(sizeForChild.Width * scale) / scale, - Math.Ceiling(sizeForChild.Height * scale) / scale); - availableSize = new Size( - Math.Ceiling(availableSize.Width * scale) / scale, - Math.Ceiling(availableSize.Height * scale) / scale); + sizeForChild = LayoutHelper.RoundLayoutSize(sizeForChild, scale, scale); + availableSize = LayoutHelper.RoundLayoutSize(availableSize, scale, scale); } switch (horizontalContentAlignment) @@ -395,8 +391,8 @@ namespace Avalonia.Controls.Presenters if (useLayoutRounding) { - originX = Math.Floor(originX * scale) / scale; - originY = Math.Floor(originY * scale) / scale; + originX = LayoutHelper.RoundLayoutValue(originX, scale); + originY = LayoutHelper.RoundLayoutValue(originY, scale); } var boundsForChild = diff --git a/src/Avalonia.Layout/LayoutHelper.cs b/src/Avalonia.Layout/LayoutHelper.cs index 2b61de00a7..d8fa00deb7 100644 --- a/src/Avalonia.Layout/LayoutHelper.cs +++ b/src/Avalonia.Layout/LayoutHelper.cs @@ -82,6 +82,64 @@ namespace Avalonia.Layout InnerInvalidateMeasure(control); } + /// + /// Rounds a size to integer values for layout purposes, compensating for high DPI screen + /// coordinates. + /// + /// Input size. + /// DPI along x-dimension. + /// DPI along y-dimension. + /// Value of size that will be rounded under screen 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 the UseLayoutRounding property and should not be used as a general rounding + /// utility. + /// + public static Size RoundLayoutSize(Size size, double dpiScaleX, double dpiScaleY) + { + return new Size(RoundLayoutValue(size.Width, dpiScaleX), RoundLayoutValue(size.Height, dpiScaleY)); + } + + /// + /// 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 the 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; + } + + /// /// Calculates the min and max height for a control. Ported from WPF. /// diff --git a/src/Avalonia.Layout/Layoutable.cs b/src/Avalonia.Layout/Layoutable.cs index cbeab0482e..ce5200f4a4 100644 --- a/src/Avalonia.Layout/Layoutable.cs +++ b/src/Avalonia.Layout/Layoutable.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Logging; +using Avalonia.Utilities; using Avalonia.VisualTree; namespace Avalonia.Layout @@ -545,8 +546,8 @@ namespace Avalonia.Layout if (UseLayoutRounding) { var scale = GetLayoutScale(); - width = Math.Ceiling(width * scale) / scale; - height = Math.Ceiling(height * scale) / scale; + width = LayoutHelper.RoundLayoutValue(width, scale); + height = LayoutHelper.RoundLayoutValue(height, scale); } return NonNegative(new Size(width, height).Inflate(margin)); @@ -623,12 +624,8 @@ namespace Avalonia.Layout if (useLayoutRounding) { - size = new Size( - Math.Ceiling(size.Width * scale) / scale, - Math.Ceiling(size.Height * scale) / scale); - availableSizeMinusMargins = new Size( - Math.Ceiling(availableSizeMinusMargins.Width * scale) / scale, - Math.Ceiling(availableSizeMinusMargins.Height * scale) / scale); + size = LayoutHelper.RoundLayoutSize(size, scale, scale); + availableSizeMinusMargins = LayoutHelper.RoundLayoutSize(availableSizeMinusMargins, scale, scale); } size = ArrangeOverride(size).Constrain(size); @@ -657,8 +654,8 @@ namespace Avalonia.Layout if (useLayoutRounding) { - originX = Math.Floor(originX * scale) / scale; - originY = Math.Floor(originY * scale) / scale; + originX = LayoutHelper.RoundLayoutValue(originX, scale); + originY = LayoutHelper.RoundLayoutValue(originY, scale); } Bounds = new Rect(originX, originY, size.Width, size.Height); diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/TrackTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/TrackTests.cs index f4001a8ca1..59276a94d0 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/TrackTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/TrackTests.cs @@ -67,7 +67,7 @@ namespace Avalonia.Controls.UnitTests.Primitives target.Measure(new Size(100, 100)); target.Arrange(new Rect(0, 0, 100, 100)); - Assert.Equal(new Rect(33, 0, 34, 12), thumb.Bounds); + Assert.Equal(new Rect(33, 0, 33, 12), thumb.Bounds); } [Fact] @@ -92,7 +92,7 @@ namespace Avalonia.Controls.UnitTests.Primitives target.Measure(new Size(100, 100)); target.Arrange(new Rect(0, 0, 100, 100)); - Assert.Equal(new Rect(0, 33, 12, 34), thumb.Bounds); + Assert.Equal(new Rect(0, 33, 12, 33), thumb.Bounds); } [Fact] diff --git a/tests/TestFiles/Direct2D1/Shapes/Path/Path_Expander_With_Border.expected.png b/tests/TestFiles/Direct2D1/Shapes/Path/Path_Expander_With_Border.expected.png index 4a7f4c26b06e0d7fefff05e0f1089072c0cd23db..7a7d70a521f2001728cddf42d0bb46b1752b8827 100644 GIT binary patch literal 1284 zcmWmEeM}Q)90%}noO)G+dvP1OQ=eY1txiI;Q&ZpAK~MTrK&0BKj5#)oW@0w45CguT zrzEyg%k>6|fxsWuWM;^OWGai(*)+67e8p_A;T9`oW5#I^S)5Gw+#k>9$@9(kzuzy9 zf5Upm%;|Hc>vX!APAIz29oG&q6?uM3M=QFLDqM~nUB@kU6g}{r*#+4;U1$A_$xo-C zXNObF-c+^i$xk;z50;a{UCWnOyjjHyFw2Opq29PB_qTyX{-)W^=hgFT1p2|j_3q6~ zdE&!|{U-mnr*2%j-T!**fJfIqvaaG<1}Pw|_AGohsX56=8o|Ey;J;MU2ko4@z_ zYBRg0+?!l-0{+Qu2b$%`fs6`Gem`={`5eu&>onV4n&Dz()A`Rd&+bB2eeFeF{$`)1 z6WkUfZtUe+{s2%eiII1)myn<2mB(U)k%!DQK6V%$XX4wrh56vQFT6nRv#3Y#t^hnv z#l74@5j^&V3Ax>(VtCgHs50@roHHLB^zjSjaidy`-#Z0W3LfH|B6vkQX>70_(q#Oq zNVf;E zPp0ErL~FVnaPY}gyh*eYav+aSCgTI5*D41Jc?%6wxD|dOLH6o#x(=qAl`7E->fqZnT*GZeq2FVc@0--wxT_h?BMGkC4wiD=*EvK@?1JY9NdNA-4nZEI_y$$Vo*9;o~(Gnfm; z6)SRg<8sZ=N@ROumSz!$%y64Ei{3-_xwU>@k=9@B()vRiwf>Mx>z5dV?_dwtNLM&= z8xvcFJ)9<$7|1!&5Fn${F<}=IOUBOSvF{UMv((2c`=XtPg+9U5E^ z4Q`&qLeS}ZREqiStc8Rr37cQ&u>={tXLrJOTIewalL$|KBE44VF$QJAlaWXd2t9^i zlHQY&NN*5sS%R{DxFnI@BHS_tb+|WG!VStJzmY`C$D-x4B?t&&q+8A)oLs?XY5_4py%ZqtK*0iH4eli IoYEuz0}8C+ZU6uP literal 1281 zcmWlZd2ka|9LK|U+rAAwHVv1sK(l#iz;cu-W6Dup(@m3D(qIIqNCnFTZ4m|*530iz z*eO}tS$5Y@+M$C2t3y!`>IEKyz-k*&W^fc~XGFRLY&aZ^GsqNleEY|K-n{Sr{&;V` zpIvcDoj>0?(Q2_+@`C{`g4cfY0Xdk*>W?45%cz$ley`>DMJA0mom5FV%mw4&A2vPi)ISjQ5KUKiorKU33>z=KZ~Og0du} zp*Q!Oj|*!LWk)dC9Lz@#nf!sx#S9%X`ExmmxvbaZH|6G}uh-;2xjDJK*JQ9OYdUxC z$eq|2c4$_tK@|3rnYRNi)y1<#VLX}X2;BDnm{-0 zK`*hq1|~tYLV>LODyItQvOP!;12yn06`ik;CGtG3D1mO-f{5s;g=c|QsgSI^H4Z`S zjvtN!(N%1^8{KoO5|x_3rV~hVss&W4o=vwS38x^%uxSBZ#3@KMu+81*qFZgHw0yQX zfd(CF4yDaxo7>T#Q*8xWh;0_opi9jG+IqI43k|weJEaw}6(SmOsCJ;uW-B_;h*Qm? zv@lx{Mizn9tJbA{lm9?^wt6Y2>+Q z8cmKnX!6-1lM|;Kn2~0aMLi}P(k7Yd=G(-_{1~Ef@hy)vwlO?nvdwbYEoyU;Y9-;L-#5KEOv6p^fD9s|(Le-Gs?`pF$=ldaE7B&(TH0CV{dpnP9H8AGe> zeKmww%ZvrEgkQ?aE&5XmT5a>MA;fBCJ{`z2uHdrV(qu~8U|hkqxFpadfN5Uq_Zdu5Rl@pW6HziREF20@2AlbjUY!C5}35{&J!Vhg5V54_$Jz5m;P&U01r% zANE}0bTzY}sDFiVZEEaGSg?}r-)>yX$6w*D{UlpZ6D{PJ_V1Xur!z5^=F4ioMf%xw z1B5NNEo$YM867T~Kk;NM4%c4FvgPjQ3OHunI6S$;rz5>;?j=0A%x6vgQ)aw#baVKN vmwubIZ+H0oe_Iy*xMOH__Nf*7E==uvqik_1Giee2jah=eI_|jl`JMj*ZcOVB diff --git a/tests/TestFiles/Skia/Shapes/Path/Path_Expander_With_Border.expected.png b/tests/TestFiles/Skia/Shapes/Path/Path_Expander_With_Border.expected.png index 4a7f4c26b06e0d7fefff05e0f1089072c0cd23db..7727284050259992e1dc6d2daf95d82b4d38fb5b 100644 GIT binary patch literal 1222 zcmWlZeM}Q)9LGz2go6OPmrRD7`t-_+!a&<>dNRb)9rkIhWfDM1$Oclzh;E}SbKvV9 z=|az7;Z7oELtp{2=|GubneHWv?lP{9EQXNi;zso7&_tYXTO4l7_Vkjw=kELYe*bvB zd44Cq;&Y`WEl)C;Oet<8m6KJE-Bdhzj_o|#PZscw(_KN4>j32skJZ_~YBA|u}zdlLWqufkj3Zg_C({(dN=ODCSW zTXsJ#2R&TZX{m>d@Ub$ArBE>5p1@kz)GI4d5Q&3M0_r}=je!-pO9@ziW*Gt32Y1E5 z$e9>;HaC`d_0w2lC+vPtxMjirRy7J*`Vh;Z48iSz_d=N#JdvgP!E`@j z8GWOq1|2_HRRCHF6t557<{}zsNm0BWc#Vk!K}){k-2$(15eZCpplnO`A;}(x|Hx7= zB@S;^lD)8=jtl~Os*;RgJrk(~_LWMq1nb!d1oi?Y*#YZ0ccVFd02Q)2FHOsid5f9~ z>}g7k7d~bp9{_ufQX|2~Y-B00Z&3P(%0*@afg@-c6{M7+EV@{Z z#^DF2&>@=IEJri&gAgjFf{hYuuB%=|;BwxGm7#X`9FyQ^rJcp&r5A}dohu+Upfz<8M)1A7}{i5oq!#ORJ#V%!&rf!z(U#E+gy z#zOb!t#Sn&tzLc=xLjuZwFMr?36s~HKoO(`?O~6Do-~u~htoiLuUJJoF?PD)pP~9C zq^K|{szcmM+B2@Z;i5ceB9(?orM=>E8u^SjB)AwFvXJt_r2G!i2fKMKz~ZSgK8MwR z(E@fnwUtk!^*$}Y;3*fM#_Ba%z=o$@=2uYuRO5~W=Qx+koc@tm$mtg}n+bQi`F2`w z)oe8GEalr7UDRw4cb4$&EIMpFchd2coxBOAyTokRU98y>aZMS|G5QypEgjdmc#hRy z)NE^TA5l5|qGsEO`&@h?73B@&g!g1CLk@PB9PF^jvM9qq9ylpmZDa((QE+}hOrrH- zW3?0BkcVh8EMYP%{bCZU7aFTQ@SHrf4nNPi0_HX!Pel_0G-Ga)_yc7A0%ZRFliObd zk9>SlCLI_b+y{-??Y}v+OIs_KFbSVkBuo5gRX!o^5hA``X=W0>uAn4-;lw67(2VAZ xSXWJ)+x*1&!twc&v%>7Yw{OnoUpn~jl5=bKBu;ZBUF64Pa+ms~v(DNh{{wkey+;55 literal 1281 zcmWlZd2ka|9LK|U+rAAwHVv1sK(l#iz;cu-W6Dup(@m3D(qIIqNCnFTZ4m|*530iz z*eO}tS$5Y@+M$C2t3y!`>IEKyz-k*&W^fc~XGFRLY&aZ^GsqNleEY|K-n{Sr{&;V` zpIvcDoj>0?(Q2_+@`C{`g4cfY0Xdk*>W?45%cz$ley`>DMJA0mom5FV%mw4&A2vPi)ISjQ5KUKiorKU33>z=KZ~Og0du} zp*Q!Oj|*!LWk)dC9Lz@#nf!sx#S9%X`ExmmxvbaZH|6G}uh-;2xjDJK*JQ9OYdUxC z$eq|2c4$_tK@|3rnYRNi)y1<#VLX}X2;BDnm{-0 zK`*hq1|~tYLV>LODyItQvOP!;12yn06`ik;CGtG3D1mO-f{5s;g=c|QsgSI^H4Z`S zjvtN!(N%1^8{KoO5|x_3rV~hVss&W4o=vwS38x^%uxSBZ#3@KMu+81*qFZgHw0yQX zfd(CF4yDaxo7>T#Q*8xWh;0_opi9jG+IqI43k|weJEaw}6(SmOsCJ;uW-B_;h*Qm? zv@lx{Mizn9tJbA{lm9?^wt6Y2>+Q z8cmKnX!6-1lM|;Kn2~0aMLi}P(k7Yd=G(-_{1~Ef@hy)vwlO?nvdwbYEoyU;Y9-;L-#5KEOv6p^fD9s|(Le-Gs?`pF$=ldaE7B&(TH0CV{dpnP9H8AGe> zeKmww%ZvrEgkQ?aE&5XmT5a>MA;fBCJ{`z2uHdrV(qu~8U|hkqxFpadfN5Uq_Zdu5Rl@pW6HziREF20@2AlbjUY!C5}35{&J!Vhg5V54_$Jz5m;P&U01r% zANE}0bTzY}sDFiVZEEaGSg?}r-)>yX$6w*D{UlpZ6D{PJ_V1Xur!z5^=F4ioMf%xw z1B5NNEo$YM867T~Kk;NM4%c4FvgPjQ3OHunI6S$;rz5>;?j=0A%x6vgQ)aw#baVKN vmwubIZ+H0oe_Iy*xMOH__Nf*7E==uvqik_1Giee2jah=eI_|jl`JMj*ZcOVB