XamlUI improvements.

1. Add XAML namespaces https://spacestation14.io with Avalonia hacks.
2. Make markup extensions work with Avalonia hacks.
3. Add LocExtension for localized strings.
4. Add Vector2 parsing type converter to XAML compilation.
5. Make SS14Window better thanks to these improvements.
This commit is contained in:
Pieter-Jan Briers
2021-02-14 02:09:37 +01:00
parent 29a39f8e0a
commit d9b2c73440
17 changed files with 385 additions and 46 deletions

View File

@@ -0,0 +1,37 @@
using System.Linq;
using Pidgin;
using static Pidgin.Parser;
namespace Robust.Build.Tasks
{
public static class MathParsing
{
public static Parser<char, float> Single { get; } = Real.Select(c => (float) c);
public static Parser<char, float> Single1 { get; }
= Single.Between(SkipWhitespaces);
public static Parser<char, (float, float)> Single2 { get; }
= Single.Before(SkipWhitespaces).Repeat(2).Select(e =>
{
var arr = e.ToArray();
return (arr[0], arr[1]);
});
public static Parser<char, (float, float, float, float)> Single4 { get; }
= Single.Before(SkipWhitespaces).Repeat(4).Select(e =>
{
var arr = e.ToArray();
return (arr[0], arr[1], arr[2], arr[3]);
});
public static Parser<char, float[]> Thickness { get; }
= SkipWhitespaces.Then(
OneOf(
Try(Single1.Select(c => new[] {c})),
Try(Single2.Select(c => new[] {c.Item1, c.Item2})),
Try(Single4.Select(c => new[] {c.Item1, c.Item2, c.Item3, c.Item4}))
));
}
}

View File

@@ -0,0 +1,93 @@
using System;
using System.Linq;
using System.Reflection.Emit;
using XamlX.Ast;
using XamlX.Emit;
using XamlX.IL;
using XamlX.TypeSystem;
namespace Robust.Build.Tasks
{
public abstract class RXamlVecLikeConstAstNode<T>
: XamlAstNode, IXamlAstValueNode, IXamlAstILEmitableNode
where T : unmanaged
{
private readonly IXamlConstructor _constructor;
protected readonly T[] Values;
public RXamlVecLikeConstAstNode(
IXamlLineInfo lineInfo,
IXamlType type, IXamlConstructor constructor,
IXamlType componentType, T[] values)
: base(lineInfo)
{
_constructor = constructor;
Values = values;
var @params = constructor.Parameters;
if (@params.Count != values.Length)
throw new ArgumentException("Invalid amount of parameters");
if (@params.Any(c => c != componentType))
throw new ArgumentException("Invalid constructor: not all parameters match component type");
Type = new XamlAstClrTypeReference(lineInfo, type, false);
}
public IXamlAstTypeReference Type { get; }
public virtual XamlILNodeEmitResult Emit(
XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context,
IXamlILEmitter codeGen)
{
codeGen.Newobj(_constructor);
return XamlILNodeEmitResult.Type(0, Type.GetClrType());
}
}
public sealed class RXamlSingleVecLikeConstAstNode : RXamlVecLikeConstAstNode<float>
{
public RXamlSingleVecLikeConstAstNode(
IXamlLineInfo lineInfo,
IXamlType type, IXamlConstructor constructor,
IXamlType componentType, float[] values)
: base(lineInfo, type, constructor, componentType, values)
{
}
public override XamlILNodeEmitResult Emit(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context,
IXamlILEmitter codeGen)
{
foreach (var value in Values)
{
codeGen.Emit(OpCodes.Ldc_R4, value);
}
return base.Emit(context, codeGen);
}
}
public sealed class RXamlInt32VecLikeConstAstNode : RXamlVecLikeConstAstNode<int>
{
public RXamlInt32VecLikeConstAstNode(
IXamlLineInfo lineInfo,
IXamlType type, IXamlConstructor constructor,
IXamlType componentType, int[] values)
: base(lineInfo, type, constructor, componentType, values)
{
}
public override XamlILNodeEmitResult Emit(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context,
IXamlILEmitter codeGen)
{
foreach (var value in Values)
{
codeGen.Emit(OpCodes.Ldc_I4, value);
}
return base.Emit(context, codeGen);
}
}
}

View File

@@ -0,0 +1,47 @@
using System.Linq;
using XamlX.Transform;
using XamlX.TypeSystem;
namespace Robust.Build.Tasks
{
class RXamlWellKnownTypes
{
public XamlTypeWellKnownTypes XamlIlTypes { get; }
public IXamlType Single { get; }
public IXamlType Int32 { get; }
public IXamlType Vector2 { get; }
public IXamlConstructor Vector2ConstructorFull { get; }
public IXamlType Vector2i { get; }
public IXamlConstructor Vector2iConstructorFull { get; }
public RXamlWellKnownTypes(TransformerConfiguration cfg)
{
var ts = cfg.TypeSystem;
XamlIlTypes = cfg.WellKnownTypes;
Single = ts.GetType("System.Single");
Int32 = ts.GetType("System.Int32");
(Vector2, Vector2ConstructorFull) = GetNumericTypeInfo("Robust.Shared.Maths.Vector2", Single, 2);
(Vector2i, Vector2iConstructorFull) = GetNumericTypeInfo("Robust.Shared.Maths.Vector2i", Int32, 2);
(IXamlType, IXamlConstructor) GetNumericTypeInfo(string name, IXamlType componentType, int componentCount)
{
var type = cfg.TypeSystem.GetType(name);
var ctor = type.GetConstructor(Enumerable.Repeat(componentType, componentCount).ToList());
return (type, ctor);
}
}
}
static class RXamlWellKnownTypesExtensions
{
public static RXamlWellKnownTypes GetRobustTypes(this AstTransformationContext ctx)
{
if (ctx.TryGetItem<RXamlWellKnownTypes>(out var rv))
return rv;
ctx.SetItem(rv = new RXamlWellKnownTypes(ctx.Configuration));
return rv;
}
}
}

View File

@@ -8,6 +8,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Build.Framework" Version="16.8.0" />
<PackageReference Include="Mono.Cecil" Version="0.11.3" />
<PackageReference Include="Pidgin" Version="2.5.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -6,6 +6,7 @@ using Microsoft.Build.Framework;
using Mono.Cecil;
using Mono.Cecil.Cil;
using Mono.Cecil.Rocks;
using Pidgin;
using XamlX;
using XamlX.Ast;
using XamlX.Emit;
@@ -70,7 +71,7 @@ namespace Robust.Build.Tasks
{
XmlnsAttributes =
{
typeSystem.GetType("Robust.Client.UserInterface.XAML.XmlnsDefinitionAttribute"),
typeSystem.GetType("Avalonia.Metadata.XmlnsDefinitionAttribute"),
},
ContentAttributes =
@@ -98,7 +99,7 @@ namespace Robust.Build.Tasks
typeSystem,
typeSystem.TargetAssembly,
xamlLanguage,
XamlXmlnsMappings.Resolve(typeSystem, xamlLanguage));
XamlXmlnsMappings.Resolve(typeSystem, xamlLanguage), CustomValueConverter);
var contextDef = new TypeDefinition("CompiledRobustXaml", "XamlIlContext",
TypeAttributes.Class, asm.MainModule.TypeSystem.Object);
@@ -297,6 +298,41 @@ namespace Robust.Build.Tasks
return true;
}
private static bool CustomValueConverter(
AstTransformationContext context,
IXamlAstValueNode node,
IXamlType type,
out IXamlAstValueNode result)
{
if (!(node is XamlAstTextNode textNode))
{
result = null;
return false;
}
var text = textNode.Text;
var types = context.GetRobustTypes();
if (type.Equals(types.Vector2))
{
var foo = MathParsing.Single2.Parse(text);
if (!foo.Success)
throw new XamlLoadException($"Unable to parse \"{text}\" as a Vector2", node);
var (x, y) = foo.Value;
result = new RXamlSingleVecLikeConstAstNode(
node,
types.Vector2, types.Vector2ConstructorFull,
types.Single, new[] {x, y});
return true;
}
result = null;
return false;
}
public const string ContextNameScopeFieldName = "RobustNameScope";
private static void EmitNameScopeField(XamlLanguageTypeMappings xamlLanguage, CecilTypeSystem typeSystem, IXamlTypeBuilder<IXamlILEmitter> typeBuilder, IXamlILEmitter constructor)

View File

@@ -24,6 +24,7 @@ namespace Robust.Client.NameGenerator
{
private const string AttributeName = "Robust.Client.AutoGenerated.GenerateTypedNameReferencesAttribute";
private const string AttributeFile = "GenerateTypedNameReferencesAttribute";
private const string AttributeCode = @"// <auto-generated />
using System;
namespace Robust.Client.AutoGenerated
@@ -54,8 +55,8 @@ namespace Robust.Client.AutoGenerated
{
var clrtype = objectNode.Type.GetClrType();
var isControl = IsControl(clrtype);
//clrtype.Interfaces.Any(i =>
//i.IsInterface && i.FullName == "Robust.Client.UserInterface.IControl");
//clrtype.Interfaces.Any(i =>
//i.IsInterface && i.FullName == "Robust.Client.UserInterface.IControl");
if (!isControl)
return node;
@@ -80,9 +81,13 @@ namespace Robust.Client.AutoGenerated
return node;
}
public void Push(IXamlAstNode node) { }
public void Push(IXamlAstNode node)
{
}
public void Pop() { }
public void Pop()
{
}
}
private static string GenerateSourceCode(
@@ -97,7 +102,10 @@ namespace Robust.Client.AutoGenerated
var compiler =
new XamlILCompiler(
new TransformerConfiguration(typeSystem, typeSystem.Assemblies[0],
new XamlLanguageTypeMappings(typeSystem)),
new XamlLanguageTypeMappings(typeSystem)
{
XmlnsAttributes = {typeSystem.GetType("Avalonia.Metadata.XmlnsDefinitionAttribute")}
}),
new XamlLanguageEmitMappings<IXamlILEmitter, XamlILNodeEmitResult>(), false);
compiler.Transformers.Add(new TypeReferenceResolver());
compiler.Transform(parsed);
@@ -105,8 +113,8 @@ namespace Robust.Client.AutoGenerated
var names = NameVisitor.GetNames(initialRoot);
//var names = NameVisitor.GetNames((XamlAstObjectNode)XDocumentXamlParser.Parse(xamlFile).Root);
var namedControls = names.Select(info => " " +
$"protected global::{info.type} {info.name} => " +
$"this.FindControl<global::{info.type}>(\"{info.name}\");");
$"protected global::{info.type} {info.name} => " +
$"this.FindControl<global::{info.type}>(\"{info.name}\");");
return $@"// <auto-generated />
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
@@ -124,7 +132,7 @@ namespace {nameSpace}
public void Execute(GeneratorExecutionContext context)
{
var comp = (CSharpCompilation) context.Compilation;
if(comp.GetTypeByMetadataName(AttributeName) == null)
if (comp.GetTypeByMetadataName(AttributeName) == null)
context.AddSource(AttributeFile, SourceText.From(AttributeCode, Encoding.UTF8));
if (!(context.SyntaxReceiver is NameReferenceSyntaxReceiver receiver))
{
@@ -132,7 +140,7 @@ namespace {nameSpace}
}
var symbols = UnpackAnnotatedTypes(context, comp, receiver);
if(symbols == null)
if (symbols == null)
return;
foreach (var typeSymbol in symbols)
@@ -168,7 +176,8 @@ namespace {nameSpace}
"Usage",
DiagnosticSeverity.Error,
true),
Location.Create(xamlFileName, new TextSpan(0,0), new LinePositionSpan(new LinePosition(0,0),new LinePosition(0,0)))));
Location.Create(xamlFileName, new TextSpan(0, 0),
new LinePositionSpan(new LinePosition(0, 0), new LinePosition(0, 0)))));
continue;
}
@@ -194,7 +203,8 @@ namespace {nameSpace}
}
}
private IReadOnlyList<INamedTypeSymbol> UnpackAnnotatedTypes(in GeneratorExecutionContext context, CSharpCompilation comp, NameReferenceSyntaxReceiver receiver)
private IReadOnlyList<INamedTypeSymbol> UnpackAnnotatedTypes(in GeneratorExecutionContext context,
CSharpCompilation comp, NameReferenceSyntaxReceiver receiver)
{
var options = (CSharpParseOptions) comp.SyntaxTrees[0].Options;
var compilation =

View File

@@ -1,10 +0,0 @@
namespace Avalonia.Data
{
// This type has to exist for Rider to recognize markup extensions like LocExtension.
// I am not kidding. This is the actual requirement.
// I decompiled Rider with Rider.
public class Binding
{
}
}

View File

@@ -1,15 +1,17 @@
<cc:SS14Window xmlns:cc="clr-namespace:Robust.Client.UserInterface.CustomControls"
xmlns:c="clr-namespace:Robust.Client.UserInterface.Controls">
<c:PanelContainer StyleClasses="windowPanel"/>
<c:VBoxContainer SeparationOverride="0">
<c:PanelContainer Name="WindowHeader" StyleClasses="windowHeader">
<c:HBoxContainer>
<c:MarginContainer MarginLeftOverride="5" SizeFlagsHorizontal="FillExpand">
<c:Label Name="TitleLabel" StyleIdentifier="foo" ClipText="True" Text="Exemplary Window Title Here" VAlign="Center" StyleClasses="windowTitle"/>
</c:MarginContainer>
<c:TextureButton Name="CloseButton" StyleClasses="windowCloseButton" SizeFlagsVertical="ShrinkCenter"></c:TextureButton>
</c:HBoxContainer>
</c:PanelContainer>
<c:MarginContainer Name="ContentsContainer" MarginBottomOverride="10" MarginLeftOverride="10" MarginRightOverride="10" MarginTopOverride="10" RectClipContent="True" SizeFlagsVertical="FillExpand" />
</c:VBoxContainer>
</cc:SS14Window>
<SS14Window xmlns="https://spacestation14.io">
<PanelContainer StyleClasses="windowPanel" />
<VBoxContainer SeparationOverride="0">
<PanelContainer Name="WindowHeader" StyleClasses="windowHeader">
<HBoxContainer>
<MarginContainer MarginLeftOverride="5" SizeFlagsHorizontal="FillExpand">
<Label Name="TitleLabel" StyleIdentifier="foo" ClipText="True"
Text="{Loc Exemplary Window Title Here}" VAlign="Center" StyleClasses="windowTitle" />
</MarginContainer>
<TextureButton Name="CloseButton" StyleClasses="windowCloseButton" SizeFlagsVertical="ShrinkCenter" />
</HBoxContainer>
</PanelContainer>
<MarginContainer Name="ContentsContainer" MarginBottomOverride="10" MarginLeftOverride="10"
MarginRightOverride="10" MarginTopOverride="10" RectClipContent="True"
SizeFlagsVertical="FillExpand" />
</VBoxContainer>
</SS14Window>

View File

@@ -1,7 +1,11 @@
using Robust.Shared.Localization;
using JetBrains.Annotations;
using Robust.Shared.Localization;
namespace Robust.Client.UserInterface
{
// TODO: Code a XAML compiler transformer to remove references to this type at compile time.
// And just replace them with the Loc.GetString() call.
[PublicAPI]
public class LocExtension
{
public string Key { get; }

View File

@@ -6,13 +6,6 @@ namespace Robust.Client.UserInterface.XAML
{
}
public class XmlnsDefinitionAttribute : Attribute
{
public XmlnsDefinitionAttribute(string xmlNamespace, string clrNamespace)
{
}
}
public class UsableDuringInitializationAttribute : Attribute
{
public UsableDuringInitializationAttribute(bool usable)

View File

@@ -0,0 +1,16 @@
using System;
using JetBrains.Annotations;
// ReSharper disable once CheckNamespace
namespace Avalonia.Data
{
[UsedImplicitly]
public class Binding
{
public Binding()
{
throw new InvalidOperationException(
"Data binding is not currently supported and this type exists only to make Rider work.");
}
}
}

View File

@@ -0,0 +1,6 @@
using Avalonia.Metadata;
[assembly: XmlnsDefinition("http://schemas.microsoft.com/winfx/2006/xaml", "Robust.Client.UserInterface.XAML.XNamespace")]
[assembly: XmlnsDefinition("https://spacestation14.io", "Robust.Client.UserInterface")]
[assembly: XmlnsDefinition("https://spacestation14.io", "Robust.Client.UserInterface.Controls")]
[assembly: XmlnsDefinition("https://spacestation14.io", "Robust.Client.UserInterface.CustomControls")]

View File

@@ -0,0 +1,12 @@
using JetBrains.Annotations;
// ReSharper disable once CheckNamespace
namespace Robust.Client.UserInterface.XAML.XNamespace
{
// x:Null
[UsedImplicitly]
public sealed class NullExtension
{
}
}

View File

@@ -0,0 +1,28 @@
# XamlUI notes about Rider
XAML support in both Rider and VS is pretty hardcoded for all supported frameworks. Rider seems less bad about this but they still hardcode a lot of type paths and do other silly stuff. Luckily, they support Avalonia. So... let's pretend to be Avalonia!
**JetBrains, if you're reading this, please don't sue me or take away our licenses pls I only reverse engineered your program to avoid wasting time on your issue tracker.**
## Where to find this info
The primary XAML support in Rider appears to be in `lib/ReSharperHost/JetBrains.ReSharper.Psi.Xaml.dll`. I recommend you decompile it with DotPeek to a project for navigating around. You can then look around the decompiled code (it's not obfuscated at all) to figure out how Rider does all this stuff.
**JetBrains, if you're reading this, please don't sue me or take away our licenses pls I only reverse engineered your program to avoid wasting time on your issue tracker.**
## Attributes
Attributes like `XmlnsAttributionAttribute` are hardcoded for full name with namespace. That's also where it stops, so we just define our own `Avalonia.Metadata.XmlnsDefinitionAttribute` and this fools Rider into recognizing it and working. Hooray!
## `http://schemas.microsoft.com/winfx/2006/xaml` Namespace
As far as I can tell, at least in Avalonia, the things in this namespace are not backed by any real .NET symbols and are just made up by Rider. This is a problem because it only does this making up if it detects that you're using Avalonia.
The heuristic for "are we using Avalonia" appears to be finding an assembly or project reference for `Avalonia.Base`. You can see this in `XamlModulePlatformCache.cs` in the decompiled ReSharper source code and also the `XamlPlatform` flags enum.
For the time being I decided to hold off on actually fully pretending we're Avalonia so did *not* commit to a reference to `Avalonia.Base` yet. Instead we just manually created stuff like a dummy `StaticExtension` class (with a constructor that always throws) and put it into the `http://schemas.microsoft.com/winfx/2006/xaml` namespace. This seems to satisfy Rider without interfering with the actual XamlIL compilation.
## Markup Extensions
Markup extensions have to be classes with a method of signature either `object ProvideValue(IServiceProvider)` or `object ProvideValue()`.
It should be noted that Rider refuses to acknowledge markup extensions unless it finds Avalonia's `Avalonia.Data.Binding` type (or `Avalonia.Markup.Xaml.Data.Binding`). Yes, this is an actual requirement.

View File

@@ -0,0 +1,22 @@
using System;
using JetBrains.Annotations;
// ReSharper disable once CheckNamespace
namespace Robust.Client.UserInterface.XAML.XNamespace
{
// x:Static
[UsedImplicitly]
public sealed class StaticExtension
{
public StaticExtension(string _)
{
throw new InvalidOperationException(
"This type only exists to make Rider work and should never be instantiated.");
}
public static object ProvideValue()
{
throw new InvalidOperationException();
}
}
}

View File

@@ -0,0 +1,22 @@
using System;
using JetBrains.Annotations;
// ReSharper disable once CheckNamespace
namespace Robust.Client.UserInterface.XAML.XNamespace
{
// x:Type
[UsedImplicitly]
public sealed class TypeExtension
{
public TypeExtension(string _)
{
throw new InvalidOperationException(
"This type only exists to make Rider work and should never be instantiated.");
}
public static object ProvideValue()
{
throw new InvalidOperationException();
}
}
}

View File

@@ -0,0 +1,20 @@
using System;
using JetBrains.Annotations;
// ReSharper disable once CheckNamespace
namespace Avalonia.Metadata
{
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
[PublicAPI]
public sealed class XmlnsDefinitionAttribute : Attribute
{
public XmlnsDefinitionAttribute(string xmlNamespace, string clrNamespace)
{
XmlNamespace = xmlNamespace;
ClrNamespace = clrNamespace;
}
public string XmlNamespace { get; }
public string ClrNamespace { get; }
}
}