From f6fce366cdec5da49ba04d1417354903be5ea378 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 14 Jul 2021 15:52:17 +0200 Subject: [PATCH 1/6] Added failing tests for TextLayout when used with a transform. --- .../Media/TextFormatting/TextLayoutTests.cs | 97 ++++++++++++++++++ .../TextLayout/TextLayout_Basic.expected.png | Bin 0 -> 2177 bytes .../TextLayout_Rotated.expected.png | Bin 0 -> 2628 bytes .../TextLayout/TextLayout_Basic.expected.png | Bin 0 -> 2013 bytes .../TextLayout_Rotated.expected.png | Bin 0 -> 2468 bytes 5 files changed, 97 insertions(+) create mode 100644 tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs create mode 100644 tests/TestFiles/Direct2D1/Media/TextFormatting/TextLayout/TextLayout_Basic.expected.png create mode 100644 tests/TestFiles/Direct2D1/Media/TextFormatting/TextLayout/TextLayout_Rotated.expected.png create mode 100644 tests/TestFiles/Skia/Media/TextFormatting/TextLayout/TextLayout_Basic.expected.png create mode 100644 tests/TestFiles/Skia/Media/TextFormatting/TextLayout/TextLayout_Rotated.expected.png diff --git a/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs new file mode 100644 index 0000000000..c0a27eb9ca --- /dev/null +++ b/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs @@ -0,0 +1,97 @@ +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Media.TextFormatting; +using Avalonia.Platform; +using Avalonia.Utilities; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +#if AVALONIA_SKIA +namespace Avalonia.Skia.RenderTests +#else + +using Avalonia.Direct2D1.RenderTests; + +namespace Avalonia.Direct2D1.RenderTests.Media +#endif +{ + public class TextLayoutTests : TestBase + { + public TextLayoutTests() + : base(@"Media\TextFormatting\TextLayout") + { + } + + [Fact] + public async Task TextLayout_Basic() + { + var t = new TextLayout( + "Avalonia!", + new Typeface(TestFontFamily), + 24, + Brushes.Black); + + var target = new Border + { + Width = 200, + Height = 200, + Background = Brushes.White, + Child = new DrawnControl(c => + { + var textRect = new Rect(t.Size); + var bounds = new Rect(0, 0, 200, 200); + var rect = bounds.CenterRect(textRect); + c.DrawRectangle(Brushes.Yellow, null, rect); + t.Draw(c, rect.Position); + }), + }; + + await RenderToFile(target); + CompareImages(); + } + + [Fact] + public async Task TextLayout_Rotated() + { + var t = new TextLayout( + "Avalonia!", + new Typeface(TestFontFamily), + 24, + Brushes.Black); + + var target = new Border + { + Width = 200, + Height = 200, + Background = Brushes.White, + Child = new DrawnControl(c => + { + var textRect = new Rect(t.Size); + var bounds = new Rect(0, 0, 200, 200); + var rect = bounds.CenterRect(textRect); + var rotate = Matrix.CreateTranslation(-100, -100) * + Matrix.CreateRotation(MathUtilities.Deg2Rad(90)) * + Matrix.CreateTranslation(100, 100); + using var transform = c.PushPreTransform(rotate); + c.DrawRectangle(Brushes.Yellow, null, rect); + t.Draw(c, rect.Position); + }), + }; + + await RenderToFile(target); + CompareImages(); + } + + private class DrawnControl : Control + { + private readonly Action _render; + public DrawnControl(Action render) => _render = render; + public override void Render(DrawingContext context) => _render(context); + } + } +} diff --git a/tests/TestFiles/Direct2D1/Media/TextFormatting/TextLayout/TextLayout_Basic.expected.png b/tests/TestFiles/Direct2D1/Media/TextFormatting/TextLayout/TextLayout_Basic.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..215e83106f449cded8d491e55c90a8633372394b GIT binary patch literal 2177 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 literal 0 HcmV?d00001 diff --git a/tests/TestFiles/Skia/Media/TextFormatting/TextLayout/TextLayout_Basic.expected.png b/tests/TestFiles/Skia/Media/TextFormatting/TextLayout/TextLayout_Basic.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..220932ff41084338bb176fec16989ad4a51f1d21 GIT binary patch literal 2013 zcmd^==~t2o6vioQG|8dVvQ?~uS+-cYi%N?|T1h$PRtk-Z85A`oC4#oOlo^ewWR#4y z*rE`UT3JwPW2H!_X^Qe@i=-^BvGY32oHKt#_x$dCK0Wt5=YF{tc88&DtQT2BAP}37 z?LpyYee-Wy{bHU~(d<&Q%s9R!B*MyERI9{X2*k25BxrNQsT;~Z0l_|HN%I&<#fIj1 zmCtXuaQ3L{p5^y)qgR5sTl2jxR_kE4YnohF*%P~5vu^M8!X87o z=Yflkn|jZdnBpd#cHhGa6`FF(v8@>MX~gFa`X0n*&I_=ze;|4{>4>v~&as;D9|%i3 zJA6O@3j2=TReqRMzp%lqBpTQPhR&?*&p zr|Ub3WHQZ>M@y=nTL`Y~>bud?cC!kQ$%M7U7+_CTQ|qN3&;%YYDJJgZrI1xOdZaJ# znh}mDA#CThxcWg_c+>hGB6DiRHMRE5nN|+PMFQKm$!+Wv!V2;|mn=9_G0@zCzyw~WoT#>i`5&^yb zr9@${VGIp*8FvVgNG5-3^xA;t(1(JoEY{SzKIy^R&5KK! zCC){mMv_dn_I|gi9gNX|#-J{SG0}KP(d|gwWib5%`avbi*=DYRHiKr?zWzTjy zst~#dswN&|>mI$gc9$Qkw?RBFBh*Zu>V>!e@6WkYzGM$M9o70^FRFIx^J`IE&Qg$<9OYG9<5OuIzk?C6=zvrrp{O|6 zW|=R|FT+G30h~nLdD@j-%r5+zwAF{5287g6oL;{*hk8BLL*DMp%J#;s_x#X10MxmG zb;7$NO)yx+$?M&pZ+B@%0{dSe(@T{Sntm+7sOIyj&5sNHi!r$=qSBg$3DVGo=6FeX z@^FMc>&YoGN91=sb^JDl1cbrgru}$V5yftTPrQbrQpjWF9IiL%$jzmk?uoSc4F~dL z$-!$`&6#A+`e6M%OqeB{K?rxk=_r>8yxh#^k^%Fs|9chiw?< zCwUZ0)jLlRKyXR(Y~b&WUU(vQKan=P6jeI)g%c!gNye`LpB06RbHwex8hwI%HEdi&y}Jw$%IpkOAsTl@L4r?@F}%H-VVS~ z5!l+s=XWHQvk0AbE|XNoI-N_?E%X>B+dY{|kr$I3I}h4COD*m__*J-QmD%o`pnN`Zgn^OWF-hi($)ebpEA3P1 zbBw^~$mY{F=Qcsk{7+dTnyVE(e3cn*LtH@oecJRhke`zVR3+W`sY9FdPe*lXVSIGbX literal 0 HcmV?d00001 diff --git a/tests/TestFiles/Skia/Media/TextFormatting/TextLayout/TextLayout_Rotated.expected.png b/tests/TestFiles/Skia/Media/TextFormatting/TextLayout/TextLayout_Rotated.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..26fe2b0c91ad39e98f97b40711fcd8cc436186af GIT binary patch literal 2468 zcmcguX;72N7G+|P03*AE0olfgxDZ4UG!P6BG{F&dL_mSCIfxHrQ6LZ!F(9(ZVifZb zB_hEESw*%GHrYl4s8K^u0wS9qVUa-CA@hOO`}KaldR^7sReh_vZ=F8p+?#m9;m9UA zO*ttksZDqs_D^vCdu_rtf@`5mQ8u_iFWcZ9Vc;Oc&Lv1m$-3gP){YVP#s$ax+=g`8 zraX+1$ibL7=^fE(PEUWU#2IqvSrvxijWz|GX1pDsYCuu)79GQ(DS)!)9*vpucQlWPF|!U5@!a|xPlt4SeU^%n;eHB>nRzJ%KTZR#sS`cdd^n8Szc!3n8Vv z<3HZuZP;OMzMZmofpz+6tqyg*!4fC4GkU9JyfUN4wL5?v61Eir=uJ;^o8mtnseFD0 z5D@daJm?BKH}+Ko+UY58mgBB00bP^qTdP@ys0~!8Q_@#qab9NT>%Xw?B4LOUIw61g zt|3Z`ip8ic$xxxQBMfXQBN&bI>}V~m7(Tzn?^kN*gF>SVq_e0H8`5|=KNkob^A1Zu z+;novPYX=B(X6EkL$J#C6N^qS40;-Db!Qvau2h81__0f=n1}pNv^oia6tZ z9p-sCIT1d#+aR)IwR1a(Irj17`0LE+^cX}Hbhx-2h3GOgthqFJtptN3zI*-vKjG@zMBKHz4bIk<>$P^XB z_Cu!+D<>%4D}ZVI8=rpVoV5YF)0>Mqh5d|ElIx zVV9CR6>4UY)fqK-DX`av;?4^1(4H{L%nqLlZgg~ms_pd+gbUj`m|Lc^YQ534zo5RZXqX`~x$7bxdfA0#e}JH*A9 zn)LTpG0<99Zs1H#YVbeYeo+mqqRpIY$s@K_#%|va8GLA~b|ILq-u_Gi>GV3o7y00; z+iqQc4762}It%ZO{f{aUCFD906B}vNAEWpCaw(p0`8`TF=)gdr{M*+uFZI%43c9Ii zh-uOf15lXW*t!(I5@)n-3=K#p?52xa)Gg4XEED2~UyDFgwV$A_+2J#Mz+)XHc!Olp2_7(+^4e)Ih zFo>0cN7*`+I&;F5%u(w2>vS##9`XG}_Jd*I3U zzeMn3q&8OQQ8RSDoO#oF-NHJ=_weGd7O=dqmgSZf(L-^r%c)ab8QrP}nihkcli;tn zjGmwy*JREX*CnB86NmM*^B=bz(;(FAL?Hwz7!-dtd`$T&2K9mGL=vFB_8U5RE-X(7 z3ySQ`gILiF)DY3KBt(sqJZp(Le-6u{WW5*oc+0D}AiMY^Tic0P*VX10^=A3bVG7~9 zf%dxnr#FZqpPooS%YhVt2q%M<0~6zHeB$H!6{(>mLw~{5zUnJAfHnx_aJW+gHN|sZ ztcSYdVxq7wb8NYdu~%BJ+qcI?yvxp

aLFHOmrhtN%Dmnhzd13j*4JmUC-)!KV80;FtzQ_>cLLfha?h``n)`I$Vm%m|(AYTYnxde< zdZ45T(vX8s({--DB4XBjG(^c$q2U1S^UksGYdi5hM!0>MKZ)6J)$|~cdZMGFI?Hj= z(GaJqI+3!}UAA_kY8>_0vAi@dEv;vSy6L1QdH8agtT+UD2VUrLDXYWYxv0v>!RG*+ zBLi}3sy=rmZtlOlQCW3<}P#s`w5fA$hz4DY~=aj4Dx#D{{2TM8Qd7Rdjl(Ew$)Dx Date: Wed, 14 Jul 2021 15:53:07 +0200 Subject: [PATCH 2/6] Use pre-transform when drawing TextLayout. Using a post-transform breaks when there's a transform applied to the drawing context. --- .../Media/TextFormatting/ShapedTextCharacters.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs index b304b19910..3c17dabbb0 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs @@ -47,7 +47,7 @@ namespace Avalonia.Media.TextFormatting /// public override void Draw(DrawingContext drawingContext, Point origin) { - using (drawingContext.PushPostTransform(Matrix.CreateTranslation(origin))) + using (drawingContext.PushPreTransform(Matrix.CreateTranslation(origin))) { if (GlyphRun.GlyphIndices.Length == 0) { From 7aa6152b340d3c65ffbdcde5761f9fc56459f2fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Mon, 23 Aug 2021 16:53:07 +0200 Subject: [PATCH 3/6] Revert "Fix skia OpacityMask push and pop methods" This reverts commit dc404b545e534ea904911ca9647f312f77e7ed6d. --- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index c9a5544672..1f186396ab 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -534,11 +534,7 @@ namespace Avalonia.Skia public void PushOpacityMask(IBrush mask, Rect bounds) { // TODO: This should be disposed - var paint = new SKPaint() - { - IsAntialias = true, - Style = SKPaintStyle.StrokeAndFill - }; + var paint = new SKPaint(); Canvas.SaveLayer(paint); _maskStack.Push(CreatePaint(paint, mask, bounds, true)); @@ -547,14 +543,7 @@ namespace Avalonia.Skia /// public void PopOpacityMask() { - using (var paint = new SKPaint - { - IsAntialias = true, - Style = SKPaintStyle.StrokeAndFill, - BlendMode = SKBlendMode.DstIn, - Color = new SKColor(0, 0, 0, 255), - ColorFilter = SKColorFilter.CreateLumaColor() - }) + using (var paint = new SKPaint { BlendMode = SKBlendMode.DstIn }) { Canvas.SaveLayer(paint); using (var paintWrapper = _maskStack.Pop()) From 51862d1b4b6cfc346c63eafd39369ca85259d6dc Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 23 Aug 2021 17:15:41 +0200 Subject: [PATCH 4/6] Don't display warning when TryCreateAndRegister succeeds. --- .../WinRT/Composition/WinUICompositorConnection.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs index 1c3c959acf..bc8f5a606c 100644 --- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs @@ -144,6 +144,7 @@ namespace Avalonia.Win32.WinRT.Composition try { TryCreateAndRegisterCore(angle); + return; } catch (Exception e) { From 9718a77fa95d86f3b6224f50234a2f17a4150313 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 23 Aug 2021 23:42:13 +0200 Subject: [PATCH 5/6] Skip TextLayout render tests on OSX. They were failing due to slightly different text rendering on that platform. --- .../Media/TextFormatting/TextLayoutTests.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs index c0a27eb9ca..b1a2d00b00 100644 --- a/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; using Xunit; +using System.Runtime.InteropServices; #if AVALONIA_SKIA namespace Avalonia.Skia.RenderTests @@ -30,6 +31,10 @@ namespace Avalonia.Direct2D1.RenderTests.Media [Fact] public async Task TextLayout_Basic() { + // Skip test on OSX: text rendering is subtly different. + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + return; + var t = new TextLayout( "Avalonia!", new Typeface(TestFontFamily), @@ -58,6 +63,10 @@ namespace Avalonia.Direct2D1.RenderTests.Media [Fact] public async Task TextLayout_Rotated() { + // Skip test on OSX: text rendering is subtly different. + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + return; + var t = new TextLayout( "Avalonia!", new Typeface(TestFontFamily), From 0f83ccb4b026d3fec9ee4864c9874aca971feff3 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 23 Aug 2021 18:30:18 -0400 Subject: [PATCH 6/6] ContentPresenter should create child without content, if template was set (#6226) * Fix #6224 * Fix data templates Match * Do not use preview features * Do not create Child if Content is null and DataTemplate was set * Update src/Avalonia.Base/Utilities/TypeUtilities.cs * Update src/Avalonia.Controls/Presenters/ContentPresenter.cs * Update src/Avalonia.Controls/Presenters/ContentPresenter.cs --- .../ControlCatalog/Pages/ComboBoxPage.xaml | 32 ++++++- src/Avalonia.Base/Utilities/TypeUtilities.cs | 34 +++++--- .../Presenters/ContentPresenter.cs | 6 +- .../Templates/FuncDataTemplate`1.cs | 6 +- .../Templates/DataTemplate.cs | 1 - .../ContentPresenterTests_Standalone.cs | 86 +++++++++++++++++++ 6 files changed, 147 insertions(+), 18 deletions(-) diff --git a/samples/ControlCatalog/Pages/ComboBoxPage.xaml b/samples/ControlCatalog/Pages/ComboBoxPage.xaml index 025b85492c..d440b7cce3 100644 --- a/samples/ControlCatalog/Pages/ComboBoxPage.xaml +++ b/samples/ControlCatalog/Pages/ComboBoxPage.xaml @@ -1,12 +1,20 @@ + xmlns:sys="using:System" + xmlns:col="using:System.Collections"> ComboBox A drop-down list. - + + + + Inline Items Inline Item 2 @@ -14,6 +22,24 @@ Inline Item 4 + + + + + Hello + World + + + + + + + + + + + + @@ -46,7 +72,7 @@ - + diff --git a/src/Avalonia.Base/Utilities/TypeUtilities.cs b/src/Avalonia.Base/Utilities/TypeUtilities.cs index 9f2308a062..179ded3549 100644 --- a/src/Avalonia.Base/Utilities/TypeUtilities.cs +++ b/src/Avalonia.Base/Utilities/TypeUtilities.cs @@ -93,13 +93,25 @@ namespace Avalonia.Utilities return !type.IsValueType || IsNullableType(type); } + ///

+ /// Returns a value indicating whether value can be casted to the specified type. + /// If value is null, checks if instances of that type can be null. + /// + /// The type to cast to + /// The value to check if cast possible + /// True if the cast is possible, otherwise false. + public static bool CanCast(object value) + { + return value is T || (value is null && AcceptsNull(typeof(T))); + } + /// /// Try to convert a value to a type by any means possible. /// - /// The type to cast to. - /// The value to cast. + /// The type to convert to. + /// The value to convert. /// The culture to use. - /// If successful, contains the cast value. + /// If successful, contains the convert value. /// True if the cast was successful, otherwise false. public static bool TryConvert(Type to, object value, CultureInfo culture, out object result) { @@ -216,10 +228,10 @@ namespace Avalonia.Utilities /// Try to convert a value to a type using the implicit conversions allowed by the C# /// language. /// - /// The type to cast to. - /// The value to cast. - /// If successful, contains the cast value. - /// True if the cast was successful, otherwise false. + /// The type to convert to. + /// The value to convert. + /// If successful, contains the converted value. + /// True if the convert was successful, otherwise false. public static bool TryConvertImplicit(Type to, object value, out object result) { if (value == null) @@ -278,8 +290,8 @@ namespace Avalonia.Utilities /// Convert a value to a type by any means possible, returning the default for that type /// if the value could not be converted. /// - /// The value to cast. - /// The type to cast to.. + /// The value to convert. + /// The type to convert to.. /// The culture to use. /// A value of . public static object ConvertOrDefault(object value, Type type, CultureInfo culture) @@ -291,8 +303,8 @@ namespace Avalonia.Utilities /// Convert a value to a type using the implicit conversions allowed by the C# language or /// return the default for the type if the value could not be converted. /// - /// The value to cast. - /// The type to cast to.. + /// The value to convert. + /// The type to convert to. /// A value of . public static object ConvertImplicitOrDefault(object value, Type type) { diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index 472727823a..1a46d84558 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -327,7 +327,11 @@ namespace Avalonia.Controls.Presenters var oldChild = Child; var newChild = content as IControl; - if (content != null && newChild == null) + // We want to allow creating Child from the Template, if Content is null. + // But it's important to not use DataTemplates, otherwise we will break content presenters in many places, + // otherwise it will blow up every ContentPresenter without Content set. + if (newChild == null + && (content != null || ContentTemplate != null)) { var dataTemplate = this.FindDataTemplate(content, ContentTemplate) ?? ( diff --git a/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs b/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs index 4a6a1c6cfb..8e7b290247 100644 --- a/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs +++ b/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs @@ -1,5 +1,7 @@ using System; +using Avalonia.Utilities; + namespace Avalonia.Controls.Templates { /// @@ -16,7 +18,7 @@ namespace Avalonia.Controls.Templates /// /// Whether the control can be recycled. public FuncDataTemplate(Func build, bool supportsRecycling = false) - : base(typeof(T), CastBuild(build), supportsRecycling) + : base(o => TypeUtilities.CanCast(o), CastBuild(build), supportsRecycling) { } @@ -63,7 +65,7 @@ namespace Avalonia.Controls.Templates /// The weakly typed function. private static Func CastMatch(Func f) { - return o => (o is T) && f((T)o); + return o => TypeUtilities.CanCast(o) && f((T)o); } /// diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs index 650534b347..b7db1a3fbb 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs @@ -9,7 +9,6 @@ namespace Avalonia.Markup.Xaml.Templates { public Type DataType { get; set; } - //we need content to be object otherwise portable.xaml is crashing [Content] [TemplateContent] public object Content { get; set; } diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs index c7aa583b6f..6b744ed79c 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs @@ -264,5 +264,91 @@ namespace Avalonia.Controls.UnitTests.Presenters // InheritanceParent is exposed via StylingParent. Assert.Same(logicalParent, ((IStyledElement)child).StylingParent); } + + [Fact] + public void Should_Create_Child_Even_With_Null_Content_When_ContentTemplate_Is_Set() + { + var target = new ContentPresenter + { + ContentTemplate = new FuncDataTemplate(_ => true, (_, __) => new TextBlock + { + Text = "Hello World" + }), + Content = null + }; + + target.UpdateChild(); + + var textBlock = Assert.IsType(target.Child); + Assert.Equal("Hello World", textBlock.Text); + } + + [Fact] + public void Should_Not_Create_Child_Even_With_Null_Content_And_DataTemplates_InsteadOf_ContentTemplate() + { + var target = new ContentPresenter + { + DataTemplates = + { + new FuncDataTemplate(_ => true, (_, __) => new TextBlock + { + Text = "Hello World" + }) + }, + Content = null + }; + + target.UpdateChild(); + + Assert.Null(target.Child); + } + + [Fact] + public void Should_Not_Create_Child_When_Content_And_Template_Are_Null() + { + var target = new ContentPresenter + { + ContentTemplate = null, + Content = null + }; + + target.UpdateChild(); + + Assert.Null(target.Child); + } + + [Fact] + public void Should_Not_Create_When_Child_Content_Is_Null_But_Expected_ValueType_With_FuncDataTemplate() + { + var target = new ContentPresenter + { + ContentTemplate = new FuncDataTemplate(_ => true, (_, __) => new TextBlock + { + Text = "Hello World" + }), + Content = null + }; + + target.UpdateChild(); + + Assert.Null(target.Child); + } + + [Fact] + public void Should_Create_Child_When_Content_Is_Null_And_Expected_NullableValueType_With_FuncDataTemplate() + { + var target = new ContentPresenter + { + ContentTemplate = new FuncDataTemplate(_ => true, (_, __) => new TextBlock + { + Text = "Hello World" + }), + Content = null + }; + + target.UpdateChild(); + + Assert.NotNull(target.Child); + } } }