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/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs index 64befe2e5c..33d1b30e3c 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) { 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/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()) 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) { 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); + } } } diff --git a/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs new file mode 100644 index 0000000000..b1a2d00b00 --- /dev/null +++ b/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs @@ -0,0 +1,106 @@ +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; +using System.Runtime.InteropServices; + +#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() + { + // Skip test on OSX: text rendering is subtly different. + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + return; + + 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() + { + // Skip test on OSX: text rendering is subtly different. + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + return; + + 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 0000000000..215e83106f Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/TextFormatting/TextLayout/TextLayout_Basic.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/TextFormatting/TextLayout/TextLayout_Rotated.expected.png b/tests/TestFiles/Direct2D1/Media/TextFormatting/TextLayout/TextLayout_Rotated.expected.png new file mode 100644 index 0000000000..237a799224 Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/TextFormatting/TextLayout/TextLayout_Rotated.expected.png differ 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 0000000000..220932ff41 Binary files /dev/null and b/tests/TestFiles/Skia/Media/TextFormatting/TextLayout/TextLayout_Basic.expected.png differ 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 0000000000..26fe2b0c91 Binary files /dev/null and b/tests/TestFiles/Skia/Media/TextFormatting/TextLayout/TextLayout_Rotated.expected.png differ