diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm index 7f2bb128da..f133fa34f6 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm @@ -298,14 +298,15 @@ HRESULT WindowBaseImpl::Resize(double x, double y, AvnPlatformResizeReason reaso } @try { - lastSize = NSSize {x, y}; - - if (!_shown) { - BaseEvents->Resized(AvnSize{x, y}, reason); - } - else if(Window != nullptr) { - [Window setContentSize:lastSize]; - [Window invalidateShadow]; + if(x != lastSize.width || y != lastSize.height) { + lastSize = NSSize{x, y}; + + if (!_shown) { + BaseEvents->Resized(AvnSize{x, y}, reason); + } else if (Window != nullptr) { + [Window setContentSize:lastSize]; + [Window invalidateShadow]; + } } } @finally { diff --git a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj index ec88852feb..e52430f50b 100644 --- a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj +++ b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj @@ -9,42 +9,37 @@ 1.0 apk true + android-arm64;android-x64 - - - Resources\drawable\Icon.png - - False - False + True + + + True no-write-symbols,nodebug Hybrid True - - False - False - - - - True + + True + True - - + + - \ No newline at end of file + diff --git a/samples/ControlCatalog.Android/Properties/AndroidManifest.xml b/samples/ControlCatalog.Android/Properties/AndroidManifest.xml index aa570ec504..6f551d2b01 100644 --- a/samples/ControlCatalog.Android/Properties/AndroidManifest.xml +++ b/samples/ControlCatalog.Android/Properties/AndroidManifest.xml @@ -1,4 +1,5 @@  - + + diff --git a/samples/ControlCatalog.Android/environment.device.txt b/samples/ControlCatalog.Android/environment.device.txt new file mode 100644 index 0000000000..107d68ca1b --- /dev/null +++ b/samples/ControlCatalog.Android/environment.device.txt @@ -0,0 +1 @@ +DOTNET_DiagnosticPorts=127.0.0.1:9000,suspend diff --git a/samples/ControlCatalog.Android/environment.emulator.txt b/samples/ControlCatalog.Android/environment.emulator.txt new file mode 100644 index 0000000000..299a0ec30b --- /dev/null +++ b/samples/ControlCatalog.Android/environment.emulator.txt @@ -0,0 +1 @@ +DOTNET_DiagnosticPorts=10.0.2.2:9001,suspend diff --git a/samples/RenderDemo/Pages/GlyphRunPage.xaml b/samples/RenderDemo/Pages/GlyphRunPage.xaml index c2914e8847..7db58e5286 100644 --- a/samples/RenderDemo/Pages/GlyphRunPage.xaml +++ b/samples/RenderDemo/Pages/GlyphRunPage.xaml @@ -2,13 +2,13 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:local="clr-namespace:RenderDemo.Pages" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="RenderDemo.Pages.GlyphRunPage"> - - - - + + + diff --git a/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs b/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs index 7f85606957..674ed8e61f 100644 --- a/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs +++ b/samples/RenderDemo/Pages/GlyphRunPage.xaml.cs @@ -9,14 +9,6 @@ namespace RenderDemo.Pages { public class GlyphRunPage : UserControl { - private Image _imageControl; - private GlyphTypeface _glyphTypeface = Typeface.Default.GlyphTypeface; - private readonly Random _rand = new Random(); - private ushort[] _glyphIndices = new ushort[1]; - private char[] _characters = new char[1]; - private float _fontSize = 20; - private int _direction = 10; - public GlyphRunPage() { this.InitializeComponent(); @@ -25,19 +17,43 @@ namespace RenderDemo.Pages private void InitializeComponent() { AvaloniaXamlLoader.Load(this); + } + } + + public class GlyphRunControl : Control + { + private GlyphTypeface _glyphTypeface = Typeface.Default.GlyphTypeface; + private readonly Random _rand = new Random(); + private ushort[] _glyphIndices = new ushort[1]; + private char[] _characters = new char[1]; + private float _fontSize = 20; + private int _direction = 10; - _imageControl = this.FindControl("imageControl"); - _imageControl.Source = new DrawingImage(); + private DispatcherTimer _timer; - DispatcherTimer.Run(() => + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + _timer = new DispatcherTimer + { + Interval = TimeSpan.FromSeconds(1) + }; + + _timer.Tick += (s,e) => { - UpdateGlyphRun(); + InvalidateVisual(); + }; + + _timer.Start(); + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + _timer.Stop(); - return true; - }, TimeSpan.FromSeconds(1)); + _timer = null; } - private void UpdateGlyphRun() + public override void Render(DrawingContext context) { var c = (char)_rand.Next(65, 90); @@ -57,27 +73,70 @@ namespace RenderDemo.Pages _characters[0] = c; - var scale = (double)_fontSize / _glyphTypeface.DesignEmHeight; + var glyphRun = new GlyphRun(_glyphTypeface, _fontSize, _characters, _glyphIndices); - var drawingGroup = new DrawingGroup(); + context.DrawGlyphRun(Brushes.Black, glyphRun); + } + } - var glyphRunDrawing = new GlyphRunDrawing + public class GlyphRunGeometryControl : Control + { + private GlyphTypeface _glyphTypeface = Typeface.Default.GlyphTypeface; + private readonly Random _rand = new Random(); + private ushort[] _glyphIndices = new ushort[1]; + private char[] _characters = new char[1]; + private float _fontSize = 20; + private int _direction = 10; + + private DispatcherTimer _timer; + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + _timer = new DispatcherTimer { - Foreground = Brushes.Black, - GlyphRun = new GlyphRun(_glyphTypeface, _fontSize, _characters, _glyphIndices) + Interval = TimeSpan.FromSeconds(1) }; - drawingGroup.Children.Add(glyphRunDrawing); - - var geometryDrawing = new GeometryDrawing + _timer.Tick += (s, e) => { - Pen = new Pen(Brushes.Black), - Geometry = new RectangleGeometry { Rect = new Rect(glyphRunDrawing.GlyphRun.Size) } + InvalidateVisual(); }; - drawingGroup.Children.Add(geometryDrawing); + _timer.Start(); + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + _timer.Stop(); + + _timer = null; + } + + public override void Render(DrawingContext context) + { + var c = (char)_rand.Next(65, 90); + + if (_fontSize + _direction > 200) + { + _direction = -10; + } + + if (_fontSize + _direction < 20) + { + _direction = 10; + } + + _fontSize += _direction; + + _glyphIndices[0] = _glyphTypeface.GetGlyph(c); + + _characters[0] = c; + + var glyphRun = new GlyphRun(_glyphTypeface, _fontSize, _characters, _glyphIndices); + + var geometry = glyphRun.BuildGeometry(); - (_imageControl.Source as DrawingImage).Drawing = drawingGroup; + context.DrawGeometry(Brushes.Green, null, geometry); } } } diff --git a/src/Avalonia.Base/Layout/LayoutManager.cs b/src/Avalonia.Base/Layout/LayoutManager.cs index fc988a8d6c..446f135c83 100644 --- a/src/Avalonia.Base/Layout/LayoutManager.cs +++ b/src/Avalonia.Base/Layout/LayoutManager.cs @@ -28,7 +28,7 @@ namespace Avalonia.Layout public LayoutManager(ILayoutRoot owner) { _owner = owner ?? throw new ArgumentNullException(nameof(owner)); - _executeLayoutPass = ExecuteLayoutPass; + _executeLayoutPass = ExecuteQueuedLayoutPass; } public virtual event EventHandler? LayoutUpdated; @@ -94,6 +94,16 @@ namespace Avalonia.Layout QueueLayoutPass(); } + private void ExecuteQueuedLayoutPass() + { + if (!_queued) + { + return; + } + + ExecuteLayoutPass(); + } + /// public virtual void ExecuteLayoutPass() { @@ -319,8 +329,8 @@ namespace Avalonia.Layout { if (!_queued && !_running) { - Dispatcher.UIThread.Post(_executeLayoutPass, DispatcherPriority.Layout); _queued = true; + Dispatcher.UIThread.Post(_executeLayoutPass, DispatcherPriority.Layout); } } diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index 22be8d8865..25c35a28e5 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -194,6 +194,25 @@ namespace Avalonia.Media } } + /// + /// Obtains geometry for the glyph run. + /// + /// The geometry returned contains the combined geometry of all glyphs in the glyph run. + public Geometry BuildGeometry() + { + var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService(); + + var geometryImpl = platformRenderInterface.BuildGlyphRunGeometry(this, out var scale); + + var geometry = new PlatformGeometry(geometryImpl); + + var transform = new MatrixTransform(Matrix.CreateTranslation(geometry.Bounds.Left, -geometry.Bounds.Top) * scale); + + geometry.Transform = transform; + + return geometry; + } + /// /// Retrieves the offset from the leading edge of the /// to the leading or trailing edge of a caret stop containing the specified character hit. diff --git a/src/Avalonia.Base/Media/PlatformGeometry.cs b/src/Avalonia.Base/Media/PlatformGeometry.cs new file mode 100644 index 0000000000..f25a14540f --- /dev/null +++ b/src/Avalonia.Base/Media/PlatformGeometry.cs @@ -0,0 +1,24 @@ +using Avalonia.Platform; + +namespace Avalonia.Media +{ + internal class PlatformGeometry : Geometry + { + private readonly IGeometryImpl _geometryImpl; + + public PlatformGeometry(IGeometryImpl geometryImpl) + { + _geometryImpl = geometryImpl; + } + + public override Geometry Clone() + { + return new PlatformGeometry(_geometryImpl); + } + + protected override IGeometryImpl? CreateDefiningGeometry() + { + return _geometryImpl; + } + } +} diff --git a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs index 0eeefddf0b..bfa9e70fce 100644 --- a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs @@ -58,6 +58,14 @@ namespace Avalonia.Platform /// A combined geometry. IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2); + /// + /// Created a geometry implementation for the glyph run. + /// + /// The glyph run to build a geometry from. + /// The scaling of the produces geometry. + /// The geometry returned contains the combined geometry of all glyphs in the glyph run. + IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale); + /// /// Creates a renderer. /// diff --git a/src/Avalonia.Controls/GridSplitter.cs b/src/Avalonia.Controls/GridSplitter.cs index 216e43e1f0..784d33ed58 100644 --- a/src/Avalonia.Controls/GridSplitter.cs +++ b/src/Avalonia.Controls/GridSplitter.cs @@ -221,7 +221,8 @@ namespace Avalonia.Controls ShowsPreview = showsPreview, ResizeDirection = resizeDirection, SplitterLength = Math.Min(Bounds.Width, Bounds.Height), - ResizeBehavior = GetEffectiveResizeBehavior(resizeDirection) + ResizeBehavior = GetEffectiveResizeBehavior(resizeDirection), + Scaling = (VisualRoot as ILayoutRoot)?.LayoutScaling ?? 1, }; // Store the rows and columns to resize on drag events. @@ -630,13 +631,17 @@ namespace Avalonia.Controls { double actualLength1 = GetActualLength(definition1); double actualLength2 = GetActualLength(definition2); + double pixelLength = 1 / _resizeData.Scaling; + double epsilon = pixelLength + LayoutHelper.LayoutEpsilon; // When splitting, Check to see if the total pixels spanned by the definitions - // is the same as before starting resize. If not cancel the drag. + // is the same as before starting resize. If not cancel the drag. We need to account for + // layout rounding here, so ignore differences of less than a device pixel to avoid problems + // that WPF has, such as https://stackoverflow.com/questions/28464843. if (_resizeData.SplitBehavior == SplitBehavior.Split && !MathUtilities.AreClose( actualLength1 + actualLength2, - _resizeData.OriginalDefinition1ActualLength + _resizeData.OriginalDefinition2ActualLength, LayoutHelper.LayoutEpsilon)) + _resizeData.OriginalDefinition1ActualLength + _resizeData.OriginalDefinition2ActualLength, epsilon)) { CancelResize(); @@ -798,6 +803,9 @@ namespace Avalonia.Controls // The minimum of Width/Height of Splitter. Used to ensure splitter // isn't hidden by resizing a row/column smaller than the splitter. public double SplitterLength; + + // The current layout scaling factor. + public double Scaling; } } diff --git a/src/Avalonia.Controls/Templates/DataTemplates.cs b/src/Avalonia.Controls/Templates/DataTemplates.cs index f203539536..d4eeda7908 100644 --- a/src/Avalonia.Controls/Templates/DataTemplates.cs +++ b/src/Avalonia.Controls/Templates/DataTemplates.cs @@ -1,3 +1,4 @@ +using System; using Avalonia.Collections; namespace Avalonia.Controls.Templates @@ -13,6 +14,22 @@ namespace Avalonia.Controls.Templates public DataTemplates() { ResetBehavior = ResetBehavior.Remove; + + Validate += ValidateDataTemplate; + } + + private static void ValidateDataTemplate(IDataTemplate template) + { + var valid = template switch + { + ITypedDataTemplate typed => typed.DataType is not null, + _ => true + }; + + if (!valid) + { + throw new InvalidOperationException("DataTemplate inside of DataTemplates must have a DataType set. Set DataType property or use ItemTemplate with single template instead."); + } } } } \ No newline at end of file diff --git a/src/Avalonia.Controls/Templates/ITypedDataTemplate.cs b/src/Avalonia.Controls/Templates/ITypedDataTemplate.cs new file mode 100644 index 0000000000..239dbd79f4 --- /dev/null +++ b/src/Avalonia.Controls/Templates/ITypedDataTemplate.cs @@ -0,0 +1,10 @@ +using System; +using Avalonia.Metadata; + +namespace Avalonia.Controls.Templates; + +public interface ITypedDataTemplate : IDataTemplate +{ + [DataType] + Type? DataType { get; } +} diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index addc248d58..6471b87bfd 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -114,6 +114,13 @@ namespace Avalonia.Headless return new HeadlessGlyphRunStub(); } + public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale) + { + scale = Matrix.Identity; + + return new HeadlessGeometryStub(new Rect(glyphRun.Size)); + } + class HeadlessGeometryStub : IGeometryImpl { public HeadlessGeometryStub(Rect bounds) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs index 1ca7be67a7..04a61e5f10 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -35,7 +35,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions Transformers.Insert(2, _designTransformer = new AvaloniaXamlIlDesignPropertiesTransformer()); Transformers.Insert(3, _bindingTransformer = new AvaloniaBindingExtensionTransformer()); - // Targeted InsertBefore( new AvaloniaXamlIlResolveClassesPropertiesTransformer(), @@ -57,6 +56,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions new AvaloniaXamlIlResolveByNameMarkupExtensionReplacer() ); + InsertAfter( + new XDataTypeTransformer()); + // After everything else InsertBefore( new AddNameScopeRegistration(), diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/XDataTypeTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/XDataTypeTransformer.cs new file mode 100644 index 0000000000..845dc5f831 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/XDataTypeTransformer.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; +using System.Linq; +using XamlX; +using XamlX.Ast; +using XamlX.Transform; +using XamlX.Transform.Transformers; +using XamlX.TypeSystem; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers +{ + internal class XDataTypeTransformer : IXamlAstTransformer + { + private const string DataTypePropertyName = "DataType"; + + /// + /// Converts x:DataType directives to regular DataType assignments if property with Avalonia.Metadata.DataTypeAttribute exists. + /// + /// + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) + { + if (node is XamlAstObjectNode on) + { + for (var c = 0; c < on.Children.Count; c++) + { + var ch = on.Children[c]; + if (ch is XamlAstXmlDirective { Namespace: XamlNamespaces.Xaml2006, Name: DataTypePropertyName } d) + { + if (on.Children.OfType() + .Any(p => ((XamlAstNamePropertyReference)p.Property)?.Name == DataTypePropertyName)) + { + // Break iteration if any DataType property was already set by user code. + break; + } + + var templateDataTypeAttribute = context.GetAvaloniaTypes().DataTypeAttribute; + + var clrType = (on.Type as XamlAstClrTypeReference)?.Type; + if (clrType is null) + { + break; + } + + // Technically it's possible to map "x:DataType" to a property with [DataType] attribute regardless of its name, + // but we go explicitly strict here and check the name as well. + var (declaringType, dataTypeProperty) = GetAllProperties(clrType) + .FirstOrDefault(t => t.property.Name == DataTypePropertyName && t.property.CustomAttributes + .Any(a => a.Type == templateDataTypeAttribute)); + + if (dataTypeProperty is not null) + { + on.Children[c] = new XamlAstXamlPropertyValueNode(d, + new XamlAstNamePropertyReference(d, + new XamlAstClrTypeReference(ch, declaringType, false), dataTypeProperty.Name, + on.Type), + d.Values); + } + } + } + } + + return node; + } + + private static IEnumerable<(IXamlType declaringType, IXamlProperty property)> GetAllProperties(IXamlType t) + { + foreach (var p in t.Properties) + yield return (t, p); + if(t.BaseType!=null) + foreach (var tuple in GetAllProperties(t.BaseType)) + yield return tuple; + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs index d2b24979cc..4da6b1b791 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs @@ -5,7 +5,7 @@ using Avalonia.Metadata; namespace Avalonia.Markup.Xaml.Templates { - public class DataTemplate : IRecyclingDataTemplate + public class DataTemplate : IRecyclingDataTemplate, ITypedDataTemplate { [DataType] public Type DataType { get; set; } diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs index 10061c3d48..04e8b0a9c0 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs @@ -9,7 +9,7 @@ using Avalonia.Metadata; namespace Avalonia.Markup.Xaml.Templates { - public class TreeDataTemplate : ITreeDataTemplate + public class TreeDataTemplate : ITreeDataTemplate, ITypedDataTemplate { [DataType] public Type DataType { get; set; } diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index d3c3585cd0..727677c82e 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -62,6 +62,41 @@ namespace Avalonia.Skia return new CombinedGeometryImpl(combineMode, g1, g2); } + public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale) + { + if (glyphRun.GlyphTypeface.PlatformImpl is not GlyphTypefaceImpl glyphTypeface) + { + throw new InvalidOperationException("PlatformImpl can't be null."); + } + + var fontRenderingEmSize = (float)glyphRun.FontRenderingEmSize; + var skFont = new SKFont(glyphTypeface.Typeface, fontRenderingEmSize) + { + Size = fontRenderingEmSize, + Edging = SKFontEdging.Antialias, + Hinting = SKFontHinting.None, + LinearMetrics = true + }; + + SKPath path = new SKPath(); + var matrix = SKMatrix.Identity; + + var currentX = 0f; + + foreach (var glyph in glyphRun.GlyphIndices) + { + var p = skFont.GetGlyphPath(glyph); + + path.AddPath(p, currentX, 0); + + currentX += p.Bounds.Right; + } + + scale = Matrix.CreateScale(matrix.ScaleX, matrix.ScaleY); + + return new StreamGeometryImpl(path); + } + /// public IBitmapImpl LoadBitmap(string fileName) { diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index d9e992bb80..04025f92e4 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -159,6 +159,34 @@ namespace Avalonia.Direct2D1 public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) => new GeometryGroupImpl(fillRule, children); public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => new CombinedGeometryImpl(combineMode, g1, g2); + public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale) + { + if (glyphRun.GlyphTypeface.PlatformImpl is not GlyphTypefaceImpl glyphTypeface) + { + throw new InvalidOperationException("PlatformImpl can't be null."); + } + + var pathGeometry = new SharpDX.Direct2D1.PathGeometry(Direct2D1Factory); + + using (var sink = pathGeometry.Open()) + { + var glyphs = new short[glyphRun.GlyphIndices.Count]; + + for (int i = 0; i < glyphRun.GlyphIndices.Count; i++) + { + glyphs[i] = (short)glyphRun.GlyphIndices[i]; + } + + glyphTypeface.FontFace.GetGlyphRunOutline((float)glyphRun.FontRenderingEmSize, glyphs, null, null, false, !glyphRun.IsLeftToRight, sink); + + sink.Close(); + } + + scale = Matrix.Identity; + + return new StreamGeometryImpl(pathGeometry); + } + /// public IBitmapImpl LoadBitmap(string fileName) { diff --git a/tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs b/tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs index f0e8e1cd11..37e07c244e 100644 --- a/tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs +++ b/tests/Avalonia.Base.UnitTests/Layout/LayoutManagerTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using Avalonia.Controls; using Avalonia.Layout; +using Avalonia.Threading; using Xunit; namespace Avalonia.Base.UnitTests.Layout @@ -421,5 +422,22 @@ namespace Avalonia.Base.UnitTests.Layout Assert.Equal(new Size(200, 200), control.Bounds.Size); Assert.Equal(new Size(200, 200), control.DesiredSize); } + + [Fact] + public void LayoutManager_Execute_Layout_Pass_Should_Clear_Queued_LayoutPasses() + { + var control = new LayoutTestControl(); + var root = new LayoutTestRoot { Child = control }; + + int layoutCount = 0; + root.LayoutUpdated += (_, _) => layoutCount++; + + root.LayoutManager.InvalidateArrange(control); + root.LayoutManager.ExecuteInitialLayoutPass(); + + Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout); + + Assert.Equal(1, layoutCount); + } } } diff --git a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs index add8f7fd73..183177495a 100644 --- a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs +++ b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs @@ -121,6 +121,11 @@ namespace Avalonia.Base.UnitTests.VisualTree throw new NotImplementedException(); } + public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale) + { + throw new NotImplementedException(); + } + class MockStreamGeometry : IStreamGeometryImpl { private MockStreamGeometryContext _impl = new MockStreamGeometryContext(); diff --git a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs index 5cbb3b2c49..51e75b6611 100644 --- a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs +++ b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs @@ -117,6 +117,11 @@ namespace Avalonia.Benchmarks return new NullGlyphRun(); } + public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale) + { + throw new NotImplementedException(); + } + public bool SupportsIndividualRoundRects => true; public AlphaFormat DefaultAlphaFormat => AlphaFormat.Premul; diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index 7e721fd7b2..f3f2d2f1e4 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -17,6 +17,7 @@ using Avalonia.Markup.Xaml.Templates; using Avalonia.Media; using Avalonia.Metadata; using Avalonia.UnitTests; +using JetBrains.Annotations; using XamlX; using Xunit; @@ -413,11 +414,11 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests' x:DataType='local:TestDataContext'> - + - + "; var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); @@ -1527,7 +1528,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions [TemplateContent] public object Content { get; set; } - public bool Match(object data) => FancyDataType.IsInstanceOfType(data); + public bool Match(object data) => FancyDataType?.IsInstanceOfType(data) ?? true; public IControl Build(object data) => TemplateContent.Load(Content)?.Control; } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs index 8188b212e1..affa292a7d 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs @@ -74,18 +74,18 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml - + - + - + - + "; diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs index 53881467e7..e005964ad0 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs @@ -1,5 +1,11 @@ +using System; +using System.Linq; using Avalonia.Controls; using Avalonia.Controls.Presenters; +using Avalonia.Controls.Templates; +using Avalonia.Markup.Xaml.Templates; +using Avalonia.Markup.Xaml.UnitTests.MarkupExtensions; +using Avalonia.Metadata; using Avalonia.UnitTests; using Xunit; @@ -89,6 +95,93 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml } } + [Fact] + public void XDataType_Should_Be_Assigned_To_Clr_Property() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var target = window.FindControl("target"); + var template = (DataTemplate)window.DataTemplates.First(); + + window.ApplyTemplate(); + target.ApplyTemplate(); + ((ContentPresenter)target.Presenter).UpdateChild(); + + Assert.Equal(typeof(string), template.DataType); + Assert.IsType(target.Presenter.Child); + } + } + + [Fact] + public void XDataType_Should_Be_Ignored_If_DataType_Already_Set() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var target = window.FindControl("target"); + + window.ApplyTemplate(); + target.ApplyTemplate(); + ((ContentPresenter)target.Presenter).UpdateChild(); + + Assert.IsType(target.Presenter.Child); + } + } + + [Fact] + public void XDataType_Should_Be_Ignored_If_DataType_Has_Non_Standard_Name() + { + // We don't want DataType to be mapped to FancyDataType, avoid possible confusion. + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + + +"; + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); + var target = window.FindControl("target"); + + window.ApplyTemplate(); + target.ApplyTemplate(); + ((ContentPresenter)target.Presenter).UpdateChild(); + + var dataTemplate = (CustomDataTemplate)target.ContentTemplate; + Assert.Null(dataTemplate.FancyDataType); + } + } + [Fact] public void Can_Set_DataContext_In_DataTemplate() { @@ -132,5 +225,25 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.Same(viewModel.Child.Child, canvas.DataContext); } } + + [Fact] + public void DataTemplates_Without_Type_Should_Throw() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + +"; + Assert.Throws(() => (Window)AvaloniaRuntimeXamlLoader.Load(xaml)); + } + } } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/TreeDataTemplateTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/TreeDataTemplateTests.cs index 3fdac49f31..81e94bde4f 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/TreeDataTemplateTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/TreeDataTemplateTests.cs @@ -14,12 +14,29 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml { using (UnitTestApplication.Start(TestServices.MockPlatformWrapper)) { - var xaml = ""; + var xaml = ""; var templates = (DataTemplates)AvaloniaRuntimeXamlLoader.Load(xaml); var template = (TreeDataTemplate)(templates.First()); Assert.IsType(template.ItemsSource); } } + + [Fact] + public void XDataType_Should_Be_Assigned_To_Clr_Property() + { + using (UnitTestApplication.Start(TestServices.MockPlatformWrapper)) + { + var xaml = @" + + +"; + var templates = (DataTemplates)AvaloniaRuntimeXamlLoader.Load(xaml); + var template = (TreeDataTemplate)(templates.First()); + + Assert.Equal(typeof(string), template.DataType); + } + } } } diff --git a/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs b/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs new file mode 100644 index 0000000000..6a8884a33a --- /dev/null +++ b/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs @@ -0,0 +1,81 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Controls.Documents; +using Avalonia.Controls.Shapes; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Xunit; + +#if AVALONIA_SKIA +namespace Avalonia.Skia.RenderTests +#else +namespace Avalonia.Direct2D1.RenderTests.Media +#endif +{ + public class GlyphRunTests : TestBase + { + public GlyphRunTests() + : base(@"Media\GlyphRun") + { + } + + [Fact] + public async Task Should_Render_GlyphRun_Geometry() + { + Decorator target = new Decorator + { + Padding = new Thickness(8), + Width = 200, + Height = 100, + Child = new GlyphRunGeometryControl + { + [TextElement.ForegroundProperty] = new LinearGradientBrush + { + StartPoint = new RelativePoint(0, 0.5, RelativeUnit.Relative), + EndPoint = new RelativePoint(1, 0.5, RelativeUnit.Relative), + GradientStops = + { + new GradientStop { Color = Colors.Red, Offset = 0 }, + new GradientStop { Color = Colors.Blue, Offset = 1 } + } + } + } + }; + + await RenderToFile(target); + + CompareImages(); + } + + public class GlyphRunGeometryControl : Control + { + private readonly Geometry _geometry; + + public GlyphRunGeometryControl() + { + var glyphTypeface = new Typeface(TestFontFamily).GlyphTypeface; + + var glyphIndices = new[] { glyphTypeface.GetGlyph('A'), glyphTypeface.GetGlyph('B'), glyphTypeface.GetGlyph('C') }; + + var characters = new[] { 'A', 'B', 'C' }; + + var glyphRun = new GlyphRun(glyphTypeface, 100, characters, glyphIndices); + + _geometry = glyphRun.BuildGeometry(); + } + + protected override Size MeasureOverride(Size availableSize) + { + return _geometry.Bounds.Size; + } + + public override void Render(DrawingContext context) + { + var foreground = TextElement.GetForeground(this); + + context.DrawGeometry(foreground, null, _geometry); + } + } + } +} diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs index 376121c269..c385e1c3eb 100644 --- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs +++ b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs @@ -122,6 +122,13 @@ namespace Avalonia.UnitTests return Mock.Of(); } + public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale) + { + scale = Matrix.Identity; + + return Mock.Of(); + } + public bool SupportsIndividualRoundRects { get; set; } public AlphaFormat DefaultAlphaFormat => AlphaFormat.Premul; diff --git a/tests/TestFiles/Direct2D1/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png b/tests/TestFiles/Direct2D1/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png new file mode 100644 index 0000000000..7f1e0d29a1 Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png differ diff --git a/tests/TestFiles/Skia/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png b/tests/TestFiles/Skia/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png new file mode 100644 index 0000000000..a8f3aa9277 Binary files /dev/null and b/tests/TestFiles/Skia/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png differ