committed by
GitHub
41 changed files with 1669 additions and 109 deletions
@ -0,0 +1,51 @@ |
|||
<UserControl x:Class="ControlCatalog.Pages.PlatformInfoPage" |
|||
xmlns="https://github.com/avaloniaui" |
|||
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" |
|||
d:DesignHeight="800" |
|||
d:DesignWidth="400" |
|||
mc:Ignorable="d"> |
|||
<StackPanel Spacing="20"> |
|||
<TextBlock Text="{Binding PlatformInfo}" /> |
|||
|
|||
<StackPanel TextElement.Foreground="White"> |
|||
<StackPanel Orientation="Horizontal"> |
|||
<Border Height="100" Width="100" Background="{OnFormFactor Gray, Desktop=Green}"> |
|||
<TextBlock Text="Desktop" /> |
|||
</Border> |
|||
<Border Height="100" Width="100" Background="{OnFormFactor Gray, Mobile=Green}"> |
|||
<TextBlock Text="Mobile" /> |
|||
</Border> |
|||
</StackPanel> |
|||
<WrapPanel> |
|||
<Border Height="100" Width="100" Background="{OnPlatform Gray, Windows=Green}"> |
|||
<TextBlock Text="Windows" /> |
|||
</Border> |
|||
<Border Height="100" Width="100" Background="{OnPlatform Gray, macOS=Green}"> |
|||
<TextBlock Text="macOS" /> |
|||
</Border> |
|||
<Border Height="100" Width="100" Background="{OnPlatform Gray, Linux=Green}"> |
|||
<TextBlock Text="Linux" /> |
|||
</Border> |
|||
<Border Height="100" Width="100" Background="{OnPlatform Gray, Browser=Green}"> |
|||
<TextBlock Text="Browser" /> |
|||
</Border> |
|||
<Border Height="100" Width="100" Background="{OnPlatform Gray, iOS=Green}"> |
|||
<TextBlock Text="iOS" /> |
|||
</Border> |
|||
<Border Height="100" Width="100" Background="{OnPlatform Gray, Android=Green}"> |
|||
<TextBlock Text="Android" /> |
|||
</Border> |
|||
<Border Height="100" Width="100"> |
|||
<Border.Background> |
|||
<OnPlatform Default="Gray" > |
|||
<On Options="macOS, Linux, Windows" Content="Green" /> |
|||
</OnPlatform> |
|||
</Border.Background> |
|||
<TextBlock Text="Win, Lin or Mac" /> |
|||
</Border> |
|||
</WrapPanel> |
|||
</StackPanel> |
|||
</StackPanel> |
|||
</UserControl> |
|||
@ -0,0 +1,20 @@ |
|||
using Avalonia.Controls; |
|||
using Avalonia.Markup.Xaml; |
|||
using ControlCatalog.ViewModels; |
|||
|
|||
namespace ControlCatalog.Pages |
|||
{ |
|||
public class PlatformInfoPage : UserControl |
|||
{ |
|||
public PlatformInfoPage() |
|||
{ |
|||
this.InitializeComponent(); |
|||
DataContext = new PlatformInformationViewModel(); |
|||
} |
|||
|
|||
private void InitializeComponent() |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,54 @@ |
|||
using Avalonia; |
|||
using Avalonia.Platform; |
|||
using MiniMvvm; |
|||
|
|||
namespace ControlCatalog.ViewModels; |
|||
#nullable enable |
|||
|
|||
public class PlatformInformationViewModel : ViewModelBase |
|||
{ |
|||
public PlatformInformationViewModel() |
|||
{ |
|||
var runtimeInfo = AvaloniaLocator.Current.GetService<IRuntimePlatform>()?.GetRuntimeInfo(); |
|||
|
|||
if (runtimeInfo is { } info) |
|||
{ |
|||
if (info.IsBrowser) |
|||
{ |
|||
if (info.IsDesktop) |
|||
{ |
|||
PlatformInfo = "Platform: Desktop (browser)"; |
|||
} |
|||
else if (info.IsMobile) |
|||
{ |
|||
PlatformInfo = "Platform: Mobile (browser)"; |
|||
} |
|||
else |
|||
{ |
|||
PlatformInfo = "Platform: Unknown (browser) - please report"; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
if (info.IsDesktop) |
|||
{ |
|||
PlatformInfo = "Platform: Desktop (native)"; |
|||
} |
|||
else if (info.IsMobile) |
|||
{ |
|||
PlatformInfo = "Platform: Mobile (native)"; |
|||
} |
|||
else |
|||
{ |
|||
PlatformInfo = "Platform: Unknown (native) - please report"; |
|||
} |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
|
|||
} |
|||
} |
|||
|
|||
public string PlatformInfo { get; } |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
using System; |
|||
|
|||
namespace Avalonia.Metadata; |
|||
|
|||
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] |
|||
public sealed class MarkupExtensionOptionAttribute : Attribute |
|||
{ |
|||
public MarkupExtensionOptionAttribute(object value) |
|||
{ |
|||
Value = value; |
|||
} |
|||
|
|||
public object Value { get; } |
|||
|
|||
public int Priority { get; set; } = 0; |
|||
} |
|||
|
|||
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] |
|||
public sealed class MarkupExtensionDefaultOptionAttribute : Attribute |
|||
{ |
|||
|
|||
} |
|||
@ -1,11 +1,8 @@ |
|||
using System; |
|||
using System.Reflection; |
|||
using Avalonia.Controls; |
|||
using Avalonia.VisualTree; |
|||
|
|||
namespace Avalonia.Diagnostics |
|||
{ |
|||
static class Convetions |
|||
internal static class Conventions |
|||
{ |
|||
public static string DefaultScreenshotsRoot => |
|||
System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures, Environment.SpecialFolderOption.Create), |
|||
@ -0,0 +1,403 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Reflection.Emit; |
|||
using XamlX; |
|||
using XamlX.Ast; |
|||
using XamlX.Emit; |
|||
using XamlX.IL; |
|||
using XamlX.Transform; |
|||
using XamlX.TypeSystem; |
|||
|
|||
namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; |
|||
|
|||
internal class AvaloniaXamlIlOptionMarkupExtensionTransformer : IXamlAstTransformer |
|||
{ |
|||
public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) |
|||
{ |
|||
if (node is XamlMarkupExtensionNode |
|||
{ |
|||
Value: XamlAstObjectNode { Type: XamlAstClrTypeReference { Type: { } type } } objectNode |
|||
} markupExtensionNode |
|||
&& type.FindMethods(m => m.IsPublic && m.Parameters.Count is 1 or 2 && m.ReturnType == context.Configuration.WellKnownTypes.Boolean && m.Name == "ShouldProvideOption").ToArray() is { } methods |
|||
&& methods.Any()) |
|||
{ |
|||
var optionAttribute = context.GetAvaloniaTypes().MarkupExtensionOptionAttribute; |
|||
var defaultOptionAttribute = context.GetAvaloniaTypes().MarkupExtensionDefaultOptionAttribute; |
|||
|
|||
var typeArgument = type.GenericArguments?.FirstOrDefault(); |
|||
|
|||
IXamlAstValueNode defaultValue = null; |
|||
var values = new List<OptionsMarkupExtensionBranch>(); |
|||
|
|||
if (objectNode.Arguments.FirstOrDefault() is { } argument) |
|||
{ |
|||
var hasDefaultProp = objectNode.Type.GetClrType().GetAllProperties().Any(p => |
|||
p.CustomAttributes.Any(a => a.Type == defaultOptionAttribute)); |
|||
if (hasDefaultProp) |
|||
{ |
|||
if (objectNode.Arguments.Count > 1) |
|||
{ |
|||
throw new XamlParseException("Options MarkupExtensions allow only single argument", objectNode); |
|||
} |
|||
|
|||
defaultValue = TransformNode(new[] { argument }, typeArgument, objectNode); |
|||
objectNode.Arguments.Remove(argument); |
|||
} |
|||
} |
|||
|
|||
foreach (var extProp in objectNode.Children.OfType<XamlAstXamlPropertyValueNode>().ToArray()) |
|||
{ |
|||
if (!extProp.Values.Any()) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
var shouldRemoveProp = false; |
|||
var onObjs = extProp.Values.OfType<XamlAstObjectNode>() |
|||
.Where(o => o.Type.GetClrType() == context.GetAvaloniaTypes().OnExtensionType).ToArray(); |
|||
if (onObjs.Any()) |
|||
{ |
|||
shouldRemoveProp = true; |
|||
foreach (var onObj in onObjs) |
|||
{ |
|||
var optionsPropNode = onObj.Children.OfType<XamlAstXamlPropertyValueNode>() |
|||
.SingleOrDefault(v => v.Property.GetClrProperty().Name == "Options") |
|||
?.Values.Single(); |
|||
var options = (optionsPropNode as XamlAstTextNode)?.Text?.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries) |
|||
?? Array.Empty<string>(); |
|||
if (options.Length == 0) |
|||
{ |
|||
throw new XamlParseException("On.Options string must be set", onObj); |
|||
} |
|||
|
|||
var content = onObj.Children.OfType<XamlAstXamlPropertyValueNode>() |
|||
.SingleOrDefault(v => v.Property.GetClrProperty().Name == "Content"); |
|||
if (content is null) |
|||
{ |
|||
throw new XamlParseException("On content object must be set", onObj); |
|||
} |
|||
|
|||
var propertiesSet = options |
|||
.Select(o => type.GetAllProperties() |
|||
.FirstOrDefault(p => o.Equals(p.Name, StringComparison.Ordinal)) |
|||
?? throw new XamlParseException($"Property \"{o}\" wasn't found on the \"{type.Name}\" type", onObj)) |
|||
.ToArray(); |
|||
foreach (var propertySet in propertiesSet) |
|||
{ |
|||
AddBranchNode(content.Values, propertySet.CustomAttributes, content); |
|||
} |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
shouldRemoveProp = AddBranchNode(extProp.Values, extProp.Property.GetClrProperty().CustomAttributes, extProp); |
|||
} |
|||
|
|||
if (shouldRemoveProp) |
|||
{ |
|||
objectNode.Children.Remove(extProp); |
|||
} |
|||
} |
|||
|
|||
if (defaultValue is null && !values.Any()) |
|||
{ |
|||
throw new XamlParseException("Options markup extension requires at least one option to be set", objectNode); |
|||
} |
|||
|
|||
return new OptionsMarkupExtensionNode( |
|||
markupExtensionNode, values.ToArray(), defaultValue, |
|||
context.Configuration.TypeMappings.ServiceProvider); |
|||
|
|||
bool AddBranchNode( |
|||
IReadOnlyCollection<IXamlAstValueNode> valueNodes, |
|||
IReadOnlyCollection<IXamlCustomAttribute> propAttributes, |
|||
IXamlLineInfo li) |
|||
{ |
|||
var transformed = TransformNode(valueNodes, typeArgument, li); |
|||
if (propAttributes.FirstOrDefault(a => a.Type == defaultOptionAttribute) is { } defAttr) |
|||
{ |
|||
defaultValue = transformed; |
|||
return true; |
|||
} |
|||
else if (propAttributes.FirstOrDefault(a => a.Type == optionAttribute) is { } optAttr) |
|||
{ |
|||
var option = optAttr.Parameters.Single(); |
|||
if (option is null) |
|||
{ |
|||
throw new XamlParseException("MarkupExtension option must not be null", li); |
|||
} |
|||
|
|||
var optionAsString = option.ToString(); |
|||
IXamlAstValueNode optionNode = null; |
|||
foreach (var method in methods) |
|||
{ |
|||
try |
|||
{ |
|||
var targetType = method.Parameters.Last(); |
|||
if (targetType.FullName == "System.Type") |
|||
{ |
|||
if (option is IXamlType typeOption) |
|||
{ |
|||
optionNode = new XamlTypeExtensionNode(li, |
|||
new XamlAstClrTypeReference(li, typeOption, false), targetType); |
|||
} |
|||
} |
|||
else if (targetType == context.Configuration.WellKnownTypes.String) |
|||
{ |
|||
optionNode = new XamlConstantNode(li, targetType, optionAsString); |
|||
} |
|||
else if (targetType.IsEnum) |
|||
{ |
|||
if (TypeSystemHelpers.TryGetEnumValueNode(targetType, optionAsString, li, false, |
|||
out var enumConstantNode)) |
|||
{ |
|||
optionNode = enumConstantNode; |
|||
} |
|||
} |
|||
else if (TypeSystemHelpers.ParseConstantIfTypeAllows(optionAsString, targetType, li, |
|||
out var constantNode)) |
|||
{ |
|||
optionNode = constantNode; |
|||
} |
|||
} |
|||
catch (FormatException) |
|||
{ |
|||
// try next method overload
|
|||
} |
|||
|
|||
if (optionNode is not null) |
|||
{ |
|||
values.Add(new OptionsMarkupExtensionBranch(optionNode, transformed, method)); |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
throw new XamlParseException($"Option value \"{optionAsString}\" is not assignable to any of existing ShouldProvideOption methods", li); |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
} |
|||
|
|||
return node; |
|||
|
|||
IXamlAstValueNode TransformNode( |
|||
IReadOnlyCollection<IXamlAstValueNode> values, |
|||
IXamlType suggestedType, |
|||
IXamlLineInfo line) |
|||
{ |
|||
if (suggestedType is not null) |
|||
{ |
|||
values = values |
|||
.Select(v => XamlTransformHelpers |
|||
.TryGetCorrectlyTypedValue(context, v, suggestedType, out var converted) |
|||
? converted : v) |
|||
.ToArray(); |
|||
} |
|||
|
|||
if (values.Count > 1) |
|||
{ |
|||
throw new XamlParseException("Options markup extension supports only a singular value", line); |
|||
} |
|||
|
|||
return values.Single(); |
|||
} |
|||
} |
|||
|
|||
internal sealed class OptionsMarkupExtensionNode : XamlMarkupExtensionNode, IXamlAstValueNode |
|||
{ |
|||
private readonly IXamlType _contextParameter; |
|||
|
|||
public OptionsMarkupExtensionNode( |
|||
XamlMarkupExtensionNode original, |
|||
OptionsMarkupExtensionBranch[] branches, |
|||
IXamlAstValueNode defaultNode, |
|||
IXamlType contextParameter) |
|||
: base( |
|||
original.Value, |
|||
new OptionsMarkupExtensionMethod(new OptionsMarkupExtensionNodesContainer(branches, defaultNode), original.Value.Type.GetClrType(), contextParameter), |
|||
original.Value) |
|||
{ |
|||
_contextParameter = contextParameter; |
|||
} |
|||
|
|||
public new OptionsMarkupExtensionMethod ProvideValue => (OptionsMarkupExtensionMethod)base.ProvideValue; |
|||
|
|||
IXamlAstTypeReference IXamlAstValueNode.Type => new XamlAstClrTypeReference(this, ProvideValue.ReturnType, false); |
|||
|
|||
public override void VisitChildren(IXamlAstVisitor visitor) |
|||
{ |
|||
ProvideValue.ExtensionNodeContainer.Visit(visitor); |
|||
base.VisitChildren(visitor); |
|||
} |
|||
|
|||
public bool ConvertToReturnType(AstTransformationContext context, IXamlType type, out OptionsMarkupExtensionNode res) |
|||
{ |
|||
IXamlAstValueNode convertedDefaultNode = null; |
|||
|
|||
if (ProvideValue.ExtensionNodeContainer.DefaultNode is { } defaultNode) |
|||
{ |
|||
if (!XamlTransformHelpers.TryGetCorrectlyTypedValue(context, defaultNode, type, out convertedDefaultNode)) |
|||
{ |
|||
res = null; |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
var convertedBranches = ProvideValue.ExtensionNodeContainer.Branches.Select(b => XamlTransformHelpers |
|||
.TryGetCorrectlyTypedValue(context, b.Value, type, out var convertedValue) ? |
|||
new OptionsMarkupExtensionBranch(b.Option, convertedValue, b.ConditionMethod) : |
|||
null).ToArray(); |
|||
if (convertedBranches.Any(b => b is null)) |
|||
{ |
|||
res = null; |
|||
return false; |
|||
} |
|||
|
|||
res = new OptionsMarkupExtensionNode(this, convertedBranches, convertedDefaultNode, _contextParameter); |
|||
return true; |
|||
} |
|||
} |
|||
|
|||
internal sealed class OptionsMarkupExtensionNodesContainer : XamlAstNode |
|||
{ |
|||
public OptionsMarkupExtensionNodesContainer( |
|||
OptionsMarkupExtensionBranch[] branches, |
|||
IXamlAstValueNode defaultNode) : base(branches.FirstOrDefault()?.Value ?? defaultNode) |
|||
{ |
|||
Branches = branches; |
|||
DefaultNode = defaultNode; |
|||
} |
|||
|
|||
public OptionsMarkupExtensionBranch[] Branches { get; } |
|||
public IXamlAstValueNode DefaultNode { get; private set; } |
|||
|
|||
public override void VisitChildren(IXamlAstVisitor visitor) |
|||
{ |
|||
VisitList(Branches, visitor); |
|||
DefaultNode = (IXamlAstValueNode)DefaultNode?.Visit(visitor); |
|||
} |
|||
|
|||
public IXamlType GetReturnType() |
|||
{ |
|||
var types = Branches.Select(b => b.Value.Type); |
|||
if (DefaultNode?.Type is { } type) |
|||
{ |
|||
types = types.Concat(new [] { type }); |
|||
} |
|||
return types.Select(t => t.GetClrType()).ToArray().GetCommonBaseClass(); |
|||
} |
|||
} |
|||
|
|||
internal sealed class OptionsMarkupExtensionBranch : XamlAstNode |
|||
{ |
|||
public OptionsMarkupExtensionBranch(IXamlAstValueNode option, IXamlAstValueNode value, IXamlMethod conditionMethod) : base(value) |
|||
{ |
|||
Option = option; |
|||
Value = value; |
|||
ConditionMethod = conditionMethod; |
|||
} |
|||
|
|||
public IXamlAstValueNode Option { get; set; } |
|||
public IXamlAstValueNode Value { get; set; } |
|||
public IXamlMethod ConditionMethod { get; } |
|||
|
|||
public bool HasContext => ConditionMethod.Parameters.Count > 1; |
|||
|
|||
public override void VisitChildren(IXamlAstVisitor visitor) |
|||
{ |
|||
Option = (IXamlAstValueNode)Option.Visit(visitor); |
|||
Value = (IXamlAstValueNode)Value.Visit(visitor); |
|||
} |
|||
} |
|||
|
|||
internal sealed class OptionsMarkupExtensionMethod : IXamlCustomEmitMethodWithContext<IXamlILEmitter, XamlILNodeEmitResult> |
|||
{ |
|||
public OptionsMarkupExtensionMethod( |
|||
OptionsMarkupExtensionNodesContainer extensionNodeContainer, |
|||
IXamlType declaringType, |
|||
IXamlType contextParameter) |
|||
{ |
|||
ExtensionNodeContainer = extensionNodeContainer; |
|||
DeclaringType = declaringType; |
|||
Parameters = extensionNodeContainer.Branches.Any(c => c.HasContext) ? |
|||
new[] { contextParameter } : |
|||
Array.Empty<IXamlType>(); |
|||
} |
|||
|
|||
public OptionsMarkupExtensionNodesContainer ExtensionNodeContainer { get; } |
|||
|
|||
public string Name => "ProvideValue"; |
|||
public bool IsPublic => true; |
|||
public bool IsStatic => false; |
|||
public IXamlType ReturnType => ExtensionNodeContainer.GetReturnType(); |
|||
public IReadOnlyList<IXamlType> Parameters { get; } |
|||
public IXamlType DeclaringType { get; } |
|||
public IXamlMethod MakeGenericMethod(IReadOnlyList<IXamlType> typeArguments) => throw new NotImplementedException(); |
|||
public IReadOnlyList<IXamlCustomAttribute> CustomAttributes => Array.Empty<IXamlCustomAttribute>(); |
|||
|
|||
public void EmitCall(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context, IXamlILEmitter codeGen) |
|||
{ |
|||
// At this point this extension will be called from MarkupExtensionEmitter.
|
|||
// Since it's a "fake" method, we share stack and locals with parent method.
|
|||
// Real ProvideValue method would pop 2 parameters from the stack and return one. This method should do the same.
|
|||
// At this point we will have on stack:
|
|||
// - context (if parameters > 1)
|
|||
// - markup ext "@this" instance (always)
|
|||
// We always pop context from the stack, as this method decide by itself either context is needed.
|
|||
// We store "@this" as a local variable. But only if any conditional method is an instance method.
|
|||
IXamlLocal @this = null; |
|||
if (Parameters.Count > 0) |
|||
{ |
|||
codeGen.Pop(); |
|||
} |
|||
if (ExtensionNodeContainer.Branches.Any(b => !b.ConditionMethod.IsStatic)) |
|||
{ |
|||
codeGen.Stloc(@this = codeGen.DefineLocal(DeclaringType)); |
|||
} |
|||
else |
|||
{ |
|||
codeGen.Pop(); |
|||
} |
|||
|
|||
// Iterate over all branches and push prepared locals into the stack if needed.
|
|||
var ret = codeGen.DefineLabel(); |
|||
foreach (var branch in ExtensionNodeContainer.Branches) |
|||
{ |
|||
var next = codeGen.DefineLabel(); |
|||
if (branch.HasContext) |
|||
{ |
|||
codeGen.Ldloc(context.ContextLocal); |
|||
} |
|||
if (!branch.ConditionMethod.IsStatic) |
|||
{ |
|||
codeGen.Ldloc(@this); |
|||
} |
|||
context.Emit(branch.Option, codeGen, branch.Option.Type.GetClrType()); |
|||
codeGen.EmitCall(branch.ConditionMethod); |
|||
codeGen.Brfalse(next); |
|||
|
|||
context.Emit(branch.Value, codeGen, branch.Value.Type.GetClrType()); |
|||
codeGen.Br(ret); |
|||
codeGen.MarkLabel(next); |
|||
} |
|||
|
|||
if (ExtensionNodeContainer.DefaultNode is {} defaultNode) |
|||
{ |
|||
// Nop is needed, otherwise Label wouldn't be set on nested CALL op (limitation of our IL validator).
|
|||
codeGen.Emit(OpCodes.Nop); |
|||
context.Emit(defaultNode, codeGen, defaultNode.Type.GetClrType()); |
|||
} |
|||
else |
|||
{ |
|||
codeGen.EmitDefault(ReturnType); |
|||
} |
|||
|
|||
codeGen.MarkLabel(ret); |
|||
} |
|||
|
|||
public bool Equals(IXamlMethod other) => ReferenceEquals(this, other); |
|||
} |
|||
} |
|||
|
|||
@ -1 +1 @@ |
|||
Subproject commit c1c0594ec2c35b08988183b1a5b3e34dfa19179d |
|||
Subproject commit 880ba5742e52b67afda048c4023cf7e3c3c16a46 |
|||
@ -0,0 +1,15 @@ |
|||
#nullable enable |
|||
using System.Collections.Generic; |
|||
using Avalonia.Metadata; |
|||
|
|||
namespace Avalonia.Markup.Xaml.MarkupExtensions; |
|||
|
|||
public class On<TReturn> |
|||
{ |
|||
public IReadOnlyList<string> Options { get; } = new List<string>(); |
|||
|
|||
[Content] |
|||
public TReturn? Content { get; set; } |
|||
} |
|||
|
|||
public class On : On<object> {} |
|||
@ -0,0 +1,60 @@ |
|||
#nullable enable |
|||
using System; |
|||
|
|||
using Avalonia.Metadata; |
|||
using Avalonia.Platform; |
|||
|
|||
namespace Avalonia.Markup.Xaml.MarkupExtensions; |
|||
|
|||
public class OnFormFactorExtension : OnFormFactorExtensionBase<object, On> |
|||
{ |
|||
public OnFormFactorExtension() |
|||
{ |
|||
|
|||
} |
|||
|
|||
public OnFormFactorExtension(object defaultValue) |
|||
{ |
|||
Default = defaultValue; |
|||
} |
|||
|
|||
public static bool ShouldProvideOption(IServiceProvider serviceProvider, FormFactorType option) |
|||
{ |
|||
return serviceProvider.GetService<IRuntimePlatform>().GetRuntimeInfo().FormFactor == option; |
|||
} |
|||
} |
|||
|
|||
public class OnFormFactorExtension<TReturn> : OnFormFactorExtensionBase<TReturn, On<TReturn>> |
|||
{ |
|||
public OnFormFactorExtension() |
|||
{ |
|||
|
|||
} |
|||
|
|||
public OnFormFactorExtension(TReturn defaultValue) |
|||
{ |
|||
Default = defaultValue; |
|||
} |
|||
|
|||
public static bool ShouldProvideOption(IServiceProvider serviceProvider, FormFactorType option) |
|||
{ |
|||
return serviceProvider.GetService<IRuntimePlatform>().GetRuntimeInfo().FormFactor == option; |
|||
} |
|||
} |
|||
|
|||
public abstract class OnFormFactorExtensionBase<TReturn, TOn> : IAddChild<TOn> |
|||
where TOn : On<TReturn> |
|||
{ |
|||
[MarkupExtensionDefaultOption] |
|||
public TReturn? Default { get; set; } |
|||
|
|||
[MarkupExtensionOption(FormFactorType.Desktop)] |
|||
public TReturn? Desktop { get; set; } |
|||
|
|||
[MarkupExtensionOption(FormFactorType.Mobile)] |
|||
public TReturn? Mobile { get; set; } |
|||
|
|||
// Required for the compiler, will be replaced with actual method compile time.
|
|||
public object ProvideValue() { return this; } |
|||
void IAddChild<TOn>.AddChild(TOn child) {} |
|||
} |
|||
@ -0,0 +1,73 @@ |
|||
#nullable enable |
|||
using System; |
|||
using Avalonia.Metadata; |
|||
using Avalonia.Platform; |
|||
|
|||
namespace Avalonia.Markup.Xaml.MarkupExtensions; |
|||
|
|||
public class OnPlatformExtension : OnPlatformExtensionBase<object, On> |
|||
{ |
|||
public OnPlatformExtension() |
|||
{ |
|||
|
|||
} |
|||
|
|||
public OnPlatformExtension(object defaultValue) |
|||
{ |
|||
Default = defaultValue; |
|||
} |
|||
|
|||
public static bool ShouldProvideOption(IServiceProvider serviceProvider, OperatingSystemType option) |
|||
{ |
|||
return serviceProvider.GetService<IRuntimePlatform>().GetRuntimeInfo().OperatingSystem == option; |
|||
} |
|||
} |
|||
|
|||
public class OnPlatformExtension<TReturn> : OnPlatformExtensionBase<TReturn, On<TReturn>> |
|||
{ |
|||
public OnPlatformExtension() |
|||
{ |
|||
|
|||
} |
|||
|
|||
public OnPlatformExtension(TReturn defaultValue) |
|||
{ |
|||
Default = defaultValue; |
|||
} |
|||
|
|||
public static bool ShouldProvideOption(IServiceProvider serviceProvider, OperatingSystemType option) |
|||
{ |
|||
return serviceProvider.GetService<IRuntimePlatform>().GetRuntimeInfo().OperatingSystem == option; |
|||
} |
|||
} |
|||
|
|||
public abstract class OnPlatformExtensionBase<TReturn, TOn> : IAddChild<TOn> |
|||
where TOn : On<TReturn> |
|||
{ |
|||
[MarkupExtensionDefaultOption] |
|||
public TReturn? Default { get; set; } |
|||
|
|||
[MarkupExtensionOption(OperatingSystemType.WinNT)] |
|||
public TReturn? Windows { get; set; } |
|||
|
|||
[MarkupExtensionOption(OperatingSystemType.OSX)] |
|||
// ReSharper disable once InconsistentNaming
|
|||
public TReturn? macOS { get; set; } |
|||
|
|||
[MarkupExtensionOption(OperatingSystemType.Linux)] |
|||
public TReturn? Linux { get; set; } |
|||
|
|||
[MarkupExtensionOption(OperatingSystemType.Android)] |
|||
public TReturn? Android { get; set; } |
|||
|
|||
[MarkupExtensionOption(OperatingSystemType.iOS)] |
|||
// ReSharper disable once InconsistentNaming
|
|||
public TReturn? iOS { get; set; } |
|||
|
|||
[MarkupExtensionOption(OperatingSystemType.Browser)] |
|||
public TReturn? Browser { get; set; } |
|||
|
|||
// Required for the compiler, will be replaced with actual method compile time.
|
|||
public object ProvideValue() { return this; } |
|||
void IAddChild<TOn>.AddChild(TOn child) {} |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
using System; |
|||
using System.Runtime.InteropServices; |
|||
using System.Runtime.InteropServices.JavaScript; |
|||
using System.Text.RegularExpressions; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Web.Interop; |
|||
|
|||
namespace Avalonia.Web; |
|||
|
|||
internal class BrowserRuntimePlatform : StandardRuntimePlatform |
|||
{ |
|||
private static readonly Lazy<RuntimePlatformInfo> Info = new(() => |
|||
{ |
|||
var result = new RuntimePlatformInfo |
|||
{ |
|||
IsCoreClr = true, // WASM browser is always CoreCLR
|
|||
IsBrowser = true, // BrowserRuntimePlatform only runs on Browser.
|
|||
OperatingSystem = OperatingSystemType.Browser, |
|||
IsMobile = AvaloniaModule.IsMobile() |
|||
}; |
|||
|
|||
return result; |
|||
}); |
|||
|
|||
public override RuntimePlatformInfo GetRuntimeInfo() => Info.Value; |
|||
} |
|||
@ -1,13 +1,25 @@ |
|||
export class Caniuse { |
|||
public static canShowOpenFilePicker(): boolean { |
|||
return typeof window.showOpenFilePicker !== "undefined"; |
|||
return typeof globalThis.showOpenFilePicker !== "undefined"; |
|||
} |
|||
|
|||
public static canShowSaveFilePicker(): boolean { |
|||
return typeof window.showSaveFilePicker !== "undefined"; |
|||
return typeof globalThis.showSaveFilePicker !== "undefined"; |
|||
} |
|||
|
|||
public static canShowDirectoryPicker(): boolean { |
|||
return typeof window.showDirectoryPicker !== "undefined"; |
|||
return typeof globalThis.showDirectoryPicker !== "undefined"; |
|||
} |
|||
|
|||
public static isMobile(): boolean { |
|||
const userAgentData = (globalThis.navigator as any)?.userAgentData; |
|||
if (userAgentData) { |
|||
return userAgentData.mobile; |
|||
} |
|||
|
|||
const userAgent = navigator.userAgent; |
|||
const regex1 = /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i; |
|||
const regex2 = /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw(n|u)|c55\/|capi|ccwa|cdm|cell|chtm|cldc|cmd|co(mp|nd)|craw|da(it|ll|ng)|dbte|dcs|devi|dica|dmob|do(c|p)o|ds(12|d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(|_)|g1 u|g560|gene|gf5|gmo|go(\.w|od)|gr(ad|un)|haie|hcit|hd(m|p|t)|hei|hi(pt|ta)|hp( i|ip)|hsc|ht(c(| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i(20|go|ma)|i230|iac( ||\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|[a-w])|libw|lynx|m1w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|mcr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|([1-8]|c))|phil|pire|pl(ay|uc)|pn2|po(ck|rt|se)|prox|psio|ptg|qaa|qc(07|12|21|32|60|[2-7]|i)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h|oo|p)|sdk\/|se(c(|0|1)|47|mc|nd|ri)|sgh|shar|sie(|m)|sk0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h|v|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl|tdg|tel(i|m)|tim|tmo|to(pl|sh)|ts(70|m|m3|m5)|tx9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas|your|zeto|zte/i; |
|||
return regex1.test(userAgent) || regex2.test(userAgent.substr(0, 4)); |
|||
} |
|||
} |
|||
|
|||
@ -0,0 +1,71 @@ |
|||
using Avalonia.Controls; |
|||
using Avalonia.Platform; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions; |
|||
|
|||
public class OnFormFactorExtensionTests : XamlTestBase |
|||
{ |
|||
[Fact] |
|||
public void Should_Resolve_Default_Value() |
|||
{ |
|||
using (AvaloniaLocator.EnterScope()) |
|||
{ |
|||
AvaloniaLocator.CurrentMutable.Bind<IRuntimePlatform>() |
|||
.ToConstant(new TestRuntimePlatform(false, false)); |
|||
|
|||
var xaml = @"
|
|||
<UserControl xmlns='https://github.com/avaloniaui'
|
|||
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
|
|||
<TextBlock Text='{OnFormFactor Default=""Hello World""}'/> |
|||
</UserControl>";
|
|||
|
|||
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml); |
|||
var textBlock = (TextBlock)userControl.Content!; |
|||
|
|||
Assert.Equal("Hello World", textBlock.Text); |
|||
} |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(false, true, "Im Mobile")] |
|||
[InlineData(true, false, "Im Desktop")] |
|||
[InlineData(false, false, "Default value")] |
|||
public void Should_Resolve_Expected_Value_Per_Platform(bool isDesktop, bool isMobile, string expectedResult) |
|||
{ |
|||
using (AvaloniaLocator.EnterScope()) |
|||
{ |
|||
AvaloniaLocator.CurrentMutable.Bind<IRuntimePlatform>() |
|||
.ToConstant(new TestRuntimePlatform(isDesktop, isMobile)); |
|||
|
|||
var xaml = @"
|
|||
<UserControl xmlns='https://github.com/avaloniaui'
|
|||
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
|
|||
<TextBlock Text='{OnFormFactor ""Default value"", |
|||
Mobile=""Im Mobile"", Desktop=""Im Desktop""}'/> |
|||
</UserControl>";
|
|||
|
|||
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml); |
|||
var textBlock = (TextBlock)userControl.Content!; |
|||
|
|||
Assert.Equal(expectedResult, textBlock.Text); |
|||
} |
|||
} |
|||
|
|||
private class TestRuntimePlatform : StandardRuntimePlatform |
|||
{ |
|||
private readonly bool _isDesktop; |
|||
private readonly bool _isMobile; |
|||
|
|||
public TestRuntimePlatform(bool isDesktop, bool isMobile) |
|||
{ |
|||
_isDesktop = isDesktop; |
|||
_isMobile = isMobile; |
|||
} |
|||
|
|||
public override RuntimePlatformInfo GetRuntimeInfo() |
|||
{ |
|||
return new RuntimePlatformInfo() { IsDesktop = _isDesktop, IsMobile = _isMobile }; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,75 @@ |
|||
using Avalonia.Controls; |
|||
using Avalonia.Platform; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions; |
|||
|
|||
public class OnPlatformExtensionTests : XamlTestBase |
|||
{ |
|||
[Fact] |
|||
public void Should_Resolve_Default_Value() |
|||
{ |
|||
using (AvaloniaLocator.EnterScope()) |
|||
{ |
|||
AvaloniaLocator.CurrentMutable.Bind<IRuntimePlatform>() |
|||
.ToConstant(new TestRuntimePlatform(OperatingSystemType.Unknown)); |
|||
|
|||
var xaml = @"
|
|||
<UserControl xmlns='https://github.com/avaloniaui'
|
|||
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
|
|||
<TextBlock Text='{OnPlatform Default=""Hello World""}'/> |
|||
</UserControl>";
|
|||
|
|||
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml); |
|||
var textBlock = (TextBlock)userControl.Content!; |
|||
|
|||
Assert.Equal("Hello World", textBlock.Text); |
|||
} |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(OperatingSystemType.WinNT, "Im Windows")] |
|||
[InlineData(OperatingSystemType.OSX, "Im macOS")] |
|||
[InlineData(OperatingSystemType.Linux, "Im Linux")] |
|||
[InlineData(OperatingSystemType.Android, "Im Android")] |
|||
[InlineData(OperatingSystemType.iOS, "Im iOS")] |
|||
[InlineData(OperatingSystemType.Browser, "Im Browser")] |
|||
[InlineData(OperatingSystemType.Unknown, "Default value")] |
|||
public void Should_Resolve_Expected_Value_Per_Platform(OperatingSystemType currentPlatform, string expectedResult) |
|||
{ |
|||
using (AvaloniaLocator.EnterScope()) |
|||
{ |
|||
AvaloniaLocator.CurrentMutable.Bind<IRuntimePlatform>() |
|||
.ToConstant(new TestRuntimePlatform(currentPlatform)); |
|||
|
|||
var xaml = @"
|
|||
<UserControl xmlns='https://github.com/avaloniaui'
|
|||
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
|
|||
<TextBlock Text='{OnPlatform ""Default value"", |
|||
Windows=""Im Windows"", macOS=""Im macOS"", |
|||
Linux=""Im Linux"", Android=""Im Android"", |
|||
iOS=""Im iOS"", Browser=""Im Browser""}'/> |
|||
</UserControl>";
|
|||
|
|||
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml); |
|||
var textBlock = (TextBlock)userControl.Content!; |
|||
|
|||
Assert.Equal(expectedResult, textBlock.Text); |
|||
} |
|||
} |
|||
|
|||
private class TestRuntimePlatform : StandardRuntimePlatform |
|||
{ |
|||
private readonly OperatingSystemType _operatingSystemType; |
|||
|
|||
public TestRuntimePlatform(OperatingSystemType operatingSystemType) |
|||
{ |
|||
_operatingSystemType = operatingSystemType; |
|||
} |
|||
|
|||
public override RuntimePlatformInfo GetRuntimeInfo() |
|||
{ |
|||
return new RuntimePlatformInfo() { OperatingSystem = _operatingSystemType }; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,605 @@ |
|||
using System; |
|||
using System.Reactive.Disposables; |
|||
using System.Runtime.InteropServices; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Markup.Xaml.MarkupExtensions; |
|||
using Avalonia.Media; |
|||
using Avalonia.Metadata; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions; |
|||
|
|||
public class OptionsMarkupExtensionTests : XamlTestBase |
|||
{ |
|||
public static Func<object, bool> RaisedOption; |
|||
public static int? ObjectsCreated; |
|||
|
|||
[Fact] |
|||
public void Resolve_Default_Value() |
|||
{ |
|||
using var _ = SetupTestGlobals("default"); |
|||
|
|||
var xaml = @"
|
|||
<TextBlock xmlns='https://github.com/avaloniaui'
|
|||
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests' |
|||
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
|
|||
Text='{local:OptionsMarkupExtension Default=""Hello World""}' />";
|
|||
|
|||
var textBlock = (TextBlock)AvaloniaRuntimeXamlLoader.Load(xaml); |
|||
|
|||
Assert.Equal("Hello World", textBlock.Text); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Resolve_Default_Value_From_Ctor() |
|||
{ |
|||
using var _ = SetupTestGlobals("default"); |
|||
|
|||
var xaml = @"
|
|||
<TextBlock xmlns='https://github.com/avaloniaui'
|
|||
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests' |
|||
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
|
|||
Text='{local:OptionsMarkupExtension ""Hello World"", OptionB=""Im Android""}' />";
|
|||
|
|||
var textBlock = (TextBlock)AvaloniaRuntimeXamlLoader.Load(xaml); |
|||
|
|||
Assert.Equal("Hello World", textBlock.Text); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Resolve_Implicit_Default_Value_Ref_Type() |
|||
{ |
|||
using var _ = SetupTestGlobals("default"); |
|||
|
|||
var xaml = @"
|
|||
<UserControl xmlns='https://github.com/avaloniaui'
|
|||
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests' |
|||
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
|
|||
Tag='{local:OptionsMarkupExtension OptionA=""Hello World"", x:DataType=x:String}' />";
|
|||
|
|||
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml); |
|||
|
|||
Assert.Equal(null, userControl.Tag); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Resolve_Implicit_Default_Value_Val_Type() |
|||
{ |
|||
using var _ = SetupTestGlobals("default"); |
|||
|
|||
var xaml = @"
|
|||
<UserControl xmlns='https://github.com/avaloniaui'
|
|||
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests' |
|||
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
|
|||
Height='{local:OptionsMarkupExtension OptionA=10}' />";
|
|||
|
|||
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml); |
|||
|
|||
Assert.Equal(0d, userControl.Height); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Resolve_Implicit_Default_Value_Avalonia_Val_Type() |
|||
{ |
|||
using var _ = SetupTestGlobals("default"); |
|||
|
|||
var xaml = @"
|
|||
<UserControl xmlns='https://github.com/avaloniaui'
|
|||
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests' |
|||
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
|
|||
Margin='{local:OptionsMarkupExtension OptionA=10}' />";
|
|||
|
|||
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml); |
|||
|
|||
Assert.Equal(new Thickness(0), userControl.Margin); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData("option 1", "Im Option 1")] |
|||
[InlineData("option 2", "Im Option 2")] |
|||
[InlineData("3", "Im Option 3")] |
|||
[InlineData("unknown", "Default value")] |
|||
public void Resolve_Expected_Value_Per_Option(object option, string expectedResult) |
|||
{ |
|||
using var _ = SetupTestGlobals(option); |
|||
|
|||
var xaml = @"
|
|||
<TextBlock xmlns='https://github.com/avaloniaui'
|
|||
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests' |
|||
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
|
|||
Text='{local:OptionsMarkupExtension ""Default value"", |
|||
OptionA=""Im Option 1"", OptionB=""Im Option 2"", |
|||
OptionNumber=""Im Option 3""}' />";
|
|||
|
|||
var textBlock = (TextBlock)AvaloniaRuntimeXamlLoader.Load(xaml); |
|||
|
|||
Assert.Equal(expectedResult, textBlock.Text); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData("option 1", "Im Option 1")] |
|||
[InlineData("option 2", "Im Option 2")] |
|||
[InlineData("3", "Im Option 3")] |
|||
[InlineData("unknown", "Default value")] |
|||
public void Resolve_Expected_Value_Per_Option_Create_Single_Object(object option, string expectedResult) |
|||
{ |
|||
using var _ = SetupTestGlobals(option); |
|||
|
|||
var xaml = @"
|
|||
<ContentControl xmlns='https://github.com/avaloniaui'
|
|||
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests' |
|||
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
|
|||
<ContentControl.Content> |
|||
<local:OptionsMarkupExtension> |
|||
<local:OptionsMarkupExtension.Default> |
|||
<local:ChildObject Name=""Default value"" /> |
|||
</local:OptionsMarkupExtension.Default> |
|||
<local:OptionsMarkupExtension.OptionA> |
|||
<local:ChildObject Name=""Im Option 1"" /> |
|||
</local:OptionsMarkupExtension.OptionA> |
|||
<local:OptionsMarkupExtension.OptionB> |
|||
<local:ChildObject Name=""Im Option 2"" /> |
|||
</local:OptionsMarkupExtension.OptionB> |
|||
<local:OptionsMarkupExtension.OptionNumber> |
|||
<local:ChildObject Name=""Im Option 3"" /> |
|||
</local:OptionsMarkupExtension.OptionNumber> |
|||
</local:OptionsMarkupExtension> |
|||
</ContentControl.Content> |
|||
</ContentControl>";
|
|||
|
|||
var contentControl = (ContentControl)AvaloniaRuntimeXamlLoader.Load(xaml); |
|||
var obj = Assert.IsType<ChildObject>(contentControl.Content); |
|||
|
|||
Assert.Equal(expectedResult, obj.Name); |
|||
Assert.Equal(1, ObjectsCreated); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Convert_Bcl_Type() |
|||
{ |
|||
using var _ = SetupTestGlobals("option 1"); |
|||
|
|||
var xaml = @"
|
|||
<Border xmlns='https://github.com/avaloniaui'
|
|||
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests' |
|||
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
|
|||
Height='{local:OptionsMarkupExtension OptionA=50.1}' />";
|
|||
|
|||
var border = (Border)AvaloniaRuntimeXamlLoader.Load(xaml); |
|||
|
|||
Assert.Equal(50.1, border.Height); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Convert_Avalonia_Type() |
|||
{ |
|||
using var _ = SetupTestGlobals("option 1"); |
|||
|
|||
var xaml = @"
|
|||
<Border xmlns='https://github.com/avaloniaui'
|
|||
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests' |
|||
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
|
|||
Padding='{local:OptionsMarkupExtension OptionA=""10, 8, 10, 8""}' />";
|
|||
|
|||
var border = (Border)AvaloniaRuntimeXamlLoader.Load(xaml); |
|||
|
|||
Assert.Equal(new Thickness(10, 8, 10, 8), border.Padding); |
|||
} |
|||
|
|||
[PlatformFact(TestPlatforms.Windows | TestPlatforms.Linux, "TypeArguments test is failing on macOS from SRE emit")] |
|||
public void Respect_Custom_TypeArgument() |
|||
{ |
|||
using var _ = SetupTestGlobals("option 1"); |
|||
|
|||
var xaml = @"
|
|||
<TextBlock xmlns='https://github.com/avaloniaui'
|
|||
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests' |
|||
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
|
|||
Tag='{local:OptionsMarkupExtensionWithGeneric Default=20, OptionA=""10, 10, 10, 10"", x:TypeArguments=Thickness}' />";
|
|||
|
|||
var textBlock = (TextBlock)AvaloniaRuntimeXamlLoader.Load(xaml); |
|||
|
|||
Assert.Equal(new Thickness(10, 10, 10, 10), textBlock.Tag); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Allow_Nester_Markup_Extensions() |
|||
{ |
|||
using var _ = SetupTestGlobals("option 1"); |
|||
|
|||
var xaml = @"
|
|||
<UserControl xmlns='https://github.com/avaloniaui'
|
|||
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests' |
|||
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
|
|||
<UserControl.Resources> |
|||
<SolidColorBrush x:Key='brush'>#ff506070</SolidColorBrush> |
|||
</UserControl.Resources> |
|||
<Border Background='{local:OptionsMarkupExtension OptionA={StaticResource brush}}'/> |
|||
</UserControl>";
|
|||
|
|||
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml); |
|||
var border = (Border)userControl.Content!; |
|||
|
|||
Assert.Equal(Color.Parse("#ff506070"), ((ISolidColorBrush)border.Background!).Color); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Allow_Nester_On_Platform_Markup_Extensions() |
|||
{ |
|||
using var _ = SetupTestGlobals("option 1"); |
|||
|
|||
var xaml = @"
|
|||
<Border xmlns='https://github.com/avaloniaui'
|
|||
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests' |
|||
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
|
|||
Margin='{local:OptionsMarkupExtension OptionA={local:OptionsMarkupExtensionMinimal OptionA=""10,10,10,10""}}' />";
|
|||
|
|||
var border = (Border)AvaloniaRuntimeXamlLoader.Load(xaml); |
|||
|
|||
Assert.Equal(new Thickness(10), border.Margin); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Support_Xml_Syntax() |
|||
{ |
|||
using var _ = SetupTestGlobals("option 1"); |
|||
|
|||
var xaml = @"
|
|||
<Border xmlns='https://github.com/avaloniaui'
|
|||
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests' |
|||
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
|
|||
<Border.Background> |
|||
<local:OptionsMarkupExtension> |
|||
<local:OptionsMarkupExtension.OptionA> |
|||
<SolidColorBrush Color='#ff506070' /> |
|||
</local:OptionsMarkupExtension.OptionA> |
|||
</local:OptionsMarkupExtension> |
|||
</Border.Background> |
|||
</Border>";
|
|||
|
|||
var border = (Border)AvaloniaRuntimeXamlLoader.Load(xaml); |
|||
|
|||
Assert.Equal(Color.Parse("#ff506070"), ((ISolidColorBrush)border.Background!).Color); |
|||
} |
|||
|
|||
[PlatformFact(TestPlatforms.Windows | TestPlatforms.Linux, "TypeArguments test is failing on macOS from SRE emit")] |
|||
public void Support_Xml_Syntax_With_Custom_TypeArguments() |
|||
{ |
|||
using var _ = SetupTestGlobals("option 1"); |
|||
|
|||
var xaml = @"
|
|||
<Border xmlns='https://github.com/avaloniaui'
|
|||
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests' |
|||
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
|
|||
<Border.Tag> |
|||
<local:OptionsMarkupExtensionWithGeneric x:TypeArguments='Thickness' OptionA='10, 10, 10, 10' Default='20' /> |
|||
</Border.Tag> |
|||
</Border>";
|
|||
|
|||
var border = (Border)AvaloniaRuntimeXamlLoader.Load(xaml); |
|||
|
|||
Assert.Equal(new Thickness(10, 10, 10, 10), border.Tag); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData("option 1", "#ff506070")] |
|||
[InlineData("3", "#000")] |
|||
public void Support_Special_On_Syntax(object option, string color) |
|||
{ |
|||
using var _ = SetupTestGlobals(option); |
|||
|
|||
var xaml = @"
|
|||
<Border xmlns='https://github.com/avaloniaui'
|
|||
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests' |
|||
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
|
|||
<Border.Background> |
|||
<local:OptionsMarkupExtension> |
|||
<On Options='OptionA, OptionB'> |
|||
<SolidColorBrush Color='#ff506070' /> |
|||
</On> |
|||
<On Options=' OptionNumber '> |
|||
<SolidColorBrush Color='#000' /> |
|||
</On> |
|||
</local:OptionsMarkupExtension> |
|||
</Border.Background> |
|||
</Border>";
|
|||
|
|||
var border = (Border)AvaloniaRuntimeXamlLoader.Load(xaml); |
|||
|
|||
Assert.Equal(Color.Parse(color), ((ISolidColorBrush)border.Background!).Color); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Support_Control_Inside_Xml_Syntax() |
|||
{ |
|||
using var _ = SetupTestGlobals("option 1"); |
|||
|
|||
var xaml = @"
|
|||
<UserControl xmlns='https://github.com/avaloniaui'
|
|||
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests' |
|||
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
|
|||
<local:OptionsMarkupExtension> |
|||
<local:OptionsMarkupExtension.OptionA> |
|||
<Button Content='Hello World' /> |
|||
</local:OptionsMarkupExtension.OptionA> |
|||
</local:OptionsMarkupExtension> |
|||
</UserControl>";
|
|||
|
|||
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml); |
|||
var button = (Button)userControl.Content!; |
|||
|
|||
Assert.Equal("Hello World", button.Content); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Support_Default_Control_Inside_Xml_Syntax() |
|||
{ |
|||
using var _ = SetupTestGlobals("unknown"); |
|||
|
|||
var xaml = @"
|
|||
<UserControl xmlns='https://github.com/avaloniaui'
|
|||
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests' |
|||
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
|
|||
<local:OptionsMarkupExtension> |
|||
<local:OptionsMarkupExtension.Default> |
|||
<Button Content='Hello World' /> |
|||
</local:OptionsMarkupExtension.Default> |
|||
</local:OptionsMarkupExtension> |
|||
</UserControl>";
|
|||
|
|||
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml); |
|||
var button = (Button)userControl.Content!; |
|||
|
|||
Assert.Equal("Hello World", button.Content); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Support_Complex_Property_Setters_Dictionary() |
|||
{ |
|||
using var _ = SetupTestGlobals("option 1"); |
|||
|
|||
var xaml = @"
|
|||
<ResourceDictionary xmlns='https://github.com/avaloniaui'
|
|||
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests' |
|||
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
|
|||
<Color x:Key='Color1'>Black</Color> |
|||
<local:OptionsMarkupExtension x:Key='MyKey'> |
|||
<local:OptionsMarkupExtension.OptionA> |
|||
<Button Content='Hello World' /> |
|||
</local:OptionsMarkupExtension.OptionA> |
|||
</local:OptionsMarkupExtension> |
|||
<Color x:Key='Color2'>White</Color> |
|||
</ResourceDictionary>";
|
|||
|
|||
var resourceDictionary = (ResourceDictionary)AvaloniaRuntimeXamlLoader.Load(xaml); |
|||
var button = Assert.IsType<Button>(resourceDictionary["MyKey"]); |
|||
Assert.Equal("Hello World", button.Content); |
|||
Assert.Equal(Colors.Black, resourceDictionary["Color1"]); |
|||
Assert.Equal(Colors.White, resourceDictionary["Color2"]); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Support_Complex_Property_Setters_List() |
|||
{ |
|||
using var _ = SetupTestGlobals("option 1"); |
|||
|
|||
var xaml = @"
|
|||
<Panel xmlns='https://github.com/avaloniaui'
|
|||
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests' |
|||
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
|
|||
<TextBlock /> |
|||
<local:OptionsMarkupExtension> |
|||
<local:OptionsMarkupExtension.OptionA> |
|||
<Button Content='Hello World' /> |
|||
</local:OptionsMarkupExtension.OptionA> |
|||
</local:OptionsMarkupExtension> |
|||
<TextBox /> |
|||
</Panel>";
|
|||
|
|||
var panel = (Panel)AvaloniaRuntimeXamlLoader.Load(xaml); |
|||
Assert.Equal(3, panel.Children.Count); |
|||
Assert.IsType<Button>(panel.Children[1]); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData("option 1", "foo")] |
|||
[InlineData("option 2", "bar")] |
|||
public void BindingExtension_Works_Inside_Of_OptionsMarkupExtension(string option, string expected) |
|||
{ |
|||
using var _ = SetupTestGlobals(option); |
|||
|
|||
var xaml = @"
|
|||
<UserControl xmlns='https://github.com/avaloniaui'
|
|||
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests' |
|||
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
|
|||
<UserControl.Resources> |
|||
<x:String x:Key='text'>foo</x:String> |
|||
</UserControl.Resources> |
|||
|
|||
<TextBlock Name='textBlock' Text='{local:OptionsMarkupExtension OptionA={CompiledBinding Source={StaticResource text}}, OptionB=bar}'/> |
|||
</UserControl>";
|
|||
|
|||
var window = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml); |
|||
var textBlock = window.FindControl<TextBlock>("textBlock"); |
|||
|
|||
Assert.Equal(expected, textBlock.Text); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Resolve_Expected_Value_With_Method_Without_ServiceProvider() |
|||
{ |
|||
using var _ = SetupTestGlobals(2); |
|||
|
|||
var xaml = @"
|
|||
<TextBlock xmlns='https://github.com/avaloniaui'
|
|||
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests' |
|||
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
|
|||
Text='{local:OptionsMarkupExtensionNoServiceProvider OptionB=""Im Option 2""}' />";
|
|||
|
|||
var textBlock = (TextBlock)AvaloniaRuntimeXamlLoader.Load(xaml); |
|||
|
|||
Assert.Equal("Im Option 2", textBlock.Text); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Resolve_Expected_Value_Minimal_Extension() |
|||
{ |
|||
var xaml = @"
|
|||
<TextBlock xmlns='https://github.com/avaloniaui'
|
|||
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests' |
|||
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
|
|||
IsVisible='{local:OptionsMarkupExtensionMinimal OptionA=True}' />";
|
|||
|
|||
var textBlock = (TextBlock)AvaloniaRuntimeXamlLoader.Load(xaml); |
|||
|
|||
Assert.True(textBlock.IsSet(Visual.IsVisibleProperty)); |
|||
Assert.True(textBlock.IsVisible); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Resolve_Expected_Value_Extension_With_Property() |
|||
{ |
|||
var xaml = @"
|
|||
<TextBlock xmlns='https://github.com/avaloniaui'
|
|||
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests' |
|||
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
|
|||
IsVisible='{local:OptionsMarkupExtensionWithProperty OptionA=True, Property=5}' />";
|
|||
|
|||
var textBlock = (TextBlock)AvaloniaRuntimeXamlLoader.Load(xaml); |
|||
|
|||
Assert.True(textBlock.IsSet(Visual.IsVisibleProperty)); |
|||
Assert.True(textBlock.IsVisible); |
|||
} |
|||
|
|||
private static IDisposable SetupTestGlobals(object acceptedOption) |
|||
{ |
|||
RaisedOption = o => o.Equals(acceptedOption); |
|||
ObjectsCreated = 0; |
|||
return Disposable.Create(() => |
|||
{ |
|||
RaisedOption = null; |
|||
ObjectsCreated = null; |
|||
}); |
|||
} |
|||
} |
|||
|
|||
public class OptionsMarkupExtension : OptionsMarkupExtensionBase<object, On> |
|||
{ |
|||
public OptionsMarkupExtension() |
|||
{ |
|||
|
|||
} |
|||
|
|||
public OptionsMarkupExtension(object defaultValue) |
|||
{ |
|||
Default = defaultValue; |
|||
} |
|||
} |
|||
|
|||
public class OptionsMarkupExtension<TReturn> : OptionsMarkupExtensionBase<TReturn, On<TReturn>> |
|||
{ |
|||
public OptionsMarkupExtension() |
|||
{ |
|||
|
|||
} |
|||
|
|||
public OptionsMarkupExtension(TReturn defaultValue) |
|||
{ |
|||
Default = defaultValue; |
|||
} |
|||
} |
|||
|
|||
public class OptionsMarkupExtensionBase<TReturn, TOn> : IAddChild<TOn> |
|||
where TOn : On<TReturn> |
|||
{ |
|||
[MarkupExtensionOption("option 1")] |
|||
public TReturn OptionA { get; set; } |
|||
|
|||
[MarkupExtensionOption("option 2")] |
|||
public TReturn OptionB { get; set; } |
|||
|
|||
[MarkupExtensionOption(3)] |
|||
public TReturn OptionNumber { get; set; } |
|||
|
|||
[Content] |
|||
[MarkupExtensionDefaultOption] |
|||
public TReturn Default { get; set; } |
|||
|
|||
public bool ShouldProvideOption(IServiceProvider serviceProvider, string option) |
|||
{ |
|||
return OptionsMarkupExtensionTests.RaisedOption(option); |
|||
} |
|||
|
|||
public TReturn ProvideValue(IServiceProvider serviceProvider) { throw null; } |
|||
|
|||
public void AddChild(TOn child) {} |
|||
} |
|||
|
|||
public class OptionsMarkupExtensionNoServiceProvider |
|||
{ |
|||
[MarkupExtensionOption(1)] |
|||
public object OptionA { get; set; } |
|||
|
|||
[MarkupExtensionOption(2)] |
|||
public object OptionB { get; set; } |
|||
|
|||
[Content] |
|||
[MarkupExtensionDefaultOption] |
|||
public object Default { get; set; } |
|||
|
|||
public static bool ShouldProvideOption(int option) |
|||
{ |
|||
return OptionsMarkupExtensionTests.RaisedOption(option); |
|||
} |
|||
|
|||
public object ProvideValue(IServiceProvider serviceProvider) { throw null; } |
|||
} |
|||
|
|||
public class OptionsMarkupExtensionMinimal |
|||
{ |
|||
[MarkupExtensionOption(11.0)] |
|||
public bool OptionA { get; set; } |
|||
|
|||
public static bool ShouldProvideOption(double option) => option > 0; |
|||
|
|||
public object ProvideValue() { throw null; } |
|||
} |
|||
|
|||
public class OptionsMarkupExtensionWithProperty |
|||
{ |
|||
[MarkupExtensionOption(5)] |
|||
public bool OptionA { get; set; } |
|||
|
|||
public int Property { get; set; } |
|||
|
|||
public bool ShouldProvideOption(int option) => option == Property; |
|||
|
|||
public object ProvideValue() { throw null; } |
|||
} |
|||
|
|||
public class OptionsMarkupExtensionWithGeneric<TResult> |
|||
{ |
|||
[MarkupExtensionOption("option 1")] |
|||
public TResult OptionA { get; set; } |
|||
|
|||
[Content] |
|||
[MarkupExtensionDefaultOption] |
|||
public TResult Default { get; set; } |
|||
|
|||
public bool ShouldProvideOption(string option) |
|||
{ |
|||
return OptionsMarkupExtensionTests.RaisedOption(option); |
|||
} |
|||
|
|||
public TResult ProvideValue() { throw null; } |
|||
} |
|||
|
|||
public class ChildObject |
|||
{ |
|||
public string Name { get; set; } |
|||
|
|||
public ChildObject() |
|||
{ |
|||
OptionsMarkupExtensionTests.ObjectsCreated++; |
|||
} |
|||
} |
|||
Loading…
Reference in new issue