Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AvaloniaList compile time parsing #11073

Merged
merged 6 commits into from
Apr 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions src/Avalonia.Base/Media/PolyLineSegment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@ public sealed class PolyLineSegment : PathSegment
/// <summary>
/// Defines the <see cref="Points"/> property.
/// </summary>
public static readonly StyledProperty<Points> PointsProperty
= AvaloniaProperty.Register<PolyLineSegment, Points>(nameof(Points));
public static readonly StyledProperty<IList<Point>> PointsProperty
= AvaloniaProperty.Register<PolyLineSegment, IList<Point>>(nameof(Points));

/// <summary>
/// Gets or sets the points.
/// </summary>
/// <value>
/// The points.
/// </value>
public Points Points
public IList<Point> Points
{
get => GetValue(PointsProperty);
set => SetValue(PointsProperty, value);
Expand All @@ -37,9 +37,9 @@ public PolyLineSegment()
/// Initializes a new instance of the <see cref="PolyLineSegment"/> class.
/// </summary>
/// <param name="points">The points.</param>
public PolyLineSegment(IEnumerable<Point> points) : this()
public PolyLineSegment(IEnumerable<Point> points)
{
Points.AddRange(points);
Points = new Points(points);
}

protected internal override void ApplyTo(StreamGeometryContext ctx)
Expand Down
18 changes: 9 additions & 9 deletions src/Avalonia.Base/Media/PolylineGeometry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,22 @@ public class PolylineGeometry : Geometry
/// <summary>
/// Defines the <see cref="Points"/> property.
/// </summary>
public static readonly DirectProperty<PolylineGeometry, Points> PointsProperty =
AvaloniaProperty.RegisterDirect<PolylineGeometry, Points>(nameof(Points), g => g.Points, (g, f) => g.Points = f);
public static readonly DirectProperty<PolylineGeometry, IList<Point>> PointsProperty =
AvaloniaProperty.RegisterDirect<PolylineGeometry, IList<Point>>(nameof(Points), g => g.Points, (g, f) => g.Points = f);

/// <summary>
/// Defines the <see cref="IsFilled"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsFilledProperty =
AvaloniaProperty.Register<PolylineGeometry, bool>(nameof(IsFilled));

private Points _points;
private IList<Point> _points;
private IDisposable? _pointsObserver;

static PolylineGeometry()
{
AffectsGeometry(IsFilledProperty);
PointsProperty.Changed.AddClassHandler<PolylineGeometry>((s, e) => s.OnPointsChanged(e.NewValue as Points));
PointsProperty.Changed.AddClassHandler<PolylineGeometry>((s, e) => s.OnPointsChanged(e.NewValue as IList<Point>));
}

/// <summary>
Expand All @@ -43,9 +43,9 @@ public PolylineGeometry()
/// <summary>
/// Initializes a new instance of the <see cref="PolylineGeometry"/> class.
/// </summary>
public PolylineGeometry(IEnumerable<Point> points, bool isFilled) : this()
public PolylineGeometry(IEnumerable<Point> points, bool isFilled)
{
Points.AddRange(points);
_points = new Points(points);
IsFilled = isFilled;
}

Expand All @@ -56,7 +56,7 @@ public PolylineGeometry(IEnumerable<Point> points, bool isFilled) : this()
/// The points.
/// </value>
[Content]
public Points Points
public IList<Point> Points
{
get => _points;
set => SetAndRaise(PointsProperty, ref _points, value);
Expand Down Expand Up @@ -97,10 +97,10 @@ public override Geometry Clone()
return geometry;
}

private void OnPointsChanged(Points? newValue)
private void OnPointsChanged(IList<Point>? newValue)
{
_pointsObserver?.Dispose();
_pointsObserver = newValue?.ForEachItem(
_pointsObserver = (newValue as IAvaloniaList<Point>)?.ForEachItem(
_ => InvalidateGeometry(),
_ => InvalidateGeometry(),
InvalidateGeometry);
Expand Down
14 changes: 13 additions & 1 deletion src/Avalonia.Base/Points.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
using System.Collections.Generic;
using Avalonia.Collections;

namespace Avalonia
{
public sealed class Points : AvaloniaList<Point> { }
public sealed class Points : AvaloniaList<Point>
{
public Points()
{

}

public Points(IEnumerable<Point> points) : base(points)
{

}
}
}
9 changes: 7 additions & 2 deletions src/Avalonia.Controls/Shapes/Polygon.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,15 @@ static Polygon()
AffectsGeometry<Polygon>(PointsProperty);
}

public Polygon()
{
Points = new Points();
}

public IList<Point> Points
{
get { return GetValue(PointsProperty); }
set { SetValue(PointsProperty, value); }
get => GetValue(PointsProperty);
set => SetValue(PointsProperty, value);
}

protected override Geometry CreateDefiningGeometry()
Expand Down
9 changes: 7 additions & 2 deletions src/Avalonia.Controls/Shapes/Polyline.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,15 @@ static Polyline()
AffectsGeometry<Polyline>(PointsProperty);
}

public Polyline()
{
Points = new Points();
}
Comment on lines +17 to +20
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not related to this PR, but changes nullability of points property to be consistent with PolyLineSegment/Geometry.


public IList<Point> Points
{
get { return GetValue(PointsProperty); }
set { SetValue(PointsProperty, value); }
get => GetValue(PointsProperty);
set => SetValue(PointsProperty, value);
}

protected override Geometry CreateDefiningGeometry()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using System.Collections.Generic;
using System.Reflection.Emit;
using Avalonia.Controls;
using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers;
using XamlX;
using XamlX.Ast;
using XamlX.Emit;
using XamlX.IL;
using XamlX.Transform;
using XamlX.TypeSystem;

namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.AstNodes
{
class AvaloniaXamlIlArrayConstantAstNode : XamlAstNode, IXamlAstValueNode, IXamlAstILEmitableNode
{
private readonly IXamlType _elementType;
private readonly IReadOnlyList<IXamlAstValueNode> _values;

public AvaloniaXamlIlArrayConstantAstNode(IXamlLineInfo lineInfo, IXamlType arrayType, IXamlType elementType, IReadOnlyList<IXamlAstValueNode> values) : base(lineInfo)
{
_elementType = elementType;
_values = values;

Type = new XamlAstClrTypeReference(lineInfo, arrayType, false);

foreach (var element in values)
{
if (!elementType.IsAssignableFrom(element.Type.GetClrType()))
{
throw new XamlParseException("x:Array element is not assignable to the array element type!", lineInfo);
}
}
}

public IXamlAstTypeReference Type { get; }

public XamlILNodeEmitResult Emit(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context, IXamlILEmitter codeGen)
{
codeGen.Ldc_I4(_values.Count)
.Newarr(_elementType);

for (var index = 0; index < _values.Count; index++)
{
var value = _values[index];

codeGen
.Dup()
.Ldc_I4(index);

context.Emit(value, codeGen, _elementType);

if (value.Type.GetClrType() is { IsValueType: true } valTypeInObjArr)
{
if (!_elementType.IsValueType)
{
codeGen.Box(valTypeInObjArr);
}
// It seems like ASM codegen for "stelem valuetype" and "stelem.i4" is identical,
// so we don't need to try to optimize it here.
codeGen.Emit(OpCodes.Stelem, valTypeInObjArr);
}
else
{
codeGen.Stelem_ref();
}
}

return XamlILNodeEmitResult.Type(0, Type.GetClrType());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,29 @@ public static bool TryConvert(AstTransformationContext context, IXamlAstValueNod
throw new XamlX.XamlLoadException($"Unable to parse \"{text}\" as a grid length", node);
}
}

if (type.Equals(types.ColumnDefinition) || type.Equals(types.RowDefinition))
{
try
{
var gridLength = GridLength.Parse(text);

result = new AvaloniaXamlIlGridLengthAstNode(node, types, gridLength);

var definitionConstructorGridLength = type.GetConstructor(new List<IXamlType> {types.GridLength});
var lengthNode = new AvaloniaXamlIlGridLengthAstNode(node, types, gridLength);
var definitionTypeRef = new XamlAstClrTypeReference(node, type, false);

result = new XamlAstNewClrObjectNode(node, definitionTypeRef,
definitionConstructorGridLength, new List<IXamlAstValueNode> {lengthNode});

return true;
}
catch
{
throw new XamlX.XamlLoadException($"Unable to parse \"{text}\" as a grid length", node);
}
}

if (type.Equals(types.Cursor))
{
Expand All @@ -211,16 +234,6 @@ public static bool TryConvert(AstTransformationContext context, IXamlAstValueNod
}
}

if (type.Equals(types.ColumnDefinitions))
{
return ConvertDefinitionList(node, text, types, types.ColumnDefinitions, types.ColumnDefinition, "column definitions", out result);
}

if (type.Equals(types.RowDefinitions))
{
return ConvertDefinitionList(node, text, types, types.RowDefinitions, types.RowDefinition, "row definitions", out result);
}

if (types.IBrush.IsAssignableFrom(type))
{
if (Color.TryParse(text, out Color color))
Expand Down Expand Up @@ -295,46 +308,89 @@ public static bool TryConvert(AstTransformationContext context, IXamlAstValueNod
}
}

result = null;
return false;
}

private static bool ConvertDefinitionList(
IXamlAstValueNode node,
string text,
AvaloniaXamlIlWellKnownTypes types,
IXamlType listType,
IXamlType elementType,
string errorDisplayName,
out IXamlAstValueNode result)
{
try
// Keep it in the end, so more specific parsers can be applied.
var elementType = GetElementType(type, context.Configuration.WellKnownTypes);
if (elementType is not null)
{
var lengths = GridLength.ParseLengths(text);

var definitionTypeRef = new XamlAstClrTypeReference(node, elementType, false);
string[] items;
// Normalize special case of Points collection.
if (elementType == types.Point)
{
var pointParts = text.Split(new[] { ",", " " }, StringSplitOptions.RemoveEmptyEntries);
if (pointParts.Length % 2 == 0)
{
items = new string[pointParts.Length / 2];
for (int i = 0; i < pointParts.Length; i += 2)
{
items[i / 2] = string.Format(CultureInfo.InvariantCulture, "{0} {1}", pointParts[i],
pointParts[i + 1]);
}
}
else
{
throw new XamlX.XamlLoadException($"Invalid PointsList.", node);
}
}
else
{
const StringSplitOptions trimOption = (StringSplitOptions)2; // StringSplitOptions.TrimEntries
var separators = new[] { "," };
var splitOptions = StringSplitOptions.RemoveEmptyEntries | trimOption;

var definitionConstructorGridLength = elementType.GetConstructor(new List<IXamlType> {types.GridLength});
items = text.Split(separators, splitOptions ^ trimOption);
// Compiler targets netstandard, so we need to emulate StringSplitOptions.TrimEntries, if it was requested.
if (splitOptions.HasFlag(trimOption))
{
items = items.Select(i => i.Trim()).ToArray();
}
}

IXamlAstValueNode CreateDefinitionNode(GridLength length)
var nodes = new IXamlAstValueNode[items.Length];
for (var index = 0; index < items.Length; index++)
{
var lengthNode = new AvaloniaXamlIlGridLengthAstNode(node, types, length);
var success = XamlTransformHelpers.TryGetCorrectlyTypedValue(
context,
new XamlAstTextNode(node, items[index], true, context.Configuration.WellKnownTypes.String),
elementType, out var itemNode);
if (!success)
{
result = null;
return false;
}

return new XamlAstNewClrObjectNode(node, definitionTypeRef,
definitionConstructorGridLength, new List<IXamlAstValueNode> {lengthNode});
nodes[index] = itemNode;
}

var definitionNodes =
new List<IXamlAstValueNode>(lengths.Select(CreateDefinitionNode));

result = new AvaloniaXamlIlAvaloniaListConstantAstNode(node, types, listType, elementType, definitionNodes);
if (types.AvaloniaList.MakeGenericType(elementType).IsAssignableFrom(type))
{
result = new AvaloniaXamlIlAvaloniaListConstantAstNode(node, types, type, elementType, nodes);
return true;
}
else if (type.IsArray)
{
result = new AvaloniaXamlIlArrayConstantAstNode(node, elementType.MakeArrayType(1), elementType, nodes);
return true;
}
else if (type == context.Configuration.WellKnownTypes.IListOfT.MakeGenericType(elementType))
{
var listType = context.Configuration.WellKnownTypes.IListOfT.MakeGenericType(elementType);
result = new AvaloniaXamlIlArrayConstantAstNode(node, listType, elementType, nodes);
return true;
}

return true;
}
catch
{
throw new XamlX.XamlLoadException($"Unable to parse \"{text}\" as a {errorDisplayName}", node);
result = null;
return false;
}

result = null;
return false;
}

private static IXamlType GetElementType(IXamlType type, XamlTypeWellKnownTypes types)
{
return type.GetAllInterfaces().FirstOrDefault(i =>
i.FullName.StartsWith(types.IEnumerableT.FullName))?
.GenericArguments[0];
}
}
}
Loading