Browse Source

Merge branch 'master' into master

pull/8328/head
Max Katz 4 years ago
committed by GitHub
parent
commit
60541e1f8e
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 27
      samples/ControlCatalog.Android/ControlCatalog.Android.csproj
  2. 3
      samples/ControlCatalog.Android/Properties/AndroidManifest.xml
  3. 1
      samples/ControlCatalog.Android/environment.device.txt
  4. 1
      samples/ControlCatalog.Android/environment.emulator.txt
  5. 4
      samples/RenderDemo/Pages/FormattedTextPage.axaml.cs
  6. 37
      src/Avalonia.Base/GeometryCollection.cs
  7. 18
      src/Avalonia.Base/Media/DrawingCollection.cs
  8. 420
      src/Avalonia.Base/Media/DrawingGroup.cs
  9. 129
      src/Avalonia.Base/Media/FormattedText.cs
  10. 45
      src/Avalonia.Base/Media/GeometryCollection.cs
  11. 12
      src/Avalonia.Base/Media/GeometryDrawing.cs
  12. 58
      src/Avalonia.Base/Media/GeometryGroup.cs
  13. 10
      src/Avalonia.Base/Media/GlyphRun.cs
  14. 3
      src/Avalonia.Base/Platform/IPlatformRenderInterface.cs
  15. 17
      src/Avalonia.Controls/Templates/DataTemplates.cs
  16. 10
      src/Avalonia.Controls/Templates/ITypedDataTemplate.cs
  17. 4
      src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
  18. 4
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
  19. 73
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/XDataTypeTransformer.cs
  20. 2
      src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs
  21. 2
      src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs
  22. 26
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  23. 22
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  24. 14
      tests/Avalonia.Base.UnitTests/Media/GeometryGroupTests.cs
  25. 2
      tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs
  26. 2
      tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
  27. 7
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs
  28. 8
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs
  29. 113
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs
  30. 19
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/TreeDataTemplateTests.cs
  31. 35
      tests/Avalonia.RenderTests/Media/GlyphRunTests.cs
  32. 4
      tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
  33. BIN
      tests/TestFiles/Direct2D1/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png
  34. BIN
      tests/TestFiles/Skia/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png

27
samples/ControlCatalog.Android/ControlCatalog.Android.csproj

@ -9,42 +9,37 @@
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion> <ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
<AndroidPackageFormat>apk</AndroidPackageFormat> <AndroidPackageFormat>apk</AndroidPackageFormat>
<MSBuildEnableWorkloadResolver>true</MSBuildEnableWorkloadResolver> <MSBuildEnableWorkloadResolver>true</MSBuildEnableWorkloadResolver>
<RuntimeIdentifiers>android-arm64;android-x64</RuntimeIdentifiers>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<None Remove="Assets\AboutAssets.txt" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<AndroidResource Include="..\..\build\Assets\Icon.png"> <AndroidResource Include="..\..\build\Assets\Icon.png">
<Link>Resources\drawable\Icon.png</Link> <Link>Resources\drawable\Icon.png</Link>
</AndroidResource> </AndroidResource>
</ItemGroup> </ItemGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release' and '$(TF_BUILD)' == ''"> <PropertyGroup Condition="'$(RunAOTCompilation)'=='' and '$(Configuration)'=='Release' and '$(TF_BUILD)'==''">
<DebugSymbols>False</DebugSymbols>
<UseInterpreter>False</UseInterpreter>
<RunAOTCompilation>True</RunAOTCompilation> <RunAOTCompilation>True</RunAOTCompilation>
</PropertyGroup>
<PropertyGroup Condition="'$(RunAOTCompilation)'=='True'">
<EnableLLVM>True</EnableLLVM> <EnableLLVM>True</EnableLLVM>
<AndroidAotAdditionalArguments>no-write-symbols,nodebug</AndroidAotAdditionalArguments> <AndroidAotAdditionalArguments>no-write-symbols,nodebug</AndroidAotAdditionalArguments>
<AndroidAotMode>Hybrid</AndroidAotMode> <AndroidAotMode>Hybrid</AndroidAotMode>
<AndroidGenerateJniMarshalMethods>True</AndroidGenerateJniMarshalMethods> <AndroidGenerateJniMarshalMethods>True</AndroidGenerateJniMarshalMethods>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(AndroidEnableProfiler)'=='True'">
<EmbedAssembliesIntoApk>False</EmbedAssembliesIntoApk> <IsEmulator Condition="'$(IsEmulator)' == ''">True</IsEmulator>
<RunAOTCompilation>False</RunAOTCompilation> <DebugSymbols>True</DebugSymbols>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<EmbedAssembliesIntoApk>True</EmbedAssembliesIntoApk>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Xamarin.AndroidX.AppCompat" Version="1.3.1.3" /> <AndroidEnvironment Condition="'$(IsEmulator)'=='True'" Include="environment.emulator.txt" />
<PackageReference Include="Xamarin.AndroidX.Lifecycle.ViewModel" Version="2.3.1.3" /> <AndroidEnvironment Condition="'$(IsEmulator)'!='True'" Include="environment.device.txt" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\src\Android\Avalonia.Android\Avalonia.Android.csproj" /> <ProjectReference Include="..\..\src\Android\Avalonia.Android\Avalonia.Android.csproj" />
<ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" /> <ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

3
samples/ControlCatalog.Android/Properties/AndroidManifest.xml

@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:installLocation="auto"> <manifest xmlns:android="http://schemas.android.com/apk/res/android" android:installLocation="auto">
<application android:label="ControlCatalog.Android" android:icon="@drawable/Icon"></application> <application android:label="ControlCatalog.Android" android:icon="@drawable/Icon"></application>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
</manifest> </manifest>

1
samples/ControlCatalog.Android/environment.device.txt

@ -0,0 +1 @@
DOTNET_DiagnosticPorts=127.0.0.1:9000,suspend

1
samples/ControlCatalog.Android/environment.emulator.txt

@ -0,0 +1 @@
DOTNET_DiagnosticPorts=10.0.2.2:9001,suspend

4
samples/RenderDemo/Pages/FormattedTextPage.axaml.cs

@ -55,6 +55,10 @@ namespace RenderDemo.Pages
formattedText.SetFontStyle(FontStyle.Italic, 28, 28); formattedText.SetFontStyle(FontStyle.Italic, 28, 28);
context.DrawText(formattedText, new Point(10, 0)); context.DrawText(formattedText, new Point(10, 0));
var geometry = formattedText.BuildGeometry(new Point(10 + formattedText.Width + 10, 0));
context.DrawGeometry(gradient, null, geometry);
} }
} }
} }

37
src/Avalonia.Base/GeometryCollection.cs

@ -1,37 +0,0 @@
using System.Collections;
using System.Collections.Generic;
using Avalonia.Animation;
#nullable enable
namespace Avalonia.Media
{
public class GeometryCollection : Animatable, IList<Geometry>, IReadOnlyList<Geometry>
{
private List<Geometry> _inner;
public GeometryCollection() => _inner = new List<Geometry>();
public GeometryCollection(IEnumerable<Geometry> collection) => _inner = new List<Geometry>(collection);
public GeometryCollection(int capacity) => _inner = new List<Geometry>(capacity);
public Geometry this[int index]
{
get => _inner[index];
set => _inner[index] = value;
}
public int Count => _inner.Count;
public bool IsReadOnly => false;
public void Add(Geometry item) => _inner.Add(item);
public void Clear() => _inner.Clear();
public bool Contains(Geometry item) => _inner.Contains(item);
public void CopyTo(Geometry[] array, int arrayIndex) => _inner.CopyTo(array, arrayIndex);
public IEnumerator<Geometry> GetEnumerator() => _inner.GetEnumerator();
public int IndexOf(Geometry item) => _inner.IndexOf(item);
public void Insert(int index, Geometry item) => _inner.Insert(index, item);
public bool Remove(Geometry item) => _inner.Remove(item);
public void RemoveAt(int index) => _inner.RemoveAt(index);
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}

18
src/Avalonia.Base/Media/DrawingCollection.cs

@ -0,0 +1,18 @@
using System.Collections.Generic;
using Avalonia.Collections;
namespace Avalonia.Media
{
public sealed class DrawingCollection : AvaloniaList<Drawing>
{
public DrawingCollection()
{
ResetBehavior = ResetBehavior.Remove;
}
public DrawingCollection(IEnumerable<Drawing> items) : base(items)
{
ResetBehavior = ResetBehavior.Remove;
}
}
}

420
src/Avalonia.Base/Media/DrawingGroup.cs

@ -1,6 +1,10 @@
using Avalonia.Collections; using System;
using System.Collections.Generic;
using Avalonia.Media.Imaging;
using Avalonia.Metadata; using Avalonia.Metadata;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Rendering.SceneGraph;
using Avalonia.Utilities;
namespace Avalonia.Media namespace Avalonia.Media
{ {
@ -18,6 +22,14 @@ namespace Avalonia.Media
public static readonly StyledProperty<IBrush> OpacityMaskProperty = public static readonly StyledProperty<IBrush> OpacityMaskProperty =
AvaloniaProperty.Register<DrawingGroup, IBrush>(nameof(OpacityMask)); AvaloniaProperty.Register<DrawingGroup, IBrush>(nameof(OpacityMask));
public static readonly DirectProperty<DrawingGroup, DrawingCollection> ChildrenProperty =
AvaloniaProperty.RegisterDirect<DrawingGroup, DrawingCollection>(
nameof(Children),
o => o.Children,
(o, v) => o.Children = v);
private DrawingCollection _children = new DrawingCollection();
public double Opacity public double Opacity
{ {
get => GetValue(OpacityProperty); get => GetValue(OpacityProperty);
@ -42,8 +54,23 @@ namespace Avalonia.Media
set => SetValue(OpacityMaskProperty, value); set => SetValue(OpacityMaskProperty, value);
} }
/// <summary>
/// Gets or sets the collection that contains the child geometries.
/// </summary>
[Content] [Content]
public AvaloniaList<Drawing> Children { get; } = new AvaloniaList<Drawing>(); public DrawingCollection Children
{
get => _children;
set
{
SetAndRaise(ChildrenProperty, ref _children, value);
}
}
public DrawingContext Open()
{
return new DrawingContext(new DrawingGroupDrawingContext(this));
}
public override void Draw(DrawingContext context) public override void Draw(DrawingContext context)
{ {
@ -75,5 +102,394 @@ namespace Avalonia.Media
return rect; return rect;
} }
private class DrawingGroupDrawingContext : IDrawingContextImpl
{
private readonly DrawingGroup _drawingGroup;
private readonly IPlatformRenderInterface _platformRenderInterface = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
private Matrix _transform;
private bool _disposed;
// Root drawing created by this DrawingContext.
//
// If there is only a single child of the root DrawingGroup, _rootDrawing
// will reference the single child, and the root _currentDrawingGroup
// value will be null. Otherwise, _rootDrawing will reference the
// root DrawingGroup, and be the same value as the root _currentDrawingGroup.
//
// Either way, _rootDrawing always references the root drawing.
protected Drawing? _rootDrawing;
// Current DrawingGroup that new children are added to
protected DrawingGroup? _currentDrawingGroup;
// Previous values of _currentDrawingGroup
private Stack<DrawingGroup?>? _previousDrawingGroupStack;
public DrawingGroupDrawingContext(DrawingGroup drawingGroup)
{
_drawingGroup = drawingGroup;
}
public Matrix Transform
{
get => _transform;
set
{
_transform = value;
PushTransform(new MatrixTransform(value));
}
}
public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect)
{
if ((brush == null) && (pen == null))
{
return;
}
// Instantiate the geometry
var geometry = _platformRenderInterface.CreateEllipseGeometry(rect);
// Add Drawing to the Drawing graph
AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry));
}
public void DrawGeometry(IBrush? brush, IPen? pen, IGeometryImpl geometry)
{
if (((brush == null) && (pen == null)) || (geometry == null))
{
return;
}
AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry));
}
public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun)
{
if (foreground == null || glyphRun == null)
{
return;
}
// Add a GlyphRunDrawing to the Drawing graph
GlyphRunDrawing glyphRunDrawing = new GlyphRunDrawing
{
Foreground = foreground,
GlyphRun = glyphRun,
};
// Add Drawing to the Drawing graph
AddDrawing(glyphRunDrawing);
}
public void DrawLine(IPen pen, Point p1, Point p2)
{
if (pen == null)
{
return;
}
// Instantiate the geometry
var geometry = _platformRenderInterface.CreateLineGeometry(p1, p2);
// Add Drawing to the Drawing graph
AddNewGeometryDrawing(null, pen, new PlatformGeometry(geometry));
}
public void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, BoxShadows boxShadows = default)
{
if ((brush == null) && (pen == null))
{
return;
}
// Instantiate the geometry
var geometry = _platformRenderInterface.CreateRectangleGeometry(rect.Rect);
// Add Drawing to the Drawing graph
AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry));
}
public void Clear(Color color)
{
throw new NotImplementedException();
}
public IDrawingContextLayerImpl CreateLayer(Size size)
{
throw new NotImplementedException();
}
public void Custom(ICustomDrawOperation custom)
{
throw new NotImplementedException();
}
public void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default)
{
throw new NotImplementedException();
}
public void DrawBitmap(IRef<IBitmapImpl> source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect)
{
throw new NotImplementedException();
}
public void PopBitmapBlendMode()
{
throw new NotImplementedException();
}
public void PopClip()
{
throw new NotImplementedException();
}
public void PopGeometryClip()
{
throw new NotImplementedException();
}
public void PopOpacity()
{
throw new NotImplementedException();
}
public void PopOpacityMask()
{
throw new NotImplementedException();
}
public void PushBitmapBlendMode(BitmapBlendingMode blendingMode)
{
throw new NotImplementedException();
}
public void PushClip(Rect clip)
{
throw new NotImplementedException();
}
public void PushClip(RoundedRect clip)
{
throw new NotImplementedException();
}
public void PushGeometryClip(IGeometryImpl clip)
{
throw new NotImplementedException();
}
public void PushOpacity(double opacity)
{
throw new NotImplementedException();
}
public void PushOpacityMask(IBrush mask, Rect bounds)
{
throw new NotImplementedException();
}
public void Dispose()
{
// Dispose may be called multiple times without throwing
// an exception.
if (!_disposed)
{
// Match any outstanding Push calls with a Pop
if (_previousDrawingGroupStack != null)
{
int stackCount = _previousDrawingGroupStack.Count;
for (int i = 0; i < stackCount; i++)
{
Pop();
}
}
// Call CloseCore with the root DrawingGroup's children
DrawingCollection rootChildren;
if (_currentDrawingGroup != null)
{
// If we created a root DrawingGroup because multiple elements
// exist at the root level, provide it's Children collection
// directly.
rootChildren = _currentDrawingGroup.Children;
}
else
{
// Create a new DrawingCollection if we didn't create a
// root DrawingGroup because the root level only contained
// a single child.
//
// This collection is needed by DrawingGroup.Open because
// Open always replaces it's Children collection. It isn't
// strictly needed for Append, but always using a collection
// simplifies the TransactionalAppend implementation (i.e.,
// a seperate implemention isn't needed for a single element)
rootChildren = new DrawingCollection();
//
// We may need to opt-out of inheritance through the new Freezable.
// This is controlled by this.CanBeInheritanceContext.
//
if (_rootDrawing != null)
{
rootChildren.Add(_rootDrawing);
}
}
// Inform our derived classes that Close was called
_drawingGroup.Children = rootChildren;
_disposed = true;
}
}
/// <summary>
/// Pop
/// </summary>
private void Pop()
{
// Verify that Pop hasn't been called too many times
if ((_previousDrawingGroupStack == null) || (_previousDrawingGroupStack.Count == 0))
{
throw new InvalidOperationException("DrawingGroupStack count missmatch.");
}
// Restore the previous value of the current drawing group
_currentDrawingGroup = _previousDrawingGroupStack.Pop();
}
/// <summary>
/// PushTransform -
/// Push a Transform which will apply to all drawing operations until the corresponding
/// Pop.
/// </summary>
/// <param name="transform"> The Transform to push. </param>
private void PushTransform(Transform transform)
{
// Instantiate a new drawing group and set it as the _currentDrawingGroup
var drawingGroup = PushNewDrawingGroup();
// Set the transform on the new DrawingGroup
drawingGroup.Transform = transform;
}
/// <summary>
/// Creates a new DrawingGroup for a Push* call by setting the
/// _currentDrawingGroup to a newly instantiated DrawingGroup,
/// and saving the previous _currentDrawingGroup value on the
/// _previousDrawingGroupStack.
/// </summary>
private DrawingGroup PushNewDrawingGroup()
{
// Instantiate a new drawing group
DrawingGroup drawingGroup = new DrawingGroup();
// Add it to the drawing graph, like any other Drawing
AddDrawing(drawingGroup);
// Lazily allocate the stack when it is needed because many uses
// of DrawingDrawingContext will have a depth of one.
if (null == _previousDrawingGroupStack)
{
_previousDrawingGroupStack = new Stack<DrawingGroup?>(2);
}
// Save the previous _currentDrawingGroup value.
//
// If this is the first call, the value of _currentDrawingGroup
// will be null because AddDrawing doesn't create a _currentDrawingGroup
// for the first drawing. Having null on the stack is valid, and simply
// denotes that this new DrawingGroup is the first child in the root
// DrawingGroup. It is also possible for the first value on the stack
// to be non-null, which means that the root DrawingGroup has other
// children.
_previousDrawingGroupStack.Push(_currentDrawingGroup);
// Set this drawing group as the current one so that subsequent drawing's
// are added as it's children until Pop is called.
_currentDrawingGroup = drawingGroup;
return drawingGroup;
}
/// <summary>
/// Contains the functionality common to GeometryDrawing operations of
/// instantiating the GeometryDrawing, setting it's Freezable state,
/// and Adding it to the Drawing Graph.
/// </summary>
private void AddNewGeometryDrawing(IBrush? brush, IPen? pen, Geometry? geometry)
{
if (geometry == null)
{
throw new ArgumentNullException(nameof(geometry));
}
// Instantiate the GeometryDrawing
GeometryDrawing geometryDrawing = new GeometryDrawing
{
// We may need to opt-out of inheritance through the new Freezable.
// This is controlled by this.CanBeInheritanceContext.
Brush = brush,
Pen = pen,
Geometry = geometry
};
// Add it to the drawing graph
AddDrawing(geometryDrawing);
}
/// <summary>
/// Adds a new Drawing to the DrawingGraph.
///
/// This method avoids creating a DrawingGroup for the common case
/// where only a single child exists in the root DrawingGroup.
/// </summary>
private void AddDrawing(Drawing newDrawing)
{
if (newDrawing == null)
{
throw new ArgumentNullException(nameof(newDrawing));
}
if (_rootDrawing == null)
{
// When a DrawingGroup is set, it should be made the root if
// a root drawing didnt exist.
Contract.Requires<NotSupportedException>(_currentDrawingGroup == null);
// If this is the first Drawing being added, avoid creating a DrawingGroup
// and set this drawing as the root drawing. This optimizes the common
// case where only a single child exists in the root DrawingGroup.
_rootDrawing = newDrawing;
}
else if (_currentDrawingGroup == null)
{
// When the second drawing is added at the root level, set a
// DrawingGroup as the root and add both drawings to it.
// Instantiate the DrawingGroup
_currentDrawingGroup = new DrawingGroup();
// Add both Children
_currentDrawingGroup.Children.Add(_rootDrawing);
_currentDrawingGroup.Children.Add(newDrawing);
// Set the new DrawingGroup as the current
_rootDrawing = _currentDrawingGroup;
}
else
{
// If there already is a current drawing group, then simply add
// the new drawing too it.
_currentDrawingGroup.Children.Add(newDrawing);
}
}
}
} }
} }

129
src/Avalonia.Base/Media/FormattedText.cs

@ -1223,7 +1223,7 @@ namespace Avalonia.Media
public double OverhangTrailing public double OverhangTrailing
{ {
get get
{ {
return BlackBoxMetrics.OverhangTrailing; return BlackBoxMetrics.OverhangTrailing;
} }
} }
@ -1252,6 +1252,46 @@ namespace Avalonia.Media
} }
} }
/// <summary>
/// Obtains geometry for the text, including underlines and strikethroughs.
/// </summary>
/// <param name="origin">The left top origin of the resulting geometry.</param>
/// <returns>The geometry returned contains the combined geometry
/// of all of the glyphs, underlines and strikeThroughs that represent the formatted text.
/// Overlapping contours are merged by performing a Boolean union operation.</returns>
public Geometry? BuildGeometry(Point origin)
{
GeometryGroup? accumulatedGeometry = null;
var lineOrigin = origin;
DrawingGroup drawing = new DrawingGroup();
using (var ctx = drawing.Open())
{
using (var enumerator = GetEnumerator())
{
while (enumerator.MoveNext())
{
var currentLine = enumerator.Current;
if (currentLine != null)
{
currentLine.Draw(ctx, lineOrigin);
AdvanceLineOrigin(ref lineOrigin, currentLine);
}
}
}
}
Transform? transform = new TranslateTransform(origin.X, origin.Y);
// recursively go down the DrawingGroup to build up the geometry
CombineGeometryRecursive(drawing, ref transform, ref accumulatedGeometry);
return accumulatedGeometry;
}
/// <summary> /// <summary>
/// Draws the text object /// Draws the text object
/// </summary> /// </summary>
@ -1284,6 +1324,93 @@ namespace Avalonia.Media
} }
} }
private void CombineGeometryRecursive(Drawing drawing, ref Transform? transform, ref GeometryGroup? accumulatedGeometry)
{
if (drawing is DrawingGroup group)
{
transform = group.Transform;
if (group.Children is DrawingCollection children)
{
// recursively go down for DrawingGroup
foreach (var child in children)
{
CombineGeometryRecursive(child, ref transform, ref accumulatedGeometry);
}
}
}
else
{
if (drawing is GlyphRunDrawing glyphRunDrawing)
{
// process glyph run
var glyphRun = glyphRunDrawing.GlyphRun;
if (glyphRun != null)
{
var glyphRunGeometry = glyphRun.BuildGeometry();
glyphRunGeometry.Transform = transform;
if (accumulatedGeometry == null)
{
accumulatedGeometry = new GeometryGroup
{
FillRule = FillRule.NonZero
};
}
accumulatedGeometry.Children.Add(glyphRunGeometry);
}
}
else
{
if (drawing is GeometryDrawing geometryDrawing)
{
// process geometry (i.e. TextDecoration on the line)
var geometry = geometryDrawing.Geometry;
if (geometry != null)
{
geometry.Transform = transform;
if (geometry is LineGeometry lineGeometry)
{
// For TextDecoration drawn by DrawLine(), the geometry is a LineGeometry which has no
// bounding area. So this line won't show up. Work aroud it by increase the Bounding rect
// to be Pen's thickness
var bounds = lineGeometry.Bounds;
if (bounds.Height == 0)
{
bounds = bounds.WithHeight(geometryDrawing.Pen?.Thickness ?? 0);
}
else if (bounds.Width == 0)
{
bounds = bounds.WithWidth(geometryDrawing.Pen?.Thickness ?? 0);
}
// convert the line geometry into a rectangle geometry
// we lost line cap info here
geometry = new RectangleGeometry(bounds);
}
if (accumulatedGeometry == null)
{
accumulatedGeometry = new GeometryGroup
{
FillRule = FillRule.NonZero
};
}
accumulatedGeometry.Children.Add(geometry);
}
}
}
}
}
private CachedMetrics DrawAndCalculateMetrics(DrawingContext? drawingContext, Point drawingOffset, bool getBlackBoxMetrics) private CachedMetrics DrawAndCalculateMetrics(DrawingContext? drawingContext, Point drawingOffset, bool getBlackBoxMetrics)
{ {
var metrics = new CachedMetrics(); var metrics = new CachedMetrics();

45
src/Avalonia.Base/Media/GeometryCollection.cs

@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using Avalonia.Collections;
#nullable enable
namespace Avalonia.Media
{
public sealed class GeometryCollection : AvaloniaList<Geometry>
{
public GeometryCollection()
{
ResetBehavior = ResetBehavior.Remove;
this.ForEachItem(
x =>
{
Parent?.Invalidate();
},
x =>
{
Parent?.Invalidate();
},
() => throw new NotSupportedException());
}
public GeometryCollection(IEnumerable<Geometry> items) : base(items)
{
ResetBehavior = ResetBehavior.Remove;
this.ForEachItem(
x =>
{
Parent?.Invalidate();
},
x =>
{
Parent?.Invalidate();
},
() => throw new NotSupportedException());
}
public GeometryGroup? Parent { get; set; }
}
}

12
src/Avalonia.Base/Media/GeometryDrawing.cs

@ -21,14 +21,14 @@ namespace Avalonia.Media
/// <summary> /// <summary>
/// Defines the <see cref="Brush"/> property. /// Defines the <see cref="Brush"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<IBrush> BrushProperty = public static readonly StyledProperty<IBrush?> BrushProperty =
AvaloniaProperty.Register<GeometryDrawing, IBrush>(nameof(Brush), Brushes.Transparent); AvaloniaProperty.Register<GeometryDrawing, IBrush?>(nameof(Brush), Brushes.Transparent);
/// <summary> /// <summary>
/// Defines the <see cref="Pen"/> property. /// Defines the <see cref="Pen"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<Pen> PenProperty = public static readonly StyledProperty<Pen?> PenProperty =
AvaloniaProperty.Register<GeometryDrawing, Pen>(nameof(Pen)); AvaloniaProperty.Register<GeometryDrawing, Pen?>(nameof(Pen));
/// <summary> /// <summary>
/// Gets or sets the <see cref="Avalonia.Media.Geometry"/> that describes the shape of this <see cref="GeometryDrawing"/>. /// Gets or sets the <see cref="Avalonia.Media.Geometry"/> that describes the shape of this <see cref="GeometryDrawing"/>.
@ -43,7 +43,7 @@ namespace Avalonia.Media
/// <summary> /// <summary>
/// Gets or sets the <see cref="Avalonia.Media.IBrush"/> used to fill the interior of the shape described by this <see cref="GeometryDrawing"/>. /// Gets or sets the <see cref="Avalonia.Media.IBrush"/> used to fill the interior of the shape described by this <see cref="GeometryDrawing"/>.
/// </summary> /// </summary>
public IBrush Brush public IBrush? Brush
{ {
get => GetValue(BrushProperty); get => GetValue(BrushProperty);
set => SetValue(BrushProperty, value); set => SetValue(BrushProperty, value);
@ -52,7 +52,7 @@ namespace Avalonia.Media
/// <summary> /// <summary>
/// Gets or sets the <see cref="Avalonia.Media.IPen"/> used to stroke this <see cref="GeometryDrawing"/>. /// Gets or sets the <see cref="Avalonia.Media.IPen"/> used to stroke this <see cref="GeometryDrawing"/>.
/// </summary> /// </summary>
public IPen Pen public IPen? Pen
{ {
get => GetValue(PenProperty); get => GetValue(PenProperty);
set => SetValue(PenProperty, value); set => SetValue(PenProperty, value);

58
src/Avalonia.Base/GeometryGroup.cs → src/Avalonia.Base/Media/GeometryGroup.cs

@ -1,7 +1,4 @@
using System; using Avalonia.Metadata;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Metadata;
using Avalonia.Platform; using Avalonia.Platform;
#nullable enable #nullable enable
@ -13,29 +10,36 @@ namespace Avalonia.Media
/// </summary> /// </summary>
public class GeometryGroup : Geometry public class GeometryGroup : Geometry
{ {
public static readonly DirectProperty<GeometryGroup, GeometryCollection?> ChildrenProperty = public static readonly DirectProperty<GeometryGroup, GeometryCollection> ChildrenProperty =
AvaloniaProperty.RegisterDirect<GeometryGroup, GeometryCollection?> ( AvaloniaProperty.RegisterDirect<GeometryGroup, GeometryCollection> (
nameof(Children), nameof(Children),
o => o.Children, o => o.Children,
(o, v) => o.Children = v); (o, v)=> o.Children = v);
public static readonly StyledProperty<FillRule> FillRuleProperty = public static readonly StyledProperty<FillRule> FillRuleProperty =
AvaloniaProperty.Register<GeometryGroup, FillRule>(nameof(FillRule)); AvaloniaProperty.Register<GeometryGroup, FillRule>(nameof(FillRule));
private GeometryCollection? _children; private GeometryCollection _children;
private bool _childrenSet;
public GeometryGroup()
{
_children = new GeometryCollection
{
Parent = this
};
}
/// <summary> /// <summary>
/// Gets or sets the collection that contains the child geometries. /// Gets or sets the collection that contains the child geometries.
/// </summary> /// </summary>
[Content] [Content]
public GeometryCollection? Children public GeometryCollection Children
{ {
get => _children ??= (!_childrenSet ? new GeometryCollection() : null); get => _children;
set set
{ {
SetAndRaise(ChildrenProperty, ref _children, value); OnChildrenChanged(_children, value);
_childrenSet = true; SetAndRaise(ChildrenProperty, ref _children, value);
} }
} }
@ -52,16 +56,28 @@ namespace Avalonia.Media
public override Geometry Clone() public override Geometry Clone()
{ {
var result = new GeometryGroup { FillRule = FillRule, Transform = Transform }; var result = new GeometryGroup { FillRule = FillRule, Transform = Transform };
if (_children?.Count > 0)
if (_children.Count > 0)
{
result.Children = new GeometryCollection(_children); result.Children = new GeometryCollection(_children);
}
return result; return result;
} }
protected void OnChildrenChanged(GeometryCollection oldChildren, GeometryCollection newChildren)
{
oldChildren.Parent = null;
newChildren.Parent = this;
}
protected override IGeometryImpl? CreateDefiningGeometry() protected override IGeometryImpl? CreateDefiningGeometry()
{ {
if (_children?.Count > 0) if (_children.Count > 0)
{ {
var factory = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>(); var factory = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
return factory.CreateGeometryGroup(FillRule, _children); return factory.CreateGeometryGroup(FillRule, _children);
} }
@ -72,10 +88,18 @@ namespace Avalonia.Media
{ {
base.OnPropertyChanged(change); base.OnPropertyChanged(change);
if (change.Property == ChildrenProperty || change.Property == FillRuleProperty) switch (change.Property.Name)
{ {
InvalidateGeometry(); case nameof(FillRule):
case nameof(Children):
InvalidateGeometry();
break;
} }
} }
internal void Invalidate()
{
InvalidateGeometry();
}
} }
} }

10
src/Avalonia.Base/Media/GlyphRun.cs

@ -202,15 +202,9 @@ namespace Avalonia.Media
{ {
var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>(); var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
var geometryImpl = platformRenderInterface.BuildGlyphRunGeometry(this, out var scale); var geometryImpl = platformRenderInterface.BuildGlyphRunGeometry(this);
var geometry = new PlatformGeometry(geometryImpl); return new PlatformGeometry(geometryImpl);
var transform = new MatrixTransform(Matrix.CreateTranslation(geometry.Bounds.Left, -geometry.Bounds.Top) * scale);
geometry.Transform = transform;
return geometry;
} }
/// <summary> /// <summary>

3
src/Avalonia.Base/Platform/IPlatformRenderInterface.cs

@ -62,9 +62,8 @@ namespace Avalonia.Platform
/// Created a geometry implementation for the glyph run. /// Created a geometry implementation for the glyph run.
/// </summary> /// </summary>
/// <param name="glyphRun">The glyph run to build a geometry from.</param> /// <param name="glyphRun">The glyph run to build a geometry from.</param>
/// <param name="scale">The scaling of the produces geometry.</param>
/// <returns>The geometry returned contains the combined geometry of all glyphs in the glyph run.</returns> /// <returns>The geometry returned contains the combined geometry of all glyphs in the glyph run.</returns>
IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale); IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun);
/// <summary> /// <summary>
/// Creates a renderer. /// Creates a renderer.

17
src/Avalonia.Controls/Templates/DataTemplates.cs

@ -1,3 +1,4 @@
using System;
using Avalonia.Collections; using Avalonia.Collections;
namespace Avalonia.Controls.Templates namespace Avalonia.Controls.Templates
@ -13,6 +14,22 @@ namespace Avalonia.Controls.Templates
public DataTemplates() public DataTemplates()
{ {
ResetBehavior = ResetBehavior.Remove; 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.");
}
} }
} }
} }

10
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; }
}

4
src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs

@ -114,10 +114,8 @@ namespace Avalonia.Headless
return new HeadlessGlyphRunStub(); return new HeadlessGlyphRunStub();
} }
public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale) public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun)
{ {
scale = Matrix.Identity;
return new HeadlessGeometryStub(new Rect(glyphRun.Size)); return new HeadlessGeometryStub(new Rect(glyphRun.Size));
} }

4
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(2, _designTransformer = new AvaloniaXamlIlDesignPropertiesTransformer());
Transformers.Insert(3, _bindingTransformer = new AvaloniaBindingExtensionTransformer()); Transformers.Insert(3, _bindingTransformer = new AvaloniaBindingExtensionTransformer());
// Targeted // Targeted
InsertBefore<PropertyReferenceResolver>( InsertBefore<PropertyReferenceResolver>(
new AvaloniaXamlIlResolveClassesPropertiesTransformer(), new AvaloniaXamlIlResolveClassesPropertiesTransformer(),
@ -57,6 +56,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
new AvaloniaXamlIlResolveByNameMarkupExtensionReplacer() new AvaloniaXamlIlResolveByNameMarkupExtensionReplacer()
); );
InsertAfter<TypeReferenceResolver>(
new XDataTypeTransformer());
// After everything else // After everything else
InsertBefore<NewObjectTransformer>( InsertBefore<NewObjectTransformer>(
new AddNameScopeRegistration(), new AddNameScopeRegistration(),

73
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";
/// <summary>
/// Converts x:DataType directives to regular DataType assignments if property with Avalonia.Metadata.DataTypeAttribute exists.
/// </summary>
/// <returns></returns>
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<XamlAstXamlPropertyValueNode>()
.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;
}
}
}

2
src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs

@ -5,7 +5,7 @@ using Avalonia.Metadata;
namespace Avalonia.Markup.Xaml.Templates namespace Avalonia.Markup.Xaml.Templates
{ {
public class DataTemplate : IRecyclingDataTemplate public class DataTemplate : IRecyclingDataTemplate, ITypedDataTemplate
{ {
[DataType] [DataType]
public Type DataType { get; set; } public Type DataType { get; set; }

2
src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs

@ -9,7 +9,7 @@ using Avalonia.Metadata;
namespace Avalonia.Markup.Xaml.Templates namespace Avalonia.Markup.Xaml.Templates
{ {
public class TreeDataTemplate : ITreeDataTemplate public class TreeDataTemplate : ITreeDataTemplate, ITypedDataTemplate
{ {
[DataType] [DataType]
public Type DataType { get; set; } public Type DataType { get; set; }

26
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@ -62,7 +62,7 @@ namespace Avalonia.Skia
return new CombinedGeometryImpl(combineMode, g1, g2); return new CombinedGeometryImpl(combineMode, g1, g2);
} }
public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale) public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun)
{ {
if (glyphRun.GlyphTypeface.PlatformImpl is not GlyphTypefaceImpl glyphTypeface) if (glyphRun.GlyphTypeface.PlatformImpl is not GlyphTypefaceImpl glyphTypeface)
{ {
@ -79,21 +79,29 @@ namespace Avalonia.Skia
}; };
SKPath path = new SKPath(); SKPath path = new SKPath();
var matrix = SKMatrix.Identity;
var currentX = 0f; var (currentX, currentY) = glyphRun.BaselineOrigin;
foreach (var glyph in glyphRun.GlyphIndices) for (var i = 0; i < glyphRun.GlyphIndices.Count; i++)
{ {
var p = skFont.GetGlyphPath(glyph); var glyph = glyphRun.GlyphIndices[i];
var glyphPath = skFont.GetGlyphPath(glyph);
path.AddPath(p, currentX, 0); if (!glyphPath.IsEmpty)
{
path.AddPath(glyphPath, (float)currentX, (float)currentY);
}
currentX += p.Bounds.Right; if (glyphRun.GlyphAdvances != null)
{
currentX += glyphRun.GlyphAdvances[i];
}
else
{
currentX += glyphPath.Bounds.Right;
}
} }
scale = Matrix.CreateScale(matrix.ScaleX, matrix.ScaleY);
return new StreamGeometryImpl(path); return new StreamGeometryImpl(path);
} }

22
src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs

@ -13,6 +13,7 @@ using Avalonia.Media.Imaging;
using SharpDX.DirectWrite; using SharpDX.DirectWrite;
using GlyphRun = Avalonia.Media.GlyphRun; using GlyphRun = Avalonia.Media.GlyphRun;
using TextAlignment = Avalonia.Media.TextAlignment; using TextAlignment = Avalonia.Media.TextAlignment;
using SharpDX.Mathematics.Interop;
namespace Avalonia namespace Avalonia
{ {
@ -159,7 +160,7 @@ namespace Avalonia.Direct2D1
public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList<Geometry> children) => new GeometryGroupImpl(fillRule, children); public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList<Geometry> children) => new GeometryGroupImpl(fillRule, children);
public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => new CombinedGeometryImpl(combineMode, g1, g2); public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => new CombinedGeometryImpl(combineMode, g1, g2);
public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale) public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun)
{ {
if (glyphRun.GlyphTypeface.PlatformImpl is not GlyphTypefaceImpl glyphTypeface) if (glyphRun.GlyphTypeface.PlatformImpl is not GlyphTypefaceImpl glyphTypeface)
{ {
@ -182,10 +183,23 @@ namespace Avalonia.Direct2D1
sink.Close(); sink.Close();
} }
scale = Matrix.Identity; var (baselineOriginX, baselineOriginY) = glyphRun.BaselineOrigin;
return new StreamGeometryImpl(pathGeometry); var transformedGeometry = new SharpDX.Direct2D1.TransformedGeometry(
} Direct2D1Factory,
pathGeometry,
new RawMatrix3x2(1.0f, 0.0f, 0.0f, 1.0f, (float)baselineOriginX, (float)baselineOriginY));
return new TransformedGeometryWrapper(transformedGeometry);
}
private class TransformedGeometryWrapper : GeometryImpl
{
public TransformedGeometryWrapper(SharpDX.Direct2D1.TransformedGeometry geometry) : base(geometry)
{
}
}
/// <inheritdoc /> /// <inheritdoc />
public IBitmapImpl LoadBitmap(string fileName) public IBitmapImpl LoadBitmap(string fileName)

14
tests/Avalonia.Base.UnitTests/Media/GeometryGroupTests.cs

@ -14,13 +14,21 @@ namespace Avalonia.Visuals.UnitTests.Media
} }
[Fact] [Fact]
public void Children_Can_Be_Set_To_Null() public void Children_Change_Should_Raise_Changed()
{ {
var target = new GeometryGroup(); var target = new GeometryGroup();
target.Children = null; var children = new GeometryCollection();
Assert.Null(target.Children); target.Children = children;
var isCalled = false;
target.Changed += (s, e) => isCalled = true;
children.Add(new StreamGeometry());
Assert.True(isCalled);
} }
} }
} }

2
tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs

@ -121,7 +121,7 @@ namespace Avalonia.Base.UnitTests.VisualTree
throw new NotImplementedException(); throw new NotImplementedException();
} }
public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale) public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }

2
tests/Avalonia.Benchmarks/NullRenderingPlatform.cs

@ -117,7 +117,7 @@ namespace Avalonia.Benchmarks
return new NullGlyphRun(); return new NullGlyphRun();
} }
public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale) public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
} }

7
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs

@ -17,6 +17,7 @@ using Avalonia.Markup.Xaml.Templates;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Metadata; using Avalonia.Metadata;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using JetBrains.Annotations;
using XamlX; using XamlX;
using Xunit; 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' xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
x:DataType='local:TestDataContext'> x:DataType='local:TestDataContext'>
<ItemsControl Items='{CompiledBinding ListProperty}' Name='target'> <ItemsControl Items='{CompiledBinding ListProperty}' Name='target'>
<ItemsControl.DataTemplates> <ItemsControl.ItemTemplate>
<DataTemplate> <DataTemplate>
<TextBlock Text='{CompiledBinding}' Name='textBlock' /> <TextBlock Text='{CompiledBinding}' Name='textBlock' />
</DataTemplate> </DataTemplate>
</ItemsControl.DataTemplates> </ItemsControl.ItemTemplate>
</ItemsControl> </ItemsControl>
</Window>"; </Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
@ -1527,7 +1528,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
[TemplateContent] [TemplateContent]
public object Content { get; set; } 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; public IControl Build(object data) => TemplateContent.Load(Content)?.Control;
} }

8
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs

@ -74,18 +74,18 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
<Window xmlns='https://github.com/avaloniaui'> <Window xmlns='https://github.com/avaloniaui'>
<DockPanel> <DockPanel>
<TabStrip Name='strip' DockPanel.Dock='Top' Items='{Binding Items}' SelectedIndex='0'> <TabStrip Name='strip' DockPanel.Dock='Top' Items='{Binding Items}' SelectedIndex='0'>
<TabStrip.DataTemplates> <TabStrip.ItemTemplate>
<DataTemplate> <DataTemplate>
<TextBlock Text='{Binding Header}'/> <TextBlock Text='{Binding Header}'/>
</DataTemplate> </DataTemplate>
</TabStrip.DataTemplates> </TabStrip.ItemTemplate>
</TabStrip> </TabStrip>
<Carousel Name='carousel' Items='{Binding Items}' SelectedIndex='{Binding #strip.SelectedIndex}'> <Carousel Name='carousel' Items='{Binding Items}' SelectedIndex='{Binding #strip.SelectedIndex}'>
<Carousel.DataTemplates> <Carousel.ItemTemplate>
<DataTemplate> <DataTemplate>
<TextBlock Text='{Binding Detail}'/> <TextBlock Text='{Binding Detail}'/>
</DataTemplate> </DataTemplate>
</Carousel.DataTemplates> </Carousel.ItemTemplate>
</Carousel> </Carousel>
</DockPanel> </DockPanel>
</Window>"; </Window>";

113
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/DataTemplateTests.cs

@ -1,5 +1,11 @@
using System;
using System.Linq;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Presenters; 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 Avalonia.UnitTests;
using Xunit; 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 = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:sys='clr-namespace:System;assembly=netstandard'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Window.DataTemplates>
<DataTemplate x:DataType='sys:String'>
<Canvas Name='foo'/>
</DataTemplate>
</Window.DataTemplates>
<ContentControl Name='target' Content='Foo'/>
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var target = window.FindControl<ContentControl>("target");
var template = (DataTemplate)window.DataTemplates.First();
window.ApplyTemplate();
target.ApplyTemplate();
((ContentPresenter)target.Presenter).UpdateChild();
Assert.Equal(typeof(string), template.DataType);
Assert.IsType<Canvas>(target.Presenter.Child);
}
}
[Fact]
public void XDataType_Should_Be_Ignored_If_DataType_Already_Set()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:sys='clr-namespace:System;assembly=netstandard'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Window.DataTemplates>
<DataTemplate DataType='sys:String' x:DataType='UserControl'>
<Canvas Name='foo'/>
</DataTemplate>
</Window.DataTemplates>
<ContentControl Name='target' Content='Foo'/>
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var target = window.FindControl<ContentControl>("target");
window.ApplyTemplate();
target.ApplyTemplate();
((ContentPresenter)target.Presenter).UpdateChild();
Assert.IsType<Canvas>(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 = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:sys='clr-namespace:System;assembly=netstandard'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'>
<ContentControl Name='target' Content='Foo'>
<ContentControl.ContentTemplate>
<local:CustomDataTemplate x:DataType='local:TestDataContext'>
<TextBlock Text='{CompiledBinding StringProperty}' Name='textBlock' />
</local:CustomDataTemplate>
</ContentControl.ContentTemplate>
</ContentControl>
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var target = window.FindControl<ContentControl>("target");
window.ApplyTemplate();
target.ApplyTemplate();
((ContentPresenter)target.Presenter).UpdateChild();
var dataTemplate = (CustomDataTemplate)target.ContentTemplate;
Assert.Null(dataTemplate.FancyDataType);
}
}
[Fact] [Fact]
public void Can_Set_DataContext_In_DataTemplate() public void Can_Set_DataContext_In_DataTemplate()
{ {
@ -132,5 +225,25 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
Assert.Same(viewModel.Child.Child, canvas.DataContext); Assert.Same(viewModel.Child.Child, canvas.DataContext);
} }
} }
[Fact]
public void DataTemplates_Without_Type_Should_Throw()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:sys='clr-namespace:System;assembly=netstandard'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Window.DataTemplates>
<DataTemplate>
<Canvas Name='foo'/>
</DataTemplate>
</Window.DataTemplates>
<ContentControl Name='target' Content='Foo'/>
</Window>";
Assert.Throws<InvalidOperationException>(() => (Window)AvaloniaRuntimeXamlLoader.Load(xaml));
}
}
} }
} }

19
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/TreeDataTemplateTests.cs

@ -14,12 +14,29 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
{ {
using (UnitTestApplication.Start(TestServices.MockPlatformWrapper)) using (UnitTestApplication.Start(TestServices.MockPlatformWrapper))
{ {
var xaml = "<DataTemplates xmlns='https://github.com/avaloniaui'><TreeDataTemplate ItemsSource='{Binding}'/></DataTemplates>"; var xaml = "<DataTemplates xmlns='https://github.com/avaloniaui'><TreeDataTemplate DataType='Control' ItemsSource='{Binding}'/></DataTemplates>";
var templates = (DataTemplates)AvaloniaRuntimeXamlLoader.Load(xaml); var templates = (DataTemplates)AvaloniaRuntimeXamlLoader.Load(xaml);
var template = (TreeDataTemplate)(templates.First()); var template = (TreeDataTemplate)(templates.First());
Assert.IsType<Binding>(template.ItemsSource); Assert.IsType<Binding>(template.ItemsSource);
} }
} }
[Fact]
public void XDataType_Should_Be_Assigned_To_Clr_Property()
{
using (UnitTestApplication.Start(TestServices.MockPlatformWrapper))
{
var xaml = @"
<DataTemplates xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<TreeDataTemplate x:DataType='x:String' />
</DataTemplates>";
var templates = (DataTemplates)AvaloniaRuntimeXamlLoader.Load(xaml);
var template = (TreeDataTemplate)(templates.First());
Assert.Equal(typeof(string), template.DataType);
}
}
} }
} }

35
tests/Avalonia.RenderTests/Media/GlyphRunTests.cs

@ -23,26 +23,28 @@ namespace Avalonia.Direct2D1.RenderTests.Media
[Fact] [Fact]
public async Task Should_Render_GlyphRun_Geometry() public async Task Should_Render_GlyphRun_Geometry()
{ {
Decorator target = new Decorator var control = new GlyphRunGeometryControl
{ {
Padding = new Thickness(8), [TextElement.ForegroundProperty] = new LinearGradientBrush
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),
StartPoint = new RelativePoint(0, 0.5, RelativeUnit.Relative), GradientStops =
EndPoint = new RelativePoint(1, 0.5, RelativeUnit.Relative),
GradientStops =
{ {
new GradientStop { Color = Colors.Red, Offset = 0 }, new GradientStop { Color = Colors.Red, Offset = 0 },
new GradientStop { Color = Colors.Blue, Offset = 1 } new GradientStop { Color = Colors.Blue, Offset = 1 }
} }
}
} }
}; };
Decorator target = new Decorator
{
Padding = new Thickness(8),
Width = 190,
Height = 120,
Child = control
};
await RenderToFile(target); await RenderToFile(target);
CompareImages(); CompareImages();
@ -50,8 +52,6 @@ namespace Avalonia.Direct2D1.RenderTests.Media
public class GlyphRunGeometryControl : Control public class GlyphRunGeometryControl : Control
{ {
private readonly Geometry _geometry;
public GlyphRunGeometryControl() public GlyphRunGeometryControl()
{ {
var glyphTypeface = new Typeface(TestFontFamily).GlyphTypeface; var glyphTypeface = new Typeface(TestFontFamily).GlyphTypeface;
@ -62,19 +62,16 @@ namespace Avalonia.Direct2D1.RenderTests.Media
var glyphRun = new GlyphRun(glyphTypeface, 100, characters, glyphIndices); var glyphRun = new GlyphRun(glyphTypeface, 100, characters, glyphIndices);
_geometry = glyphRun.BuildGeometry(); Geometry = glyphRun.BuildGeometry();
} }
protected override Size MeasureOverride(Size availableSize) public Geometry Geometry { get; }
{
return _geometry.Bounds.Size;
}
public override void Render(DrawingContext context) public override void Render(DrawingContext context)
{ {
var foreground = TextElement.GetForeground(this); var foreground = TextElement.GetForeground(this);
context.DrawGeometry(foreground, null, _geometry); context.DrawGeometry(foreground, null, Geometry);
} }
} }
} }

4
tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs

@ -122,10 +122,8 @@ namespace Avalonia.UnitTests
return Mock.Of<IGlyphRunImpl>(); return Mock.Of<IGlyphRunImpl>();
} }
public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun, out Matrix scale) public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun)
{ {
scale = Matrix.Identity;
return Mock.Of<IGeometryImpl>(); return Mock.Of<IGeometryImpl>();
} }

BIN
tests/TestFiles/Direct2D1/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
tests/TestFiles/Skia/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Loading…
Cancel
Save