diff --git a/samples/ControlCatalog.Blazor.Web/Properties/launchSettings.json b/samples/ControlCatalog.Blazor.Web/Properties/launchSettings.json index e4da60f7ca..ad2b1e30f6 100644 --- a/samples/ControlCatalog.Blazor.Web/Properties/launchSettings.json +++ b/samples/ControlCatalog.Blazor.Web/Properties/launchSettings.json @@ -8,14 +8,6 @@ } }, "profiles": { - "ControlCatalog.Web - IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, "ControlCatalog.Web": { "commandName": "Project", "dotnetRunMessages": "true", diff --git a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj index 6c17e9ac43..e4c83dca49 100644 --- a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj +++ b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj @@ -5,7 +5,7 @@ net6.0 true true - 6.0.9 + 6.0.8 diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index ec198c6bba..0d7a1ea4d7 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -12,8 +12,8 @@ - - + + @@ -118,6 +118,9 @@ + + + @@ -130,7 +133,7 @@ - + diff --git a/samples/ControlCatalog/Pages/PlatformInfoPage.xaml b/samples/ControlCatalog/Pages/PlatformInfoPage.xaml new file mode 100644 index 0000000000..a60f54d10e --- /dev/null +++ b/samples/ControlCatalog/Pages/PlatformInfoPage.xaml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/PlatformInfoPage.xaml.cs b/samples/ControlCatalog/Pages/PlatformInfoPage.xaml.cs new file mode 100644 index 0000000000..1f37451782 --- /dev/null +++ b/samples/ControlCatalog/Pages/PlatformInfoPage.xaml.cs @@ -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); + } + } +} diff --git a/samples/ControlCatalog/ViewModels/PlatformInformationViewModel.cs b/samples/ControlCatalog/ViewModels/PlatformInformationViewModel.cs new file mode 100644 index 0000000000..e4f6c3ac73 --- /dev/null +++ b/samples/ControlCatalog/ViewModels/PlatformInformationViewModel.cs @@ -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()?.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; } +} diff --git a/src/Avalonia.Base/Metadata/MarkupExtensionOption.cs b/src/Avalonia.Base/Metadata/MarkupExtensionOption.cs new file mode 100644 index 0000000000..4009720bcb --- /dev/null +++ b/src/Avalonia.Base/Metadata/MarkupExtensionOption.cs @@ -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 +{ + +} diff --git a/src/Avalonia.Base/Platform/IRuntimePlatform.cs b/src/Avalonia.Base/Platform/IRuntimePlatform.cs index 8ab04f5995..3f8983479f 100644 --- a/src/Avalonia.Base/Platform/IRuntimePlatform.cs +++ b/src/Avalonia.Base/Platform/IRuntimePlatform.cs @@ -17,13 +17,16 @@ namespace Avalonia.Platform IntPtr Address { get; } int Size { get; } bool IsDisposed { get; } - + } [Unstable] public struct RuntimePlatformInfo { public OperatingSystemType OperatingSystem { get; set; } + + public FormFactorType FormFactor => IsDesktop ? FormFactorType.Desktop : + IsMobile ? FormFactorType.Mobile : FormFactorType.Unknown; public bool IsDesktop { get; set; } public bool IsMobile { get; set; } public bool IsBrowser { get; set; } @@ -44,4 +47,12 @@ namespace Avalonia.Platform iOS, Browser } + + [Unstable] + public enum FormFactorType + { + Unknown, + Desktop, + Mobile + } } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs index 1e2a77c34d..1d4794f02a 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs @@ -198,6 +198,7 @@ namespace Avalonia.Markup.Xaml.XamlIl compiler.ParseAndCompile(xaml, uri?.ToString(), null, _sreTypeSystem.CreateTypeBuilder(tb), overrideType); var created = tb.CreateTypeInfo(); clrPropertyBuilder.CreateTypeInfo(); + indexerClosureType.CreateTypeInfo(); trampolineBuilder.CreateTypeInfo(); return LoadOrPopulate(created, rootInstance); diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs index 4572f72e3d..1692238d06 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -22,10 +22,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions private AvaloniaXamlIlCompiler(TransformerConfiguration configuration, XamlLanguageEmitMappings emitMappings) : base(configuration, emitMappings, true) { - void InsertAfter(params IXamlAstTransformer[] t) + void InsertAfter(params IXamlAstTransformer[] t) => Transformers.InsertRange(Transformers.FindIndex(x => x is T) + 1, t); - void InsertBefore(params IXamlAstTransformer[] t) + void InsertBefore(params IXamlAstTransformer[] t) => Transformers.InsertRange(Transformers.FindIndex(x => x is T), t); @@ -35,7 +35,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions Transformers.Insert(1, new IgnoredDirectivesTransformer()); Transformers.Insert(2, _designTransformer = new AvaloniaXamlIlDesignPropertiesTransformer()); Transformers.Insert(3, _bindingTransformer = new AvaloniaBindingExtensionTransformer()); - + // Targeted InsertBefore( new AvaloniaXamlIlResolveClassesPropertiesTransformer(), @@ -58,6 +58,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions new AvaloniaXamlIlTransitionsTypeMetadataTransformer(), new AvaloniaXamlIlResolveByNameMarkupExtensionReplacer() ); + InsertBefore( + new AvaloniaXamlIlOptionMarkupExtensionTransformer()); InsertAfter( new XDataTypeTransformer()); @@ -89,14 +91,14 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions _contextType = CreateContextType(contextTypeBuilder); } - + public AvaloniaXamlIlCompiler(TransformerConfiguration configuration, XamlLanguageEmitMappings emitMappings, IXamlType contextType) : this(configuration, emitMappings) { _contextType = contextType; } - + public const string PopulateName = "__AvaloniaXamlIlPopulate"; public const string BuildName = "__AvaloniaXamlIlBuild"; @@ -118,7 +120,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions { {XamlNamespaces.Blend2008, XamlNamespaces.Blend2008} }); - + var rootObject = (XamlAstObjectNode)parsed.Root; var classDirective = rootObject.Children @@ -133,8 +135,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions false) : TypeReferenceResolver.ResolveType(CreateTransformationContext(parsed, true), (XamlAstXmlTypeReference)rootObject.Type, true); - - + + if (overrideRootType != null) { if (!rootType.Type.IsAssignableFrom(overrideRootType)) @@ -147,7 +149,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions Transform(parsed); Compile(parsed, tb, _contextType, PopulateName, BuildName, "__AvaloniaXamlIlNsInfo", baseUri, fileSource); - + } public void OverrideRootType(XamlDocument doc, IXamlAstTypeReference newType) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs index c8fced515d..b4999136a4 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs @@ -185,6 +185,15 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions public static bool CustomValueConverter(AstTransformationContext context, IXamlAstValueNode node, IXamlType type, out IXamlAstValueNode result) { + if (node is AvaloniaXamlIlOptionMarkupExtensionTransformer.OptionsMarkupExtensionNode optionsNode) + { + if (optionsNode.ConvertToReturnType(context, type, out var newOptionsNode)) + { + result = newOptionsNode; + return true; + } + } + if (!(node is XamlAstTextNode textNode)) { result = null; diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs index 8d9439a748..cc833858fb 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs @@ -48,10 +48,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions try { var thickness = Thickness.Parse(text); - + result = new AvaloniaXamlIlVectorLikeConstantAstNode(node, types, types.Thickness, types.ThicknessFullConstructor, new[] { thickness.Left, thickness.Top, thickness.Right, thickness.Bottom }); - + return true; } catch @@ -65,10 +65,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions try { var point = Point.Parse(text); - + result = new AvaloniaXamlIlVectorLikeConstantAstNode(node, types, types.Point, types.PointFullConstructor, new[] { point.X, point.Y }); - + return true; } catch @@ -76,16 +76,16 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions throw new XamlX.XamlLoadException($"Unable to parse \"{text}\" as a point", node); } } - + if (type.Equals(types.Vector)) { try { var vector = Vector.Parse(text); - + result = new AvaloniaXamlIlVectorLikeConstantAstNode(node, types, types.Vector, types.VectorFullConstructor, new[] { vector.X, vector.Y }); - + return true; } catch @@ -93,16 +93,16 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions throw new XamlX.XamlLoadException($"Unable to parse \"{text}\" as a vector", node); } } - + if (type.Equals(types.Size)) { try { var size = Size.Parse(text); - + result = new AvaloniaXamlIlVectorLikeConstantAstNode(node, types, types.Size, types.SizeFullConstructor, new[] { size.Width, size.Height }); - + return true; } catch @@ -110,16 +110,16 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions throw new XamlX.XamlLoadException($"Unable to parse \"{text}\" as a size", node); } } - + if (type.Equals(types.Matrix)) { try { var matrix = Matrix.Parse(text); - + result = new AvaloniaXamlIlVectorLikeConstantAstNode(node, types, types.Matrix, types.MatrixFullConstructor, new[] { matrix.M11, matrix.M12, matrix.M21, matrix.M22, matrix.M31, matrix.M32 }); - + return true; } catch @@ -127,16 +127,16 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions throw new XamlX.XamlLoadException($"Unable to parse \"{text}\" as a matrix", node); } } - + if (type.Equals(types.CornerRadius)) { try { var cornerRadius = CornerRadius.Parse(text); - + result = new AvaloniaXamlIlVectorLikeConstantAstNode(node, types, types.CornerRadius, types.CornerRadiusFullConstructor, new[] { cornerRadius.TopLeft, cornerRadius.TopRight, cornerRadius.BottomRight, cornerRadius.BottomLeft }); - + return true; } catch @@ -144,7 +144,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions throw new XamlX.XamlLoadException($"Unable to parse \"{text}\" as a corner radius", node); } } - + if (type.Equals(types.Color)) { if (!Color.TryParse(text, out Color color)) @@ -165,9 +165,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions try { var relativePoint = RelativePoint.Parse(text); - + var relativePointTypeRef = new XamlAstClrTypeReference(node, types.RelativePoint, false); - + result = new XamlAstNewClrObjectNode(node, relativePointTypeRef, types.RelativePointFullConstructor, new List { new XamlConstantNode(node, types.XamlIlTypes.Double, relativePoint.Point.X), @@ -188,9 +188,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions try { var gridLength = GridLength.Parse(text); - + result = new AvaloniaXamlIlGridLengthAstNode(node, types, gridLength); - + return true; } catch @@ -201,12 +201,12 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions if (type.Equals(types.Cursor)) { - if (TypeSystemHelpers.TryGetEnumValueNode(types.StandardCursorType, text, node, out var enumConstantNode)) + if (TypeSystemHelpers.TryGetEnumValueNode(types.StandardCursorType, text, node, false, out var enumConstantNode)) { var cursorTypeRef = new XamlAstClrTypeReference(node, types.Cursor, false); result = new XamlAstNewClrObjectNode(node, cursorTypeRef, types.CursorTypeConstructor, new List { enumConstantNode }); - + return true; } } @@ -296,7 +296,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions } private static bool ConvertDefinitionList( - IXamlAstValueNode node, + IXamlAstValueNode node, string text, AvaloniaXamlIlWellKnownTypes types, IXamlType listType, diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlOptionMarkupExtensionTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlOptionMarkupExtensionTransformer.cs new file mode 100644 index 0000000000..5004e594f7 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlOptionMarkupExtensionTransformer.cs @@ -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(); + + 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().ToArray()) + { + if (!extProp.Values.Any()) + { + continue; + } + + var shouldRemoveProp = false; + var onObjs = extProp.Values.OfType() + .Where(o => o.Type.GetClrType() == context.GetAvaloniaTypes().OnExtensionType).ToArray(); + if (onObjs.Any()) + { + shouldRemoveProp = true; + foreach (var onObj in onObjs) + { + var optionsPropNode = onObj.Children.OfType() + .SingleOrDefault(v => v.Property.GetClrProperty().Name == "Options") + ?.Values.Single(); + var options = (optionsPropNode as XamlAstTextNode)?.Text?.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries) + ?? Array.Empty(); + if (options.Length == 0) + { + throw new XamlParseException("On.Options string must be set", onObj); + } + + var content = onObj.Children.OfType() + .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 valueNodes, + IReadOnlyCollection 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 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 + { + public OptionsMarkupExtensionMethod( + OptionsMarkupExtensionNodesContainer extensionNodeContainer, + IXamlType declaringType, + IXamlType contextParameter) + { + ExtensionNodeContainer = extensionNodeContainer; + DeclaringType = declaringType; + Parameters = extensionNodeContainer.Branches.Any(c => c.HasContext) ? + new[] { contextParameter } : + Array.Empty(); + } + + public OptionsMarkupExtensionNodesContainer ExtensionNodeContainer { get; } + + public string Name => "ProvideValue"; + public bool IsPublic => true; + public bool IsStatic => false; + public IXamlType ReturnType => ExtensionNodeContainer.GetReturnType(); + public IReadOnlyList Parameters { get; } + public IXamlType DeclaringType { get; } + public IXamlMethod MakeGenericMethod(IReadOnlyList typeArguments) => throw new NotImplementedException(); + public IReadOnlyList CustomAttributes => Array.Empty(); + + public void EmitCall(XamlEmitContext 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); + } +} + diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index ee9ae1a0e8..4ebce0e7ba 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -9,6 +9,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { class AvaloniaXamlIlWellKnownTypes { + public IXamlType RuntimeHelpers { get; } public IXamlType AvaloniaObject { get; } public IXamlType IAvaloniaObject { get; } public IXamlType BindingPriority { get; } @@ -29,6 +30,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlType AssignBindingAttribute { get; } public IXamlType DependsOnAttribute { get; } public IXamlType DataTypeAttribute { get; } + public IXamlType MarkupExtensionOptionAttribute { get; } + public IXamlType MarkupExtensionDefaultOptionAttribute { get; } + public IXamlType OnExtensionType { get; } public IXamlType UnsetValueType { get; } public IXamlType StyledElement { get; } public IXamlType IStyledElement { get; } @@ -107,6 +111,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public AvaloniaXamlIlWellKnownTypes(TransformerConfiguration cfg) { + RuntimeHelpers = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.XamlIl.Runtime.XamlIlRuntimeHelpers"); + XamlIlTypes = cfg.WellKnownTypes; AvaloniaObject = cfg.TypeSystem.GetType("Avalonia.AvaloniaObject"); IAvaloniaObject = cfg.TypeSystem.GetType("Avalonia.IAvaloniaObject"); @@ -128,6 +134,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers AssignBindingAttribute = cfg.TypeSystem.GetType("Avalonia.Data.AssignBindingAttribute"); DependsOnAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.DependsOnAttribute"); DataTypeAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.DataTypeAttribute"); + MarkupExtensionOptionAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.MarkupExtensionOptionAttribute"); + MarkupExtensionDefaultOptionAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.MarkupExtensionDefaultOptionAttribute"); + OnExtensionType = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.On"); AvaloniaObjectBindMethod = AvaloniaObjectExtensions.FindMethod("Bind", IDisposable, false, IAvaloniaObject, AvaloniaProperty, IBinding, cfg.WellKnownTypes.Object); @@ -245,7 +254,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers ctx.SetItem(rv = new AvaloniaXamlIlWellKnownTypes(ctx.Configuration)); return rv; } - + public static AvaloniaXamlIlWellKnownTypes GetAvaloniaTypes(this XamlEmitContext ctx) { if (ctx.TryGetItem(out var rv)) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github b/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github index c1c0594ec2..880ba5742e 160000 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github @@ -1 +1 @@ -Subproject commit c1c0594ec2c35b08988183b1a5b3e34dfa19179d +Subproject commit 880ba5742e52b67afda048c4023cf7e3c3c16a46 diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index 75746273c2..f5bf14c2a4 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -29,6 +29,9 @@ + + + diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/On.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/On.cs new file mode 100644 index 0000000000..0752ab43b5 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/On.cs @@ -0,0 +1,15 @@ +#nullable enable +using System.Collections.Generic; +using Avalonia.Metadata; + +namespace Avalonia.Markup.Xaml.MarkupExtensions; + +public class On +{ + public IReadOnlyList Options { get; } = new List(); + + [Content] + public TReturn? Content { get; set; } +} + +public class On : On {} diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/OnFormFactorExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/OnFormFactorExtension.cs new file mode 100644 index 0000000000..51e09eef71 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/OnFormFactorExtension.cs @@ -0,0 +1,60 @@ +#nullable enable +using System; + +using Avalonia.Metadata; +using Avalonia.Platform; + +namespace Avalonia.Markup.Xaml.MarkupExtensions; + +public class OnFormFactorExtension : OnFormFactorExtensionBase +{ + public OnFormFactorExtension() + { + + } + + public OnFormFactorExtension(object defaultValue) + { + Default = defaultValue; + } + + public static bool ShouldProvideOption(IServiceProvider serviceProvider, FormFactorType option) + { + return serviceProvider.GetService().GetRuntimeInfo().FormFactor == option; + } +} + +public class OnFormFactorExtension : OnFormFactorExtensionBase> +{ + public OnFormFactorExtension() + { + + } + + public OnFormFactorExtension(TReturn defaultValue) + { + Default = defaultValue; + } + + public static bool ShouldProvideOption(IServiceProvider serviceProvider, FormFactorType option) + { + return serviceProvider.GetService().GetRuntimeInfo().FormFactor == option; + } +} + +public abstract class OnFormFactorExtensionBase : IAddChild + where TOn : On +{ + [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.AddChild(TOn child) {} +} diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/OnPlatformExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/OnPlatformExtension.cs new file mode 100644 index 0000000000..1ac7a522f1 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/OnPlatformExtension.cs @@ -0,0 +1,73 @@ +#nullable enable +using System; +using Avalonia.Metadata; +using Avalonia.Platform; + +namespace Avalonia.Markup.Xaml.MarkupExtensions; + +public class OnPlatformExtension : OnPlatformExtensionBase +{ + public OnPlatformExtension() + { + + } + + public OnPlatformExtension(object defaultValue) + { + Default = defaultValue; + } + + public static bool ShouldProvideOption(IServiceProvider serviceProvider, OperatingSystemType option) + { + return serviceProvider.GetService().GetRuntimeInfo().OperatingSystem == option; + } +} + +public class OnPlatformExtension : OnPlatformExtensionBase> +{ + public OnPlatformExtension() + { + + } + + public OnPlatformExtension(TReturn defaultValue) + { + Default = defaultValue; + } + + public static bool ShouldProvideOption(IServiceProvider serviceProvider, OperatingSystemType option) + { + return serviceProvider.GetService().GetRuntimeInfo().OperatingSystem == option; + } +} + +public abstract class OnPlatformExtensionBase : IAddChild + where TOn : On +{ + [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.AddChild(TOn child) {} +} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs index 168b9835dd..6682532455 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs @@ -1,11 +1,12 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Reflection; using Avalonia.Controls; using Avalonia.Controls.Templates; using Avalonia.Data; +using Avalonia.Platform; + // ReSharper disable UnusedMember.Global // ReSharper disable UnusedParameter.Global @@ -18,7 +19,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime { return DeferredTransformationFactoryV2(builder, provider); } - + public static Func DeferredTransformationFactoryV2(Func builder, IServiceProvider provider) { @@ -31,7 +32,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime var scope = parentScope != null ? new ChildNameScope(parentScope) : (INameScope)new NameScope(); var obj = builder(new DeferredParentServiceProvider(sp, resourceNodes, rootObject, scope)); scope.Complete(); - + if(typeof(T) == typeof(IControl)) return new ControlTemplateResult((IControl)obj, scope); @@ -107,9 +108,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime throw new ArgumentException("Don't know what to do with " + value.GetType()); } - public static IServiceProvider CreateInnerServiceProviderV1(IServiceProvider compiled) + public static IServiceProvider CreateInnerServiceProviderV1(IServiceProvider compiled) => new InnerServiceProvider(compiled); - + class InnerServiceProvider : IServiceProvider { private readonly IServiceProvider _compiledProvider; @@ -136,7 +137,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime { _nsInfo = nsInfo; } - + public Type Resolve(string qualifiedTypeName) { var sp = qualifiedTypeName.Split(new[] {':'}, 2); @@ -166,22 +167,27 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime return new RootServiceProvider(new NameScope()); } #line default - + class RootServiceProvider : IServiceProvider, IAvaloniaXamlIlParentStackProvider { private readonly INameScope _nameScope; + private readonly IRuntimePlatform _runtimePlatform; public RootServiceProvider(INameScope nameScope) { _nameScope = nameScope; + _runtimePlatform = AvaloniaLocator.Current.GetService(); } - + public object GetService(Type serviceType) { if (serviceType == typeof(INameScope)) return _nameScope; if (serviceType == typeof(IAvaloniaXamlIlParentStackProvider)) return this; + if (serviceType == typeof(IRuntimePlatform)) + return _runtimePlatform ?? throw new KeyNotFoundException($"{nameof(IRuntimePlatform)} was not registered"); + return null; } diff --git a/src/Web/Avalonia.Web/BrowserRuntimePlatform.cs b/src/Web/Avalonia.Web/BrowserRuntimePlatform.cs new file mode 100644 index 0000000000..ebcd3a9921 --- /dev/null +++ b/src/Web/Avalonia.Web/BrowserRuntimePlatform.cs @@ -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 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; +} diff --git a/src/Web/Avalonia.Web/BrowserSingleViewLifetime.cs b/src/Web/Avalonia.Web/BrowserSingleViewLifetime.cs index 00ed961fbe..0dcc474f76 100644 --- a/src/Web/Avalonia.Web/BrowserSingleViewLifetime.cs +++ b/src/Web/Avalonia.Web/BrowserSingleViewLifetime.cs @@ -3,6 +3,7 @@ using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Web.Skia; using System.Runtime.Versioning; +using Avalonia.Platform; namespace Avalonia.Web; @@ -23,7 +24,6 @@ public class BrowserPlatformOptions public Func FrameworkAssetPathResolver { get; set; } = new(fileName => $"./{fileName}"); } - [SupportedOSPlatform("browser")] public static class WebAppBuilder { diff --git a/src/Web/Avalonia.Web/Interop/AvaloniaModule.cs b/src/Web/Avalonia.Web/Interop/AvaloniaModule.cs index 176b8d60fc..0e54deb515 100644 --- a/src/Web/Avalonia.Web/Interop/AvaloniaModule.cs +++ b/src/Web/Avalonia.Web/Interop/AvaloniaModule.cs @@ -3,7 +3,7 @@ using System.Threading.Tasks; namespace Avalonia.Web.Interop; -internal static class AvaloniaModule +internal static partial class AvaloniaModule { public const string MainModuleName = "avalonia"; public const string StorageModuleName = "storage"; @@ -19,4 +19,7 @@ internal static class AvaloniaModule var options = AvaloniaLocator.Current.GetService() ?? new BrowserPlatformOptions(); return JSHost.ImportAsync(StorageModuleName, options.FrameworkAssetPathResolver("storage.js")); } + + [JSImport("Caniuse.isMobile", AvaloniaModule.MainModuleName)] + public static partial bool IsMobile(); } diff --git a/src/Web/Avalonia.Web/WindowingPlatform.cs b/src/Web/Avalonia.Web/WindowingPlatform.cs index c399c22c61..828964afa7 100644 --- a/src/Web/Avalonia.Web/WindowingPlatform.cs +++ b/src/Web/Avalonia.Web/WindowingPlatform.cs @@ -31,8 +31,10 @@ namespace Avalonia.Web public static void Register() { var instance = new BrowserWindowingPlatform(); + s_keyboard = new KeyboardDevice(); AvaloniaLocator.CurrentMutable + .Bind().ToSingleton() .Bind().ToSingleton() .Bind().ToSingleton() .Bind().ToConstant(s_keyboard) @@ -64,11 +66,11 @@ namespace Avalonia.Web { if (_signaled) return; - + _signaled = true; - + IDisposable? disp = null; - + disp = GetRuntimePlatform() .StartSystemTimer(TimeSpan.FromMilliseconds(1), () => diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/caniuse.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/caniuse.ts index 6dedcb724f..e019f92113 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia/caniuse.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/caniuse.ts @@ -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)); } } diff --git a/tests/Avalonia.IntegrationTests.Appium/PlatformFactAttribute.cs b/tests/Avalonia.IntegrationTests.Appium/PlatformFactAttribute.cs index 53ae5d924f..3acf9fd5e2 100644 --- a/tests/Avalonia.IntegrationTests.Appium/PlatformFactAttribute.cs +++ b/tests/Avalonia.IntegrationTests.Appium/PlatformFactAttribute.cs @@ -1,27 +1,34 @@ +#nullable enable using System; -using System.Linq; using System.Runtime.InteropServices; using Xunit; -namespace Avalonia.IntegrationTests.Appium +namespace Avalonia { [Flags] internal enum TestPlatforms { Windows = 0x01, MacOS = 0x02, - All = Windows | MacOS, + Linux = 0x04, + All = Windows | MacOS | Linux, } - + internal class PlatformFactAttribute : FactAttribute { - public PlatformFactAttribute(TestPlatforms platforms = TestPlatforms.All) => Platforms = platforms; - + private readonly string? _reason; + + public PlatformFactAttribute(TestPlatforms platforms, string? reason = null) + { + _reason = reason; + Platforms = platforms; + } + public TestPlatforms Platforms { get; } - + public override string? Skip { - get => IsSupported() ? null : $"Ignored on {RuntimeInformation.OSDescription}"; + get => IsSupported() ? null : $"Ignored on {RuntimeInformation.OSDescription}" + (_reason is not null ? $" reason: \"{_reason}\"" : ""); set => throw new NotSupportedException(); } @@ -31,6 +38,8 @@ namespace Avalonia.IntegrationTests.Appium return Platforms.HasAnyFlag(TestPlatforms.Windows); if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) return Platforms.HasAnyFlag(TestPlatforms.MacOS); + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + return Platforms.HasAnyFlag(TestPlatforms.Linux); return false; } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj index f562529cb8..7ff19e1049 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj @@ -31,5 +31,10 @@ + + + PlatformFactAttribute.cs + + diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/OnFormFactorExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/OnFormFactorExtensionTests.cs new file mode 100644 index 0000000000..783e647738 --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/OnFormFactorExtensionTests.cs @@ -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() + .ToConstant(new TestRuntimePlatform(false, false)); + + var xaml = @" + + +"; + + 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() + .ToConstant(new TestRuntimePlatform(isDesktop, isMobile)); + + var xaml = @" + + +"; + + 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 }; + } + } +} diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/OnPlatformExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/OnPlatformExtensionTests.cs new file mode 100644 index 0000000000..1d37378010 --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/OnPlatformExtensionTests.cs @@ -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() + .ToConstant(new TestRuntimePlatform(OperatingSystemType.Unknown)); + + var xaml = @" + + +"; + + 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() + .ToConstant(new TestRuntimePlatform(currentPlatform)); + + var xaml = @" + + +"; + + 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 }; + } + } +} diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/OptionsMarkupExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/OptionsMarkupExtensionTests.cs new file mode 100644 index 0000000000..2d1f961743 --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/OptionsMarkupExtensionTests.cs @@ -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 RaisedOption; + public static int? ObjectsCreated; + + [Fact] + public void Resolve_Default_Value() + { + using var _ = SetupTestGlobals("default"); + + var xaml = @" +"; + + 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 = @" +"; + + 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 = @" +"; + + 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 = @" +"; + + 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 = @" +"; + + 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 = @" +"; + + 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 = @" + + + + + + + + + + + + + + + + + +"; + + var contentControl = (ContentControl)AvaloniaRuntimeXamlLoader.Load(xaml); + var obj = Assert.IsType(contentControl.Content); + + Assert.Equal(expectedResult, obj.Name); + Assert.Equal(1, ObjectsCreated); + } + + [Fact] + public void Convert_Bcl_Type() + { + using var _ = SetupTestGlobals("option 1"); + + var xaml = @" +"; + + 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 = @" +"; + + 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 = @" +"; + + 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 = @" + + + #ff506070 + + +"; + + 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 = @" +"; + + 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 = @" + + + + + + + + +"; + + 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 = @" + + + + +"; + + 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 = @" + + + + + + + + + + + +"; + + 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 = @" + + + +