From c222070d84626495db9bca870afb113ac67ec35f Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 14 Jun 2019 11:31:08 +0300 Subject: [PATCH 001/693] Added IPropertyInfo and use it in IProvideValueTarget when possible --- src/Avalonia.Base/AvaloniaProperty.cs | 8 +- .../Data/Core/ClrPropertyInfo.cs | 70 +++++++++ src/Avalonia.Base/Data/Core/IPropertyInfo.cs | 11 ++ .../XamlCompilerTaskExecutor.cs | 9 +- .../Avalonia.Markup.Xaml.csproj | 2 + .../XamlIl/AvaloniaXamlIlRuntimeCompiler.cs | 11 +- .../AvaloniaXamlIlCompiler.cs | 6 +- .../AvaloniaXamlIlCompilerConfiguration.cs | 21 +++ .../AvaloniaXamlIlLanguage.cs | 2 +- .../AvaloniaXamlIlWellKnownTypes.cs | 42 +++-- .../XamlIlAvaloniaPropertyHelper.cs | 14 ++ .../XamlIlClrPropertyInfoHelper.cs | 147 ++++++++++++++++++ .../Avalonia.Markup.Xaml/XamlIl/xamlil.github | 2 +- .../Xaml/XamlIlTests.cs | 30 ++++ 14 files changed, 348 insertions(+), 27 deletions(-) create mode 100644 src/Avalonia.Base/Data/Core/ClrPropertyInfo.cs create mode 100644 src/Avalonia.Base/Data/Core/IPropertyInfo.cs create mode 100644 src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs create mode 100644 src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlClrPropertyInfoHelper.cs diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index 1de5cb06c6..3e70f549f3 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.Reactive.Subjects; using System.Reflection; using Avalonia.Data; +using Avalonia.Data.Core; using Avalonia.Utilities; namespace Avalonia @@ -14,7 +15,7 @@ namespace Avalonia /// /// Base class for avalonia properties. /// - public class AvaloniaProperty : IEquatable + public class AvaloniaProperty : IEquatable, IPropertyInfo { /// /// Represents an unset property value. @@ -546,7 +547,10 @@ namespace Avalonia } } - + bool IPropertyInfo.CanGet => true; + bool IPropertyInfo.CanSet => true; + object IPropertyInfo.Get(object target) => ((AvaloniaObject)target).GetValue(this); + void IPropertyInfo.Set(object target, object value) => ((AvaloniaObject)target).SetValue(this, value); } /// /// Class representing the . diff --git a/src/Avalonia.Base/Data/Core/ClrPropertyInfo.cs b/src/Avalonia.Base/Data/Core/ClrPropertyInfo.cs new file mode 100644 index 0000000000..05059b6617 --- /dev/null +++ b/src/Avalonia.Base/Data/Core/ClrPropertyInfo.cs @@ -0,0 +1,70 @@ +using System; +using System.Linq.Expressions; +using System.Reflection; + +namespace Avalonia.Data.Core +{ + public class ClrPropertyInfo : IPropertyInfo + { + private readonly Func _getter; + private readonly Action _setter; + + public ClrPropertyInfo(string name, Func getter, Action setter) + { + _getter = getter; + _setter = setter; + Name = name; + } + + public string Name { get; } + public object Get(object target) + { + if (_getter == null) + throw new NotSupportedException("Property " + Name + " doesn't have a getter"); + return _getter(target); + } + + public void Set(object target, object value) + { + if (_setter == null) + throw new NotSupportedException("Property " + Name + " doesn't have a setter"); + _setter(target, value); + } + + public bool CanSet => _setter != null; + public bool CanGet => _getter != null; + } + + public class ReflectionClrPropertyInfo : ClrPropertyInfo + { + static Action CreateSetter(PropertyInfo info) + { + if (info.SetMethod == null) + return null; + var target = Expression.Parameter(typeof(object), "target"); + var value = Expression.Parameter(typeof(object), "value"); + return Expression.Lambda>( + Expression.Call(Expression.Convert(target, info.DeclaringType), info.SetMethod, + Expression.Convert(value, info.SetMethod.GetParameters()[0].ParameterType)), + target, value) + .Compile(); + } + + static Func CreateGetter(PropertyInfo info) + { + if (info.GetMethod == null) + return null; + var target = Expression.Parameter(typeof(object), "target"); + return Expression.Lambda>( + Expression.Convert(Expression.Call(Expression.Convert(target, info.DeclaringType), info.GetMethod), + typeof(object))) + .Compile(); + } + + public ReflectionClrPropertyInfo(PropertyInfo info) : base(info.Name, + CreateGetter(info), CreateSetter(info)) + { + + } + } +} diff --git a/src/Avalonia.Base/Data/Core/IPropertyInfo.cs b/src/Avalonia.Base/Data/Core/IPropertyInfo.cs new file mode 100644 index 0000000000..530b98d08b --- /dev/null +++ b/src/Avalonia.Base/Data/Core/IPropertyInfo.cs @@ -0,0 +1,11 @@ +namespace Avalonia.Data.Core +{ + public interface IPropertyInfo + { + string Name { get; } + object Get(object target); + void Set(object target, object value); + bool CanSet { get; } + bool CanGet { get; } + } +} diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs index c54d8e19a1..4981b27959 100644 --- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs +++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs @@ -49,13 +49,18 @@ namespace Avalonia.Build.Tasks if (avares.Resources.Count(CheckXamlName) == 0 && emres.Resources.Count(CheckXamlName) == 0) // Nothing to do return new CompileResult(true); + + var clrPropertiesDef = new TypeDefinition("CompiledAvaloniaXaml", "XamlIlHelpers", + TypeAttributes.Class, asm.MainModule.TypeSystem.Object); + asm.MainModule.Types.Add(clrPropertiesDef); var xamlLanguage = AvaloniaXamlIlLanguage.Configure(typeSystem); - var compilerConfig = new XamlIlTransformerConfiguration(typeSystem, + var compilerConfig = new AvaloniaXamlIlCompilerConfiguration(typeSystem, typeSystem.TargetAssembly, xamlLanguage, XamlIlXmlnsMappings.Resolve(typeSystem, xamlLanguage), - AvaloniaXamlIlLanguage.CustomValueConverter); + AvaloniaXamlIlLanguage.CustomValueConverter, + new XamlIlClrPropertyInfoEmitter(typeSystem.CreateTypeBuilder(clrPropertiesDef))); var contextDef = new TypeDefinition("CompiledAvaloniaXaml", "XamlIlContext", diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index 6f3dabd568..91d0feedfb 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -38,6 +38,7 @@ + @@ -55,6 +56,7 @@ + diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/AvaloniaXamlIlRuntimeCompiler.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/AvaloniaXamlIlRuntimeCompiler.cs index 167b75603f..5f95ae55a9 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/AvaloniaXamlIlRuntimeCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/AvaloniaXamlIlRuntimeCompiler.cs @@ -114,11 +114,15 @@ namespace Avalonia.Markup.Xaml.XamlIl InitializeSre(); var asm = localAssembly == null ? null : _sreTypeSystem.GetAssembly(localAssembly); + + var tb = _sreBuilder.DefineType("Builder_" + Guid.NewGuid().ToString("N") + "_" + uri); + var clrPropertyBuilder = tb.DefineNestedType("ClrProperties_" + Guid.NewGuid().ToString("N")); - var compiler = new AvaloniaXamlIlCompiler(new XamlIlTransformerConfiguration(_sreTypeSystem, asm, - _sreMappings, _sreXmlns, AvaloniaXamlIlLanguage.CustomValueConverter), + var compiler = new AvaloniaXamlIlCompiler(new AvaloniaXamlIlCompilerConfiguration(_sreTypeSystem, asm, + _sreMappings, _sreXmlns, AvaloniaXamlIlLanguage.CustomValueConverter, + new XamlIlClrPropertyInfoEmitter(_sreTypeSystem.CreateTypeBuilder(clrPropertyBuilder))), _sreContextType); - var tb = _sreBuilder.DefineType("Builder_" + Guid.NewGuid().ToString("N") + "_" + uri); + IXamlIlType overrideType = null; if (rootInstance != null) @@ -129,6 +133,7 @@ namespace Avalonia.Markup.Xaml.XamlIl compiler.IsDesignMode = isDesignMode; compiler.ParseAndCompile(xaml, uri?.ToString(), null, _sreTypeSystem.CreateTypeBuilder(tb), overrideType); var created = tb.CreateTypeInfo(); + clrPropertyBuilder.CreateTypeInfo(); return LoadOrPopulate(created, rootInstance); } diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs index b84f50fa8d..0d0b0e6967 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -16,7 +16,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions private readonly IXamlIlType _contextType; private readonly AvaloniaXamlIlDesignPropertiesTransformer _designTransformer; - private AvaloniaXamlIlCompiler(XamlIlTransformerConfiguration configuration) : base(configuration, true) + private AvaloniaXamlIlCompiler(AvaloniaXamlIlCompilerConfiguration configuration) : base(configuration, true) { _configuration = configuration; @@ -57,14 +57,14 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions } - public AvaloniaXamlIlCompiler(XamlIlTransformerConfiguration configuration, + public AvaloniaXamlIlCompiler(AvaloniaXamlIlCompilerConfiguration configuration, IXamlIlTypeBuilder contextTypeBuilder) : this(configuration) { _contextType = CreateContextType(contextTypeBuilder); } - public AvaloniaXamlIlCompiler(XamlIlTransformerConfiguration configuration, + public AvaloniaXamlIlCompiler(AvaloniaXamlIlCompilerConfiguration configuration, IXamlIlType contextType) : this(configuration) { _contextType = contextType; diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs new file mode 100644 index 0000000000..26965ffea4 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs @@ -0,0 +1,21 @@ +using XamlIl.Transform; +using XamlIl.TypeSystem; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions +{ + class AvaloniaXamlIlCompilerConfiguration : XamlIlTransformerConfiguration + { + public XamlIlClrPropertyInfoEmitter ClrPropertyEmitter { get; } + + public AvaloniaXamlIlCompilerConfiguration(IXamlIlTypeSystem typeSystem, + IXamlIlAssembly defaultAssembly, + XamlIlLanguageTypeMappings typeMappings, + XamlIlXmlnsMappings xmlnsMappings, + XamlIlValueConverter customValueConverter, + XamlIlClrPropertyInfoEmitter clrPropertyEmitter) : base(typeSystem, defaultAssembly, typeMappings, xmlnsMappings, customValueConverter) + { + ClrPropertyEmitter = clrPropertyEmitter; + AddExtra(ClrPropertyEmitter); + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs index c25e1186d0..dad69da3be 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs @@ -50,7 +50,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions }, InnerServiceProviderFactoryMethod = runtimeHelpers.FindMethod(m => m.Name == "CreateInnerServiceProviderV1"), - ProvideValueTargetPropertyEmitter = XamlIlAvaloniaPropertyHelper.Emit, + ProvideValueTargetPropertyEmitter = XamlIlAvaloniaPropertyHelper.EmitProvideValueTarget, }; rv.CustomAttributeResolver = new AttributeResolver(typeSystem, rv); return rv; diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index c054e57380..4c118899dc 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -19,26 +19,30 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlIlType Transitions { get; } public IXamlIlType AssignBindingAttribute { get; } public IXamlIlType UnsetValueType { get; } + public IXamlIlType IPropertyInfo { get; } + public IXamlIlType ClrPropertyInfo { get; } - public AvaloniaXamlIlWellKnownTypes(XamlIlAstTransformationContext ctx) + public AvaloniaXamlIlWellKnownTypes(XamlIlTransformerConfiguration cfg) { - XamlIlTypes = ctx.Configuration.WellKnownTypes; - AvaloniaObject = ctx.Configuration.TypeSystem.GetType("Avalonia.AvaloniaObject"); - IAvaloniaObject = ctx.Configuration.TypeSystem.GetType("Avalonia.IAvaloniaObject"); - AvaloniaObjectExtensions = ctx.Configuration.TypeSystem.GetType("Avalonia.AvaloniaObjectExtensions"); - AvaloniaProperty = ctx.Configuration.TypeSystem.GetType("Avalonia.AvaloniaProperty"); - AvaloniaPropertyT = ctx.Configuration.TypeSystem.GetType("Avalonia.AvaloniaProperty`1"); - BindingPriority = ctx.Configuration.TypeSystem.GetType("Avalonia.Data.BindingPriority"); - IBinding = ctx.Configuration.TypeSystem.GetType("Avalonia.Data.IBinding"); - IDisposable = ctx.Configuration.TypeSystem.GetType("System.IDisposable"); - Transitions = ctx.Configuration.TypeSystem.GetType("Avalonia.Animation.Transitions"); - AssignBindingAttribute = ctx.Configuration.TypeSystem.GetType("Avalonia.Data.AssignBindingAttribute"); + XamlIlTypes = cfg.WellKnownTypes; + AvaloniaObject = cfg.TypeSystem.GetType("Avalonia.AvaloniaObject"); + IAvaloniaObject = cfg.TypeSystem.GetType("Avalonia.IAvaloniaObject"); + AvaloniaObjectExtensions = cfg.TypeSystem.GetType("Avalonia.AvaloniaObjectExtensions"); + AvaloniaProperty = cfg.TypeSystem.GetType("Avalonia.AvaloniaProperty"); + AvaloniaPropertyT = cfg.TypeSystem.GetType("Avalonia.AvaloniaProperty`1"); + BindingPriority = cfg.TypeSystem.GetType("Avalonia.Data.BindingPriority"); + IBinding = cfg.TypeSystem.GetType("Avalonia.Data.IBinding"); + IDisposable = cfg.TypeSystem.GetType("System.IDisposable"); + Transitions = cfg.TypeSystem.GetType("Avalonia.Animation.Transitions"); + AssignBindingAttribute = cfg.TypeSystem.GetType("Avalonia.Data.AssignBindingAttribute"); AvaloniaObjectBindMethod = AvaloniaObjectExtensions.FindMethod("Bind", IDisposable, false, IAvaloniaObject, AvaloniaProperty, - IBinding, ctx.Configuration.WellKnownTypes.Object); - UnsetValueType = ctx.Configuration.TypeSystem.GetType("Avalonia.UnsetValueType"); + IBinding, cfg.WellKnownTypes.Object); + UnsetValueType = cfg.TypeSystem.GetType("Avalonia.UnsetValueType"); AvaloniaObjectSetValueMethod = AvaloniaObject.FindMethod("SetValue", XamlIlTypes.Void, false, AvaloniaProperty, XamlIlTypes.Object, BindingPriority); + IPropertyInfo = cfg.TypeSystem.GetType("Avalonia.Data.Core.IPropertyInfo"); + ClrPropertyInfo = cfg.TypeSystem.GetType("Avalonia.Data.Core.ClrPropertyInfo"); } } @@ -48,7 +52,15 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { if (ctx.TryGetItem(out var rv)) return rv; - ctx.SetItem(rv = new AvaloniaXamlIlWellKnownTypes(ctx)); + ctx.SetItem(rv = new AvaloniaXamlIlWellKnownTypes(ctx.Configuration)); + return rv; + } + + public static AvaloniaXamlIlWellKnownTypes GetAvaloniaTypes(this XamlIlEmitContext ctx) + { + if (ctx.TryGetItem(out var rv)) + return rv; + ctx.SetItem(rv = new AvaloniaXamlIlWellKnownTypes(ctx.Configuration)); return rv; } } diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs index 6fc83cb5a5..e0783364db 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs @@ -15,6 +15,20 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions { class XamlIlAvaloniaPropertyHelper { + public static bool EmitProvideValueTarget(XamlIlEmitContext context, IXamlIlEmitter emitter, + XamlIlAstClrProperty property) + { + if (Emit(context, emitter, property)) + return true; + var foundClr = property.DeclaringType.Properties.FirstOrDefault(p => p.Name == property.Name); + if (foundClr == null) + return false; + context + .Configuration.GetExtra() + .Emit(context, emitter, foundClr); + return true; + } + public static bool Emit(XamlIlEmitContext context, IXamlIlEmitter emitter, XamlIlAstClrProperty property) { if (property is IXamlIlAvaloniaProperty ap) diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlClrPropertyInfoHelper.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlClrPropertyInfoHelper.cs new file mode 100644 index 0000000000..5b681e94a9 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlClrPropertyInfoHelper.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; +using XamlIl.Ast; +using XamlIl.Transform; +using XamlIl.TypeSystem; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions +{ + class XamlIlClrPropertyInfoHelper + { + + class ClrPropertyInfoNode : XamlIlAstNode, IXamlIlAstValueNode + { + public ClrPropertyInfoNode(AvaloniaXamlIlWellKnownTypes types, IXamlIlLineInfo lineInfo) : + base(lineInfo) + { + Type = new XamlIlAstClrTypeReference(this, types.XamlIlTypes.Object, false); + } + + public IXamlIlAstTypeReference Type { get; } + } + } + + class XamlIlClrPropertyInfoEmitter + { + private readonly IXamlIlTypeBuilder _builder; + + private Dictionary> _fields + = new Dictionary>(); + + public XamlIlClrPropertyInfoEmitter(IXamlIlTypeBuilder builder) + { + _builder = builder; + } + + static string GetKey(IXamlIlProperty property) + => property.Getter.DeclaringType.GetFullName() + "." + property.Name; + + public IXamlIlType Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen, IXamlIlProperty property) + { + var types = context.GetAvaloniaTypes(); + IXamlIlMethod Get() + { + var key = GetKey(property); + if (!_fields.TryGetValue(key, out var lst)) + _fields[key] = lst = new List<(IXamlIlProperty prop, IXamlIlMethod get)>(); + + foreach (var cached in lst) + { + if ( + ((cached.prop.Getter == null && property.Getter == null) || + cached.prop.Getter?.Equals(property.Getter) == true) && + ((cached.prop.Setter == null && property.Setter == null) || + cached.prop.Setter?.Equals(property.Setter) == true) + ) + return cached.get; + } + + var name = lst.Count == 0 ? key : key + "_" + Guid.NewGuid().ToString("N"); + + var field = _builder.DefineField(types.IPropertyInfo, name + "!Field", false, true); + + var getter = property.Getter == null ? + null : + _builder.DefineMethod(types.XamlIlTypes.Object, + new[] {types.XamlIlTypes.Object}, name + "!Getter", false, true, false); + if (getter != null) + { + getter.Generator + .Ldarg_0() + .Castclass(property.Getter.DeclaringType) + .EmitCall(property.Getter); + if (property.Getter.ReturnType.IsValueType) + getter.Generator.Box(property.Getter.ReturnType); + getter.Generator.Ret(); + } + + var setter = property.Setter == null ? + null : + _builder.DefineMethod(types.XamlIlTypes.Void, + new[] {types.XamlIlTypes.Object, types.XamlIlTypes.Object}, + name + "!Setter", false, true, false); + if (setter != null) + { + setter.Generator + .Ldarg_0() + .Castclass(property.Setter.DeclaringType) + .Ldarg(1); + if (property.Setter.Parameters[0].IsValueType) + setter.Generator.Unbox_Any(property.Setter.Parameters[0]); + else + setter.Generator.Castclass(property.Setter.Parameters[0]); + setter.Generator + .EmitCall(property.Setter, true) + .Ret(); + } + + var get = _builder.DefineMethod(types.IPropertyInfo, Array.Empty(), + name + "!Property", true, true, false); + + + var ctor = types.ClrPropertyInfo.Constructors.First(c => + c.Parameters.Count == 3 && c.IsStatic == false); + + var cacheMiss = get.Generator.DefineLabel(); + get.Generator + .Ldsfld(field) + .Brfalse(cacheMiss) + .Ldsfld(field) + .Ret() + .MarkLabel(cacheMiss) + .Ldstr(property.Name); + + void EmitFunc(IXamlIlEmitter emitter, IXamlIlMethod method, IXamlIlType del) + { + if (method == null) + emitter.Ldnull(); + else + { + emitter + .Ldnull() + .Ldftn(method) + .Newobj(del.Constructors.First(c => + c.Parameters.Count == 2 && + c.Parameters[0].Equals(context.Configuration.WellKnownTypes.Object))); + } + } + + EmitFunc(get.Generator, getter, ctor.Parameters[1]); + EmitFunc(get.Generator, setter, ctor.Parameters[2]); + get.Generator + .Newobj(ctor) + .Stsfld(field) + .Ldsfld(field) + .Ret(); + + lst.Add((property, get)); + return get; + } + + codeGen.EmitCall(Get()); + return types.IPropertyInfo; + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github index 610cda30c6..3ea474b672 160000 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github @@ -1 +1 @@ -Subproject commit 610cda30c69e32e83c8235060606480904c937bc +Subproject commit 3ea474b672ff63629300c36a9644caac2b74a1f2 diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs index 5398e76f63..913dd754f1 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs @@ -7,6 +7,7 @@ using System.Runtime.CompilerServices; using Avalonia.Controls; using Avalonia.Controls.Presenters; using Avalonia.Data.Converters; +using Avalonia.Data.Core; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Media; @@ -234,6 +235,17 @@ namespace Avalonia.Markup.Xaml.UnitTests Assert.Equal(100, XamlIlBugTestsStaticClassWithAttachedProperty.GetTestInt(tb)); } } + + [Fact] + public void Provide_Value_Target_Should_Provide_Clr_Property_Info() + { + var parsed = AvaloniaXamlLoader.Parse(@" +", typeof(XamlIlClassWithClrPropertyWithValue).Assembly); + Assert.Equal(6, parsed.Count); + } } public class XamlIlBugTestsEventHandlerCodeBehind : Window @@ -314,4 +326,22 @@ namespace Avalonia.Markup.Xaml.UnitTests return (int)control.GetValue(TestIntProperty); } } + + public class XamlIlCheckClrPropertyInfoExtension + { + public string ExpectedPropertyName { get; set; } + + public object ProvideValue(IServiceProvider prov) + { + var pvt = prov.GetService(); + var info = (ClrPropertyInfo)pvt.TargetProperty; + var v = (int)info.Get(pvt.TargetObject); + return v + 1; + } + } + + public class XamlIlClassWithClrPropertyWithValue + { + public int Count { get; set; }= 5; + } } From 048660ed8bacd61910375ed1f5236a8c4ad9c00f Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 14 Jun 2019 11:59:04 +0300 Subject: [PATCH 002/693] Implemented PropertyPathGrammar --- .../Utilities/CharacterReader.cs | 24 +- .../Utilities/IdentifierParser.cs | 2 +- src/Avalonia.Base/Utilities/KeywordParser.cs | 34 +++ .../Parsers/PropertyParser.cs | 2 +- .../Markup/Parsers/ArgumentListParser.cs | 2 +- .../Markup/Parsers/ExpressionParser.cs | 2 +- .../Markup/Parsers/PropertyPathGrammar.cs | 217 ++++++++++++++++++ .../Markup/Parsers/SelectorGrammar.cs | 10 +- .../Data/Core/PropertyPathGrammarTests.cs | 103 +++++++++ 9 files changed, 384 insertions(+), 12 deletions(-) create mode 100644 src/Avalonia.Base/Utilities/KeywordParser.cs create mode 100644 src/Markup/Avalonia.Markup/Markup/Parsers/PropertyPathGrammar.cs create mode 100644 tests/Avalonia.Base.UnitTests/Data/Core/PropertyPathGrammarTests.cs diff --git a/src/Avalonia.Base/Utilities/CharacterReader.cs b/src/Avalonia.Base/Utilities/CharacterReader.cs index 9be2d230d4..67a77f1c5c 100644 --- a/src/Avalonia.Base/Utilities/CharacterReader.cs +++ b/src/Avalonia.Base/Utilities/CharacterReader.cs @@ -19,7 +19,7 @@ namespace Avalonia.Utilities } public bool End => _s.IsEmpty; - public char Peek => _s[0]; + public char PeekOneOrThrow => _s[0]; public int Position { get; private set; } public char Take() { @@ -38,7 +38,7 @@ namespace Avalonia.Utilities public bool TakeIf(char c) { - if (Peek == c) + if (PeekOneOrThrow == c) { Take(); return true; @@ -51,7 +51,7 @@ namespace Avalonia.Utilities public bool TakeIf(Func condition) { - if (condition(Peek)) + if (condition(PeekOneOrThrow)) { Take(); return true; @@ -82,5 +82,23 @@ namespace Avalonia.Utilities Position += len; return span; } + + public ReadOnlySpan TryPeek(int count) + { + return _s.Slice(0, count); + } + + public ReadOnlySpan PeekWhitespace() + { + var trimmed = _s.TrimStart(); + return _s.Slice(0, _s.Length - trimmed.Length); + } + + public void Skip(int count) + { + if (_s.Length < count) + throw new IndexOutOfRangeException(); + _s = _s.Slice(count); + } } } diff --git a/src/Avalonia.Base/Utilities/IdentifierParser.cs b/src/Avalonia.Base/Utilities/IdentifierParser.cs index a57a2b7ba5..ab14ea2c58 100644 --- a/src/Avalonia.Base/Utilities/IdentifierParser.cs +++ b/src/Avalonia.Base/Utilities/IdentifierParser.cs @@ -13,7 +13,7 @@ namespace Avalonia.Utilities { public static ReadOnlySpan ParseIdentifier(this ref CharacterReader r) { - if (IsValidIdentifierStart(r.Peek)) + if (IsValidIdentifierStart(r.PeekOneOrThrow)) { return r.TakeWhile(IsValidIdentifierChar); } diff --git a/src/Avalonia.Base/Utilities/KeywordParser.cs b/src/Avalonia.Base/Utilities/KeywordParser.cs new file mode 100644 index 0000000000..5c7d415820 --- /dev/null +++ b/src/Avalonia.Base/Utilities/KeywordParser.cs @@ -0,0 +1,34 @@ +using System; + +namespace Avalonia.Utilities +{ +#if !BUILDTASK + public +#endif + static class KeywordParser + { + public static bool CheckKeyword(this ref CharacterReader r, string keyword) + { + return (CheckKeywordInternal(ref r, keyword) >= 0); + } + + static int CheckKeywordInternal(this ref CharacterReader r, string keyword) + { + var ws = r.PeekWhitespace(); + + var chars = r.TryPeek(ws.Length + keyword.Length); + if (chars.Slice(ws.Length).Equals(keyword.AsSpan(), StringComparison.Ordinal)) + return chars.Length; + return -1; + } + + public static bool TakeIfKeyword(this ref CharacterReader r, string keyword) + { + var l = CheckKeywordInternal(ref r, keyword); + if (l < 0) + return false; + r.Skip(l); + return true; + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs b/src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs index ea94012096..ebd6347a86 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs @@ -41,7 +41,7 @@ namespace Avalonia.Markup.Xaml.Parsers throw new ExpressionParseException(r.Position, $"Expected ')'."); } - throw new ExpressionParseException(r.Position, $"Unexpected '{r.Peek}'."); + throw new ExpressionParseException(r.Position, $"Unexpected '{r.PeekOneOrThrow}'."); } } else if (!r.End && r.TakeIf(':')) diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs index cb9c81caa1..fd6f7b20bd 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs @@ -11,7 +11,7 @@ namespace Avalonia.Markup.Parsers { public static IList ParseArguments(this ref CharacterReader r, char open, char close, char delimiter = ',') { - if (r.Peek == open) + if (r.PeekOneOrThrow == open) { var result = new List(); diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs index a1350a8393..2a737d36e9 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs @@ -304,7 +304,7 @@ namespace Avalonia.Markup.Parsers private static bool PeekOpenBracket(ref CharacterReader r) { - return !r.End && r.Peek == '['; + return !r.End && r.PeekOneOrThrow == '['; } private static bool ParseStreamOperator(ref CharacterReader r) diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/PropertyPathGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/PropertyPathGrammar.cs new file mode 100644 index 0000000000..89490db2aa --- /dev/null +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/PropertyPathGrammar.cs @@ -0,0 +1,217 @@ +using System; +using System.Collections.Generic; +using Avalonia.Data.Core; +using Avalonia.Utilities; + +namespace Avalonia.Markup.Parsers +{ +#if !BUILDTASK + public +#endif + class PropertyPathGrammar + { + private enum State + { + Start, + Next, + AfterProperty, + End + } + + public static IEnumerable Parse(string s) + { + var r = new CharacterReader(s.AsSpan()); + return Parse(ref r); + } + + private static IEnumerable Parse(ref CharacterReader r) + { + var state = State.Start; + var parsed = new List(); + while (state != State.End) + { + ISyntax syntax = null; + if (state == State.Start) + (state, syntax) = ParseStart(ref r); + else if (state == State.Next) + (state, syntax) = ParseNext(ref r); + else if (state == State.AfterProperty) + (state, syntax) = ParseAfterProperty(ref r); + + + if (syntax != null) + { + parsed.Add(syntax); + } + } + + if (state != State.End && r.End) + { + throw new ExpressionParseException(r.Position, "Unexpected end of property path"); + } + + return parsed; + } + + private static (State, ISyntax) ParseNext(ref CharacterReader r) + { + r.SkipWhitespace(); + if (r.End) + return (State.End, null); + + return ParseStart(ref r); + } + + private static (State, ISyntax) ParseStart(ref CharacterReader r) + { + r.SkipWhitespace(); + + if (r.TakeIf('(')) + return ParseTypeQualifiedProperty(ref r); + + return ParseProperty(ref r); + } + + private static (State, ISyntax) ParseTypeQualifiedProperty(ref CharacterReader r) + { + r.SkipWhitespace(); + const string error = + "Unable to parse qualified property name, expected `(ns:TypeName.PropertyName)` or `(TypeName.PropertyName)` after `(`"; + + var typeName = ParseXamlIdentifier(ref r); + + + if (!r.TakeIf('.')) + throw new ExpressionParseException(r.Position, error); + + var propertyName = r.ParseIdentifier(); + if (propertyName.IsEmpty) + throw new ExpressionParseException(r.Position, error); + + r.SkipWhitespace(); + if (!r.TakeIf(')')) + throw new ExpressionParseException(r.Position, + "Expected ')' after qualified property name " + + typeName.ns + ':' + typeName.name + + "." + propertyName.ToString()); + + return (State.AfterProperty, + new TypeQualifiedPropertySyntax + { + Name = propertyName.ToString(), + TypeName = typeName.name, + TypeNamespace = typeName.ns + }); + } + + static (string ns, string name) ParseXamlIdentifier(ref CharacterReader r) + { + var ident = r.ParseIdentifier(); + if (ident.IsEmpty) + throw new ExpressionParseException(r.Position, "Expected identifier"); + if (r.TakeIf(':')) + { + var part2 = r.ParseIdentifier(); + if (part2.IsEmpty) + throw new ExpressionParseException(r.Position, + "Expected the rest of the identifier after " + ident.ToString() + ":"); + return (ident.ToString(), part2.ToString()); + } + + return (null, ident.ToString()); + } + + private static (State, ISyntax) ParseProperty(ref CharacterReader r) + { + r.SkipWhitespace(); + var prop = r.ParseIdentifier(); + if (prop.IsEmpty) + throw new ExpressionParseException(r.Position, "Unable to parse property name"); + return (State.AfterProperty, new PropertySyntax {Name = prop.ToString()}); + } + + + private static (State, ISyntax) ParseAfterProperty(ref CharacterReader r) + { + r.SkipWhitespace(); + if (r.End) + return (State.End, null); + if (r.TakeIf('.')) + return (State.Next, ChildTraversalSyntax.Instance); + if (r.TakeIfKeyword(":=")) + return ParseEnsureType(ref r); + + if (r.TakeIfKeyword(":>") || r.TakeIfKeyword("as ")) + return ParseCastType(ref r); + + throw new ExpressionParseException(r.Position, "Unexpected character " + r.PeekOneOrThrow + " after property name"); + } + + private static (State, ISyntax) ParseEnsureType(ref CharacterReader r) + { + r.SkipWhitespace(); + var type = ParseXamlIdentifier(ref r); + return (State.AfterProperty, new EnsureTypeSyntax {TypeName = type.name, TypeNamespace = type.ns}); + } + + private static (State, ISyntax) ParseCastType(ref CharacterReader r) + { + r.SkipWhitespace(); + var type = ParseXamlIdentifier(ref r); + return (State.AfterProperty, new CastTypeSyntax {TypeName = type.name, TypeNamespace = type.ns}); + } + + public interface ISyntax + { + + } + + public class PropertySyntax : ISyntax + { + public string Name { get; set; } + + public override bool Equals(object obj) + => obj is PropertySyntax other + && other.Name == Name; + } + + public class TypeQualifiedPropertySyntax : ISyntax + { + public string Name { get; set; } + public string TypeName { get; set; } + public string TypeNamespace { get; set; } + + public override bool Equals(object obj) + => obj is TypeQualifiedPropertySyntax other + && other.Name == Name + && other.TypeName == TypeName + && other.TypeNamespace == TypeNamespace; + } + + public class ChildTraversalSyntax : ISyntax + { + public static ChildTraversalSyntax Instance { get; } = new ChildTraversalSyntax(); + public override bool Equals(object obj) => obj is ChildTraversalSyntax; + } + + public class EnsureTypeSyntax : ISyntax + { + public string TypeName { get; set; } + public string TypeNamespace { get; set; } + public override bool Equals(object obj) + => obj is EnsureTypeSyntax other + && other.TypeName == TypeName + && other.TypeNamespace == TypeNamespace; + } + + public class CastTypeSyntax : ISyntax + { + public string TypeName { get; set; } + public string TypeNamespace { get; set; } + public override bool Equals(object obj) + => obj is CastTypeSyntax other + && other.TypeName == TypeName + && other.TypeNamespace == TypeNamespace; + } + } +} diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs index c57b8c4cf6..2de66d5501 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs @@ -123,7 +123,7 @@ namespace Avalonia.Markup.Parsers { return (State.Class, null); } - else if (r.TakeIf(char.IsWhiteSpace) || r.Peek == '>') + else if (r.TakeIf(char.IsWhiteSpace) || r.PeekOneOrThrow == '>') { return (State.Traversal, null); } @@ -139,7 +139,7 @@ namespace Avalonia.Markup.Parsers { return (State.Start, new CommaSyntax()); } - else if (end.HasValue && !r.End && r.Peek == end.Value) + else if (end.HasValue && !r.End && r.PeekOneOrThrow == end.Value) { return (State.End, null); } @@ -262,7 +262,7 @@ namespace Avalonia.Markup.Parsers if (!r.TakeIf('=')) { - throw new ExpressionParseException(r.Position, $"Expected '=', got '{r.Peek}'"); + throw new ExpressionParseException(r.Position, $"Expected '=', got '{r.PeekOneOrThrow}'"); } var value = r.TakeUntil(']'); @@ -281,7 +281,7 @@ namespace Avalonia.Markup.Parsers if (namespaceOrTypeName.IsEmpty) { - throw new ExpressionParseException(r.Position, $"Expected an identifier, got '{r.Peek}"); + throw new ExpressionParseException(r.Position, $"Expected an identifier, got '{r.PeekOneOrThrow}"); } if (!r.End && r.TakeIf('|')) @@ -311,7 +311,7 @@ namespace Avalonia.Markup.Parsers } else if (!r.TakeIf(')')) { - throw new ExpressionParseException(r.Position, $"Expected '{c}', got '{r.Peek}'."); + throw new ExpressionParseException(r.Position, $"Expected '{c}', got '{r.PeekOneOrThrow}'."); } } diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/PropertyPathGrammarTests.cs b/tests/Avalonia.Base.UnitTests/Data/Core/PropertyPathGrammarTests.cs new file mode 100644 index 0000000000..99144b7381 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Data/Core/PropertyPathGrammarTests.cs @@ -0,0 +1,103 @@ +using System.Collections.Generic; +using System.Linq; +using Avalonia.Markup.Parsers; +using Xunit; + +namespace Avalonia.Base.UnitTests.Data.Core +{ + public class PropertyPathGrammarTests + { + static void Check(string s, params PropertyPathGrammar.ISyntax[] expected) + { + var parsed = PropertyPathGrammar.Parse(s).ToList(); + Assert.Equal(expected.Length, parsed.Count); + for (var c = 0; c < parsed.Count; c++) + Assert.Equal(expected[c], parsed[c]); + } + + [Fact] + public void PropertyPath_Should_Support_Simple_Properties() + { + Check("SomeProperty", new PropertyPathGrammar.PropertySyntax {Name = "SomeProperty"}); + } + + [Fact] + public void PropertyPath_Should_Ignore_Trailing_Whitespace() + { + Check(" SomeProperty ", new PropertyPathGrammar.PropertySyntax {Name = "SomeProperty"}); + } + + [Fact] + public void PropertyPath_Should_Support_Qualified_Properties() + { + Check(" ( somens:SomeType.SomeProperty ) ", + new PropertyPathGrammar.TypeQualifiedPropertySyntax() + { + Name = "SomeProperty", TypeName = "SomeType", TypeNamespace = "somens" + }); + } + + [Fact] + public void PropertyPath_Should_Support_Property_Paths() + { + Check(" ( somens:SomeType.SomeProperty ).Child . SubChild ", + new PropertyPathGrammar.TypeQualifiedPropertySyntax() + { + Name = "SomeProperty", TypeName = "SomeType", TypeNamespace = "somens" + }, + PropertyPathGrammar.ChildTraversalSyntax.Instance, + new PropertyPathGrammar.PropertySyntax {Name = "Child"}, + PropertyPathGrammar.ChildTraversalSyntax.Instance, + new PropertyPathGrammar.PropertySyntax {Name = "SubChild"} + ); + } + + [Fact] + public void PropertyPath_Should_Support_Casts() + { + Check(" ( somens:SomeType.SomeProperty ) :> SomeType.Child as somens:SomeType . SubChild ", + new PropertyPathGrammar.TypeQualifiedPropertySyntax() + { + Name = "SomeProperty", TypeName = "SomeType", TypeNamespace = "somens" + }, + new PropertyPathGrammar.CastTypeSyntax + { + TypeName = "SomeType" + }, + PropertyPathGrammar.ChildTraversalSyntax.Instance, + new PropertyPathGrammar.PropertySyntax {Name = "Child"}, + new PropertyPathGrammar.CastTypeSyntax + { + TypeName = "SomeType", + TypeNamespace = "somens" + }, + PropertyPathGrammar.ChildTraversalSyntax.Instance, + new PropertyPathGrammar.PropertySyntax {Name = "SubChild"} + ); + } + + [Fact] + public void PropertyPath_Should_Support_Ensure_Type() + { + Check(" ( somens:SomeType.SomeProperty ) := SomeType.Child := somens:SomeType . SubChild ", + new PropertyPathGrammar.TypeQualifiedPropertySyntax() + { + Name = "SomeProperty", TypeName = "SomeType", TypeNamespace = "somens" + }, + new PropertyPathGrammar.EnsureTypeSyntax + { + TypeName = "SomeType" + }, + PropertyPathGrammar.ChildTraversalSyntax.Instance, + new PropertyPathGrammar.PropertySyntax {Name = "Child"}, + new PropertyPathGrammar.EnsureTypeSyntax + { + TypeName = "SomeType", + TypeNamespace = "somens" + }, + PropertyPathGrammar.ChildTraversalSyntax.Instance, + new PropertyPathGrammar.PropertySyntax {Name = "SubChild"} + ); + } + } +} From 8ad9a0f675a54f1a14bf00d5517d751abe65762b Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 14 Jun 2019 14:26:34 +0300 Subject: [PATCH 003/693] Compiled property paths --- src/Avalonia.Base/Data/Core/PropertyPath.cs | 91 +++++++ .../Utilities/CharacterReader.cs | 2 + src/Avalonia.Base/Utilities/KeywordParser.cs | 12 +- src/Avalonia.Styling/Styling/Setter.cs | 6 + .../Avalonia.Markup.Xaml.csproj | 1 + .../AvaloniaXamlIlCompiler.cs | 1 + .../AvaloniaXamlIlPropertyPathTransformer.cs | 250 ++++++++++++++++++ .../AvaloniaXamlIlSetterTransformer.cs | 35 ++- .../AvaloniaXamlIlWellKnownTypes.cs | 4 + .../XamlIlAvaloniaPropertyHelper.cs | 38 +-- .../XamlIlClrPropertyInfoHelper.cs | 39 ++- .../Avalonia.Markup.Xaml/XamlIl/xamlil.github | 2 +- .../Markup/Parsers/PropertyPathGrammar.cs | 24 +- .../Xaml/XamlIlTests.cs | 48 ++++ 14 files changed, 496 insertions(+), 57 deletions(-) create mode 100644 src/Avalonia.Base/Data/Core/PropertyPath.cs create mode 100644 src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlPropertyPathTransformer.cs diff --git a/src/Avalonia.Base/Data/Core/PropertyPath.cs b/src/Avalonia.Base/Data/Core/PropertyPath.cs new file mode 100644 index 0000000000..665953c4a1 --- /dev/null +++ b/src/Avalonia.Base/Data/Core/PropertyPath.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Avalonia.Data.Core +{ + public class PropertyPath + { + public IReadOnlyList Elements { get; } + + public PropertyPath(IEnumerable elements) + { + Elements = elements.ToList(); + } + } + + public class PropertyPathBuilder + { + readonly List _elements = new List(); + + public PropertyPathBuilder Property(IPropertyInfo property) + { + _elements.Add(new PropertyPropertyPathElement(property)); + return this; + } + + + public PropertyPathBuilder ChildTraversal() + { + _elements.Add(new ChildTraversalPropertyPathElement()); + return this; + } + + public PropertyPathBuilder EnsureType(Type type) + { + _elements.Add(new EnsureTypePropertyPathElement(type)); + return this; + } + + public PropertyPathBuilder Cast(Type type) + { + _elements.Add(new CastTypePropertyPathElement(type)); + return this; + } + + public PropertyPath Build() + { + return new PropertyPath(_elements); + } + } + + public interface IPropertyPathElement + { + + } + + public class PropertyPropertyPathElement : IPropertyPathElement + { + public IPropertyInfo Property { get; } + + public PropertyPropertyPathElement(IPropertyInfo property) + { + Property = property; + } + } + + public class ChildTraversalPropertyPathElement : IPropertyPathElement + { + + } + + public class EnsureTypePropertyPathElement : IPropertyPathElement + { + public Type Type { get; } + + public EnsureTypePropertyPathElement(Type type) + { + Type = type; + } + } + + public class CastTypePropertyPathElement : IPropertyPathElement + { + public CastTypePropertyPathElement(Type type) + { + Type = type; + } + + public Type Type { get; } + } +} diff --git a/src/Avalonia.Base/Utilities/CharacterReader.cs b/src/Avalonia.Base/Utilities/CharacterReader.cs index 67a77f1c5c..3ba79c6866 100644 --- a/src/Avalonia.Base/Utilities/CharacterReader.cs +++ b/src/Avalonia.Base/Utilities/CharacterReader.cs @@ -85,6 +85,8 @@ namespace Avalonia.Utilities public ReadOnlySpan TryPeek(int count) { + if (_s.Length < count) + return ReadOnlySpan.Empty; return _s.Slice(0, count); } diff --git a/src/Avalonia.Base/Utilities/KeywordParser.cs b/src/Avalonia.Base/Utilities/KeywordParser.cs index 5c7d415820..34e1ea6f8e 100644 --- a/src/Avalonia.Base/Utilities/KeywordParser.cs +++ b/src/Avalonia.Base/Utilities/KeywordParser.cs @@ -17,11 +17,21 @@ namespace Avalonia.Utilities var ws = r.PeekWhitespace(); var chars = r.TryPeek(ws.Length + keyword.Length); - if (chars.Slice(ws.Length).Equals(keyword.AsSpan(), StringComparison.Ordinal)) + if (SpanEquals(chars.Slice(ws.Length), keyword.AsSpan())) return chars.Length; return -1; } + static bool SpanEquals(ReadOnlySpan left, ReadOnlySpan right) + { + if (left.Length != right.Length) + return false; + for(var c=0; c + /// Gets or sets the property path + /// + public PropertyPath PropertyPath { get; set; } /// /// Gets or sets the property value. diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index 91d0feedfb..040ed57383 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -45,6 +45,7 @@ + diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs index 0d0b0e6967..eadad60cde 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -44,6 +44,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions InsertBefore( new AvaloniaXamlIlSelectorTransformer(), + new AvaloniaXamlIlPropertyPathTransformer(), new AvaloniaXamlIlSetterTransformer(), new AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer(), new AvaloniaXamlIlConstructorServiceProviderTransformer(), diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlPropertyPathTransformer.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlPropertyPathTransformer.cs new file mode 100644 index 0000000000..99ba264214 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlPropertyPathTransformer.cs @@ -0,0 +1,250 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Markup.Parsers; +using XamlIl; +using XamlIl.Ast; +using XamlIl.Transform; +using XamlIl.Transform.Transformers; +using XamlIl.TypeSystem; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers +{ + class AvaloniaXamlIlPropertyPathTransformer : IXamlIlAstTransformer + { + public IXamlIlAstNode Transform(XamlIlAstTransformationContext context, IXamlIlAstNode node) + { + if (node is XamlIlAstXamlPropertyValueNode pv + && pv.Values.Count == 1 + && pv.Values[0] is XamlIlAstTextNode text + && pv.Property.GetClrProperty().Getter.ReturnType.Equals(context.GetAvaloniaTypes().PropertyPath) + ) + { + var parentScope = context.ParentNodes().OfType() + .FirstOrDefault(); + if(parentScope == null) + throw new XamlIlParseException("No target type scope found for property path", text); + if (parentScope.ScopeType != AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.Style) + throw new XamlIlParseException("PropertyPath is currently only valid for styles", pv); + + + IEnumerable parsed; + try + { + parsed = PropertyPathGrammar.Parse(text.Text); + } + catch (Exception e) + { + throw new XamlIlParseException("Unable to parse PropertyPath: " + e.Message, text); + } + + var elements = new List(); + IXamlIlType currentType = parentScope.TargetType.GetClrType(); + + + var expectProperty = true; + var expectCast = true; + var expectTraversal = false; + var types = context.GetAvaloniaTypes(); + + IXamlIlType GetType(string ns, string name) + { + return XamlIlTypeReferenceResolver.ResolveType(context, $"{ns}:{name}", false, + text, true).GetClrType(); + } + + void HandleProperty(string name, string typeNamespace, string typeName) + { + if(!expectProperty || currentType == null) + throw new XamlIlParseException("Unexpected property node", text); + + var propertySearchType = + typeName != null ? GetType(typeNamespace, typeName) : currentType; + + IXamlIlPropertyPathElementNode prop = null; + var avaloniaPropertyFieldName = name + "Property"; + var avaloniaPropertyField = propertySearchType.GetAllFields().FirstOrDefault(f => + f.IsStatic && f.IsPublic && f.Name == avaloniaPropertyFieldName); + if (avaloniaPropertyField != null) + { + prop = new XamlIlAvaloniaPropertyPropertyPathElementNode(avaloniaPropertyField, + XamlIlAvaloniaPropertyHelper.GetAvaloniaPropertyType(avaloniaPropertyField, types, text)); + } + else + { + var clrProperty = propertySearchType.GetAllProperties().FirstOrDefault(p => p.Name == name); + prop = new XamlIClrPropertyPathElementNode(clrProperty); + } + + if (prop == null) + throw new XamlIlParseException( + $"Unable to resolve property {name} on type {propertySearchType.GetFqn()}", + text); + + currentType = prop.Type; + elements.Add(prop); + expectProperty = false; + expectTraversal = expectCast = true; + } + + foreach (var ge in parsed) + { + if (ge is PropertyPathGrammar.ChildTraversalSyntax) + { + if (!expectTraversal) + throw new XamlIlParseException("Unexpected child traversal .", text); + elements.Add(new XamlIlChildTraversalPropertyPathElementNode()); + expectTraversal = expectCast = false; + expectProperty = true; + } + else if (ge is PropertyPathGrammar.EnsureTypeSyntax ets) + { + if(!expectCast) + throw new XamlIlParseException("Unexpected cast node", text); + currentType = GetType(ets.TypeNamespace, ets.TypeName); + elements.Add(new XamlIlCastPropertyPathElementNode(currentType, true)); + expectProperty = false; + expectCast = expectTraversal = true; + } + else if (ge is PropertyPathGrammar.CastTypeSyntax cts) + { + if(!expectCast) + throw new XamlIlParseException("Unexpected cast node", text); + //TODO: Check if cast can be done + currentType = GetType(cts.TypeNamespace, cts.TypeName); + elements.Add(new XamlIlCastPropertyPathElementNode(currentType, false)); + expectProperty = false; + expectCast = expectTraversal = true; + } + else if (ge is PropertyPathGrammar.PropertySyntax ps) + { + HandleProperty(ps.Name, null, null); + } + else if (ge is PropertyPathGrammar.TypeQualifiedPropertySyntax tqps) + { + HandleProperty(tqps.Name, tqps.TypeNamespace, tqps.TypeName); + } + else + throw new XamlIlParseException("Unexpected node " + ge, text); + + } + var propertyPathNode = new XamlIlPropertyPathNode(text, elements, types); + if (propertyPathNode.Type == null) + throw new XamlIlParseException("Unexpected end of the property path", text); + pv.Values[0] = propertyPathNode; + } + + return node; + } + + interface IXamlIlPropertyPathElementNode + { + void Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen); + IXamlIlType Type { get; } + } + + class XamlIlChildTraversalPropertyPathElementNode : IXamlIlPropertyPathElementNode + { + public void Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + => codeGen.EmitCall( + context.GetAvaloniaTypes() + .PropertyPathBuilder.FindMethod(m => m.Name == "ChildTraversal")); + + public IXamlIlType Type => null; + } + + class XamlIlAvaloniaPropertyPropertyPathElementNode : IXamlIlPropertyPathElementNode + { + private readonly IXamlIlField _field; + + public XamlIlAvaloniaPropertyPropertyPathElementNode(IXamlIlField field, IXamlIlType propertyType) + { + _field = field; + Type = propertyType; + } + + public void Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + => codeGen + .Ldsfld(_field) + .EmitCall(context.GetAvaloniaTypes() + .PropertyPathBuilder.FindMethod(m => m.Name == "Property")); + + public IXamlIlType Type { get; } + } + + class XamlIClrPropertyPathElementNode : IXamlIlPropertyPathElementNode + { + private readonly IXamlIlProperty _property; + + public XamlIClrPropertyPathElementNode(IXamlIlProperty property) + { + _property = property; + } + + public void Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + { + context.Configuration.GetExtra() + .Emit(context, codeGen, _property); + + codeGen.EmitCall(context.GetAvaloniaTypes() + .PropertyPathBuilder.FindMethod(m => m.Name == "Property")); + } + + public IXamlIlType Type => _property.Getter?.ReturnType ?? _property.Setter?.Parameters[0]; + } + + class XamlIlCastPropertyPathElementNode : IXamlIlPropertyPathElementNode + { + private readonly IXamlIlType _type; + private readonly bool _ensureType; + + public XamlIlCastPropertyPathElementNode(IXamlIlType type, bool ensureType) + { + _type = type; + _ensureType = ensureType; + } + + public void Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + { + codeGen + .Ldtype(_type) + .EmitCall(context.GetAvaloniaTypes() + .PropertyPathBuilder.FindMethod(m => m.Name == (_ensureType ? "EnsureType" : "Cast"))); + } + + public IXamlIlType Type => _type; + } + + class XamlIlPropertyPathNode : XamlIlAstNode, IXamlIlPropertyPathNode, IXamlIlAstEmitableNode + { + private readonly List _elements; + private readonly AvaloniaXamlIlWellKnownTypes _types; + + public XamlIlPropertyPathNode(IXamlIlLineInfo lineInfo, + List elements, + AvaloniaXamlIlWellKnownTypes types) : base(lineInfo) + { + _elements = elements; + _types = types; + Type = new XamlIlAstClrTypeReference(this, types.PropertyPath, false); + } + + public IXamlIlAstTypeReference Type { get; } + public IXamlIlType PropertyType => _elements.LastOrDefault()?.Type; + public XamlIlNodeEmitResult Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + { + codeGen + .Newobj(_types.PropertyPathBuilder.FindConstructor()); + foreach(var e in _elements) + e.Emit(context, codeGen); + codeGen.EmitCall(_types.PropertyPathBuilder.FindMethod(m => m.Name == "Build")); + return XamlIlNodeEmitResult.Type(0, _types.PropertyPath); + } + } + } + + interface IXamlIlPropertyPathNode : IXamlIlAstValueNode + { + IXamlIlType PropertyType { get; } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs index 629e2562d3..468717f281 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Avalonia.Data.Core; using XamlIl; using XamlIl.Ast; using XamlIl.Transform; @@ -33,29 +34,39 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers throw new XamlIlParseException( "Can not resolve parent Style Selector type", node); - + IXamlIlType propType = null; var property = @on.Children.OfType() .FirstOrDefault(x => x.Property.GetClrProperty().Name == "Property"); - if (property == null) - throw new XamlIlParseException("Setter without a property is not valid", node); + if (property != null) + { - var propertyName = property.Values.OfType().FirstOrDefault()?.Text; - if (propertyName == null) - throw new XamlIlParseException("Setter.Property must be a string", node); + var propertyName = property.Values.OfType().FirstOrDefault()?.Text; + if (propertyName == null) + throw new XamlIlParseException("Setter.Property must be a string", node); - var avaloniaPropertyNode = XamlIlAvaloniaPropertyHelper.CreateNode(context, propertyName, - new XamlIlAstClrTypeReference(selector, selector.TargetType, false), property.Values[0]); - property.Values = new List + var avaloniaPropertyNode = XamlIlAvaloniaPropertyHelper.CreateNode(context, propertyName, + new XamlIlAstClrTypeReference(selector, selector.TargetType, false), property.Values[0]); + property.Values = new List {avaloniaPropertyNode}; + propType = avaloniaPropertyNode.AvaloniaPropertyType; + } + else { - avaloniaPropertyNode - }; + var propertyPath = on.Children.OfType() + .FirstOrDefault(x => x.Property.GetClrProperty().Name == "PropertyPath"); + if (propertyPath == null) + throw new XamlIlParseException("Setter without a property or property path is not valid", node); + if (propertyPath.Values[0] is IXamlIlPropertyPathNode ppn + && ppn.PropertyType != null) + propType = ppn.PropertyType; + else + throw new XamlIlParseException("Unable to get the property path property type", node); + } var valueProperty = on.Children .OfType().FirstOrDefault(p => p.Property.GetClrProperty().Name == "Value"); if (valueProperty?.Values?.Count == 1 && valueProperty.Values[0] is XamlIlAstTextNode) { - var propType = avaloniaPropertyNode.AvaloniaPropertyType; if (!XamlIlTransformHelpers.TryGetCorrectlyTypedValue(context, valueProperty.Values[0], propType, out var converted)) throw new XamlIlParseException( diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index 4c118899dc..f9f59dfb87 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -21,6 +21,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlIlType UnsetValueType { get; } public IXamlIlType IPropertyInfo { get; } public IXamlIlType ClrPropertyInfo { get; } + public IXamlIlType PropertyPath { get; } + public IXamlIlType PropertyPathBuilder { get; } public AvaloniaXamlIlWellKnownTypes(XamlIlTransformerConfiguration cfg) { @@ -43,6 +45,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers false, AvaloniaProperty, XamlIlTypes.Object, BindingPriority); IPropertyInfo = cfg.TypeSystem.GetType("Avalonia.Data.Core.IPropertyInfo"); ClrPropertyInfo = cfg.TypeSystem.GetType("Avalonia.Data.Core.ClrPropertyInfo"); + PropertyPath = cfg.TypeSystem.GetType("Avalonia.Data.Core.PropertyPath"); + PropertyPathBuilder = cfg.TypeSystem.GetType("Avalonia.Data.Core.PropertyPathBuilder"); } } diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs index e0783364db..bc7c58bbb4 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs @@ -94,6 +94,26 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions context.Configuration.TypeSystem.GetType("Avalonia.AvaloniaProperty"), clrProperty); } + + public static IXamlIlType GetAvaloniaPropertyType(IXamlIlField field, + AvaloniaXamlIlWellKnownTypes types, IXamlIlLineInfo lineInfo) + { + var avaloniaPropertyType = field.FieldType; + while (avaloniaPropertyType != null) + { + if (avaloniaPropertyType.GenericTypeDefinition?.Equals(types.AvaloniaPropertyT) == true) + { + return avaloniaPropertyType.GenericArguments[0]; + } + + avaloniaPropertyType = avaloniaPropertyType.BaseType; + } + + throw new XamlIlParseException( + $"{field.Name}'s type {field.FieldType} doesn't inherit from AvaloniaProperty, make sure to use typed properties", + lineInfo); + + } } interface IXamlIlAvaloniaPropertyNode : IXamlIlAstValueNode @@ -132,22 +152,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions IXamlIlLineInfo lineInfo, IXamlIlField field) : base(lineInfo) { _field = field; - var avaloniaPropertyType = field.FieldType; - while (avaloniaPropertyType != null) - { - if (avaloniaPropertyType.GenericTypeDefinition?.Equals(types.AvaloniaPropertyT) == true) - { - AvaloniaPropertyType = avaloniaPropertyType.GenericArguments[0]; - return; - } - - avaloniaPropertyType = avaloniaPropertyType.BaseType; - } - - throw new XamlIlParseException( - $"{field.Name}'s type {field.FieldType} doesn't inherit from AvaloniaProperty, make sure to use typed properties", - lineInfo); - + AvaloniaPropertyType = XamlIlAvaloniaPropertyHelper.GetAvaloniaPropertyType(field, + types, lineInfo); } diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlClrPropertyInfoHelper.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlClrPropertyInfoHelper.cs index 5b681e94a9..95ec8536fc 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlClrPropertyInfoHelper.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlClrPropertyInfoHelper.cs @@ -8,21 +8,6 @@ using XamlIl.TypeSystem; namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions { - class XamlIlClrPropertyInfoHelper - { - - class ClrPropertyInfoNode : XamlIlAstNode, IXamlIlAstValueNode - { - public ClrPropertyInfoNode(AvaloniaXamlIlWellKnownTypes types, IXamlIlLineInfo lineInfo) : - base(lineInfo) - { - Type = new XamlIlAstClrTypeReference(this, types.XamlIlTypes.Object, false); - } - - public IXamlIlAstTypeReference Type { get; } - } - } - class XamlIlClrPropertyInfoEmitter { private readonly IXamlIlTypeBuilder _builder; @@ -62,16 +47,25 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions var field = _builder.DefineField(types.IPropertyInfo, name + "!Field", false, true); + void Load(IXamlIlMethod m, IXamlIlEmitter cg) + { + cg + .Ldarg_0(); + if (m.DeclaringType.IsValueType) + cg.Unbox(m.DeclaringType); + else + cg.Castclass(m.DeclaringType); + } + var getter = property.Getter == null ? null : _builder.DefineMethod(types.XamlIlTypes.Object, new[] {types.XamlIlTypes.Object}, name + "!Getter", false, true, false); if (getter != null) { - getter.Generator - .Ldarg_0() - .Castclass(property.Getter.DeclaringType) - .EmitCall(property.Getter); + Load(property.Getter, getter.Generator); + + getter.Generator.EmitCall(property.Getter); if (property.Getter.ReturnType.IsValueType) getter.Generator.Box(property.Getter.ReturnType); getter.Generator.Ret(); @@ -84,10 +78,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions name + "!Setter", false, true, false); if (setter != null) { - setter.Generator - .Ldarg_0() - .Castclass(property.Setter.DeclaringType) - .Ldarg(1); + Load(property.Setter, setter.Generator); + + setter.Generator.Ldarg(1); if (property.Setter.Parameters[0].IsValueType) setter.Generator.Unbox_Any(property.Setter.Parameters[0]); else diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github index 3ea474b672..9ae023a988 160000 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github @@ -1 +1 @@ -Subproject commit 3ea474b672ff63629300c36a9644caac2b74a1f2 +Subproject commit 9ae023a988d1b419e19386cfd3448d8e604bbc7e diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/PropertyPathGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/PropertyPathGrammar.cs index 89490db2aa..1f0f421233 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/PropertyPathGrammar.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/PropertyPathGrammar.cs @@ -64,6 +64,8 @@ namespace Avalonia.Markup.Parsers private static (State, ISyntax) ParseStart(ref CharacterReader r) { + if (TryParseCasts(ref r, out var rv)) + return rv; r.SkipWhitespace(); if (r.TakeIf('(')) @@ -130,19 +132,33 @@ namespace Avalonia.Markup.Parsers return (State.AfterProperty, new PropertySyntax {Name = prop.ToString()}); } + private static bool TryParseCasts(ref CharacterReader r, out (State, ISyntax) rv) + { + if (r.TakeIfKeyword(":=")) + rv = ParseEnsureType(ref r); + else if (r.TakeIfKeyword(":>") || r.TakeIfKeyword("as ")) + rv = ParseCastType(ref r); + else + { + rv = default; + return false; + } + return true; + } + private static (State, ISyntax) ParseAfterProperty(ref CharacterReader r) { + if (TryParseCasts(ref r, out var rv)) + return rv; + r.SkipWhitespace(); if (r.End) return (State.End, null); if (r.TakeIf('.')) return (State.Next, ChildTraversalSyntax.Instance); - if (r.TakeIfKeyword(":=")) - return ParseEnsureType(ref r); + - if (r.TakeIfKeyword(":>") || r.TakeIfKeyword("as ")) - return ParseCastType(ref r); throw new ExpressionParseException(r.Position, "Unexpected character " + r.PeekOneOrThrow + " after property name"); } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs index 913dd754f1..6bc7f33039 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs @@ -11,6 +11,7 @@ using Avalonia.Data.Core; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Media; +using Avalonia.Styling; using Avalonia.Threading; using Avalonia.UnitTests; using Avalonia.VisualTree; @@ -246,6 +247,53 @@ namespace Avalonia.Markup.Xaml.UnitTests />", typeof(XamlIlClassWithClrPropertyWithValue).Assembly); Assert.Equal(6, parsed.Count); } + + [Fact] + public void Should_Provide_PropertyPath_For_Setters() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var parsed = AvaloniaXamlLoader.Parse"); + var s1e = ((Setter)parsed.Setters[0]).PropertyPath.Elements; + var s2e = ((Setter)parsed.Setters[1]).PropertyPath.Elements; + var s3e = ((Setter)parsed.Setters[2]).PropertyPath.Elements; + + Assert.Equal(typeof(Visual), ((CastTypePropertyPathElement)s1e[0]).Type); + Assert.IsType(s1e[1]); + Assert.Equal("Bounds", ((AvaloniaProperty)((PropertyPropertyPathElement)s1e[2]).Property).Name); + Assert.IsType(s1e[3]); + var bottomRight = ((PropertyPropertyPathElement)s1e[4]).Property; + Assert.IsType(s1e[5]); + var pointX = ((PropertyPropertyPathElement)s1e[6]).Property; + + var orect = (object)(new Rect(100, 100, 200, 200)); + var point = bottomRight.Get(orect); + var x = pointX.Get(point); + Assert.Equal(300, (double)x); + + Assert.Equal(typeof(Visual), ((CastTypePropertyPathElement)s2e[0]).Type); + Assert.IsType(s2e[1]); + Assert.Equal("RenderTransform", ((AvaloniaProperty)((PropertyPropertyPathElement)s2e[2]).Property).Name); + Assert.Equal(typeof(ScaleTransform), ((EnsureTypePropertyPathElement)s2e[3]).Type); + Assert.IsType(s2e[4]); + Assert.Equal("ScaleX", ((AvaloniaProperty)((PropertyPropertyPathElement)s2e[5]).Property).Name); + + + var s3fqp = (AvaloniaProperty)((PropertyPropertyPathElement)s3e[0]).Property; + Assert.Equal("RenderTransform", s3fqp.Name); + Assert.Equal(typeof(Visual), s3fqp.OwnerType); + Assert.Equal(typeof(ScaleTransform), ((EnsureTypePropertyPathElement)s3e[1]).Type); + Assert.IsType(s3e[2]); + Assert.Equal("ScaleX", ((AvaloniaProperty)((PropertyPropertyPathElement)s3e[3]).Property).Name); + + + } + } } public class XamlIlBugTestsEventHandlerCodeBehind : Window From bc6502c0cd67415486339273bb738b1cebfb02ab Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 14 Jun 2019 14:34:32 +0300 Subject: [PATCH 004/693] Missing file --- src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj index 582e4499c5..1b4424a050 100644 --- a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj +++ b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj @@ -30,6 +30,9 @@ Markup/%(RecursiveDir)%(FileName)%(Extension) + + Markup/%(RecursiveDir)%(FileName)%(Extension) + Markup/%(RecursiveDir)%(FileName)%(Extension) @@ -38,11 +41,13 @@ Markup/%(RecursiveDir)%(FileName)%(Extension) - + Markup/%(RecursiveDir)%(FileName)%(Extension) - + + Markup/%(RecursiveDir)%(FileName)%(Extension) + From 6f158f463459c72731e6e382b5f9a043353c6002 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 14 Jun 2019 14:51:53 +0300 Subject: [PATCH 005/693] Do not crash --- src/Avalonia.Base/Utilities/KeywordParser.cs | 2 ++ .../Transformers/AvaloniaXamlIlPropertyPathTransformer.cs | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Utilities/KeywordParser.cs b/src/Avalonia.Base/Utilities/KeywordParser.cs index 34e1ea6f8e..16ef95f5f4 100644 --- a/src/Avalonia.Base/Utilities/KeywordParser.cs +++ b/src/Avalonia.Base/Utilities/KeywordParser.cs @@ -17,6 +17,8 @@ namespace Avalonia.Utilities var ws = r.PeekWhitespace(); var chars = r.TryPeek(ws.Length + keyword.Length); + if (chars.IsEmpty) + return -1; if (SpanEquals(chars.Slice(ws.Length), keyword.AsSpan())) return chars.Length; return -1; diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlPropertyPathTransformer.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlPropertyPathTransformer.cs index 99ba264214..1d2a4ea50b 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlPropertyPathTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlPropertyPathTransformer.cs @@ -17,7 +17,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers if (node is XamlIlAstXamlPropertyValueNode pv && pv.Values.Count == 1 && pv.Values[0] is XamlIlAstTextNode text - && pv.Property.GetClrProperty().Getter.ReturnType.Equals(context.GetAvaloniaTypes().PropertyPath) + && pv.Property.GetClrProperty().Getter?.ReturnType + .Equals(context.GetAvaloniaTypes().PropertyPath) == true ) { var parentScope = context.ParentNodes().OfType() From 30066c5e7093207e95022ae7c5620ea1a0ed0ec7 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sun, 23 Jun 2019 17:17:03 -0500 Subject: [PATCH 006/693] Convert current binding parsing to create an AST. --- .../Avalonia.Markup/Avalonia.Markup.csproj | 6 + .../Parsers/BindingExpressionGrammar.cs | 386 ++++++++++++++++++ .../Markup/Parsers/ExpressionParser.cs | 341 ++-------------- 3 files changed, 435 insertions(+), 298 deletions(-) create mode 100644 src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs diff --git a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj index 673eb51901..aca39dd8b0 100644 --- a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj +++ b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj @@ -3,6 +3,12 @@ netstandard2.0 Avalonia + + + + + + diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs new file mode 100644 index 0000000000..b8308155fe --- /dev/null +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs @@ -0,0 +1,386 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Avalonia.Data.Core; +using Avalonia.Markup.Parsers.Nodes; +using Avalonia.Utilities; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Avalonia.Markup.Parsers +{ + internal enum SourceMode + { + Data, + Control + } + internal static class BindingExpressionGrammar + { + public static (IList Nodes, SourceMode Mode) Parse(ref CharacterReader r) + { + var nodes = new List(); + var state = State.Start; + var mode = SourceMode.Data; + + while (!r.End && state != State.End) + { + switch (state) + { + case State.Start: + state = ParseStart(ref r, nodes); + break; + + case State.AfterMember: + state = ParseAfterMember(ref r, nodes); + break; + + case State.BeforeMember: + state = ParseBeforeMember(ref r, nodes); + break; + + case State.AttachedProperty: + state = ParseAttachedProperty(ref r, nodes); + break; + + case State.Indexer: + state = ParseIndexer(ref r, nodes); + break; + + case State.ElementName: + state = ParseElementName(ref r, nodes); + mode = SourceMode.Control; + break; + + case State.RelativeSource: + state = ParseRelativeSource(ref r, nodes); + mode = SourceMode.Control; + break; + } + } + + if (state == State.BeforeMember) + { + throw new ExpressionParseException(r.Position, "Unexpected end of expression."); + } + + return (nodes, mode); + } + + private static State ParseStart(ref CharacterReader r, IList nodes) + { + if (ParseNot(ref r)) + { + nodes.Add(new NotNode()); + return State.Start; + } + + else if (ParseSharp(ref r)) + { + return State.ElementName; + } + else if (ParseDollarSign(ref r)) + { + return State.RelativeSource; + } + else if (ParseOpenBrace(ref r)) + { + return State.AttachedProperty; + } + else if (PeekOpenBracket(ref r)) + { + return State.Indexer; + } + else if (ParseDot(ref r)) + { + nodes.Add(new EmptyExpressionNode()); + return State.End; + } + else + { + var identifier = r.ParseIdentifier(); + + if (!identifier.IsEmpty) + { + nodes.Add(new PropertyNameNode { PropertyName = identifier.ToString() }); + return State.AfterMember; + } + } + + return State.End; + } + + private static State ParseAfterMember(ref CharacterReader r, IList nodes) + { + if (ParseMemberAccessor(ref r)) + { + return State.BeforeMember; + } + else if (ParseStreamOperator(ref r)) + { + nodes.Add(new StreamNode()); + return State.AfterMember; + } + else if (PeekOpenBracket(ref r)) + { + return State.Indexer; + } + + return State.End; + } + + private static State ParseBeforeMember(ref CharacterReader r, IList nodes) + { + if (ParseOpenBrace(ref r)) + { + return State.AttachedProperty; + } + else + { + var identifier = r.ParseIdentifier(); + + if (!identifier.IsEmpty) + { + nodes.Add(new PropertyNameNode { PropertyName = identifier.ToString() }); + return State.AfterMember; + } + + return State.End; + } + } + + private static State ParseAttachedProperty(ref CharacterReader r, List nodes) + { + var (ns, owner) = ParseTypeName(ref r); + + if (r.End || !r.TakeIf('.')) + { + throw new ExpressionParseException(r.Position, "Invalid attached property name."); + } + + var name = r.ParseIdentifier(); + + if (r.End || !r.TakeIf(')')) + { + throw new ExpressionParseException(r.Position, "Expected ')'."); + } + + nodes.Add(new AttachedPropertyNameNode + { + Namespace = ns.ToString(), + TypeName = owner.ToString(), + PropertyName = name.ToString() + }); + return State.AfterMember; + } + + private static State ParseIndexer(ref CharacterReader r, List nodes) + { + var args = r.ParseArguments('[', ']'); + + if (args.Count == 0) + { + throw new ExpressionParseException(r.Position, "Indexer may not be empty."); + } + + nodes.Add(new IndexerNode { Arguments = args }); + return State.AfterMember; + } + + private static State ParseElementName(ref CharacterReader r, List nodes) + { + var name = r.ParseIdentifier(); + + if (name == null) + { + throw new ExpressionParseException(r.Position, "Element name expected after '#'."); + } + + nodes.Add(new NameNode { Name = name.ToString() }); + return State.AfterMember; + } + + private static State ParseRelativeSource(ref CharacterReader r, List nodes) + { + var mode = r.ParseIdentifier(); + + if (mode.Equals("self".AsSpan(), StringComparison.InvariantCulture)) + { + nodes.Add(new SelfNode()); + } + else if (mode.Equals("parent".AsSpan(), StringComparison.InvariantCulture)) + { + string ancestorNamespace = null; + string ancestorType = null; + var ancestorLevel = 0; + if (PeekOpenBracket(ref r)) + { + var args = r.ParseArguments('[', ']', ';'); + if (args.Count > 2 || args.Count == 0) + { + throw new ExpressionParseException(r.Position, "Too many arguments in RelativeSource syntax sugar"); + } + else if (args.Count == 1) + { + if (int.TryParse(args[0], out int level)) + { + ancestorType = null; + ancestorLevel = level; + } + else + { + var reader = new CharacterReader(args[0].AsSpan()); + (ancestorNamespace, ancestorType) = ParseTypeName(ref reader); + } + } + else + { + var reader = new CharacterReader(args[0].AsSpan()); + (ancestorNamespace, ancestorType) = ParseTypeName(ref reader); + ancestorLevel = int.Parse(args[1]); + } + } + nodes.Add(new AncestorNode + { + Namespace = ancestorNamespace, + TypeName = ancestorType, + Level = ancestorLevel + }); + } + else + { + throw new ExpressionParseException(r.Position, "Unknown RelativeSource mode."); + } + + return State.AfterMember; + } + + private static TypeName ParseTypeName(ref CharacterReader r) + { + ReadOnlySpan ns, typeName; + ns = ReadOnlySpan.Empty; + var typeNameOrNamespace = r.ParseIdentifier(); + + if (!r.End && r.TakeIf(':')) + { + ns = typeNameOrNamespace; + typeName = r.ParseIdentifier(); + } + else + { + typeName = typeNameOrNamespace; + } + + return new TypeName(ns, typeName); + } + + private static bool ParseNot(ref CharacterReader r) + { + return !r.End && r.TakeIf('!'); + } + + private static bool ParseMemberAccessor(ref CharacterReader r) + { + return !r.End && r.TakeIf('.'); + } + + private static bool ParseOpenBrace(ref CharacterReader r) + { + return !r.End && r.TakeIf('('); + } + + private static bool PeekOpenBracket(ref CharacterReader r) + { + return !r.End && r.PeekOneOrThrow == '['; + } + + private static bool ParseStreamOperator(ref CharacterReader r) + { + return !r.End && r.TakeIf('^'); + } + + private static bool ParseDollarSign(ref CharacterReader r) + { + return !r.End && r.TakeIf('$'); + } + + private static bool ParseSharp(ref CharacterReader r) + { + return !r.End && r.TakeIf('#'); + } + + private static bool ParseDot(ref CharacterReader r) + { + return !r.End && r.TakeIf('.'); + } + + private enum State + { + Start, + RelativeSource, + ElementName, + AfterMember, + BeforeMember, + AttachedProperty, + Indexer, + End, + } + + private readonly ref struct TypeName + { + public TypeName(ReadOnlySpan ns, ReadOnlySpan typeName) + { + Namespace = ns; + Type = typeName; + } + + public readonly ReadOnlySpan Namespace; + public readonly ReadOnlySpan Type; + + public void Deconstruct(out string ns, out string typeName) + { + ns = Namespace.ToString(); + typeName = Type.ToString(); + } + } + + public interface INode {} + + public class EmptyExpressionNode : INode { } + + public class PropertyNameNode : INode + { + public string PropertyName { get; set; } + } + + public class AttachedPropertyNameNode : INode + { + public string Namespace { get; set; } + public string TypeName { get; set; } + public string PropertyName { get; set; } + } + + public class IndexerNode : INode + { + public IList Arguments { get; set; } + } + + public class NotNode : INode {} + + public class StreamNode : INode {} + + public class SelfNode : INode {} + + public class NameNode : INode + { + public string Name { get; set; } + } + + public class AncestorNode : INode + { + public string Namespace { get; set; } + public string TypeName { get; set; } + public int Level { get; set; } + } + } +} diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs index 2a737d36e9..fe58ac139e 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs @@ -10,12 +10,6 @@ using System.Linq; namespace Avalonia.Markup.Parsers { - internal enum SourceMode - { - Data, - Control - } - internal class ExpressionParser { private readonly bool _enableValidation; @@ -29,332 +23,83 @@ namespace Avalonia.Markup.Parsers public (ExpressionNode Node, SourceMode Mode) Parse(ref CharacterReader r) { - var nodes = new List(); - var state = State.Start; - var mode = SourceMode.Data; + ExpressionNode node = null; + var (astNodes, mode) = BindingExpressionGrammar.Parse(ref r); - while (!r.End && state != State.End) + foreach (var astNode in astNodes) { - switch (state) + ExpressionNode nextNode = null; + switch (astNode) { - case State.Start: - state = ParseStart(ref r, nodes); + case BindingExpressionGrammar.EmptyExpressionNode _: + nextNode = new EmptyExpressionNode(); break; - - case State.AfterMember: - state = ParseAfterMember(ref r, nodes); + case BindingExpressionGrammar.NotNode _: + nextNode = new LogicalNotNode(); break; - - case State.BeforeMember: - state = ParseBeforeMember(ref r, nodes); + case BindingExpressionGrammar.StreamNode _: + nextNode = new StreamNode(); break; - - case State.AttachedProperty: - state = ParseAttachedProperty(ref r, nodes); + case BindingExpressionGrammar.PropertyNameNode propName: + nextNode = new PropertyAccessorNode(propName.PropertyName, _enableValidation); break; - - case State.Indexer: - state = ParseIndexer(ref r, nodes); + case BindingExpressionGrammar.IndexerNode indexer: + nextNode = new StringIndexerNode(indexer.Arguments); break; - - case State.ElementName: - state = ParseElementName(ref r, nodes); - mode = SourceMode.Control; + case BindingExpressionGrammar.AttachedPropertyNameNode attachedProp: + nextNode = ParseAttachedProperty(attachedProp); break; - - case State.RelativeSource: - state = ParseRelativeSource(ref r, nodes); - mode = SourceMode.Control; + case BindingExpressionGrammar.SelfNode _: + nextNode = new SelfNode(); + break; + case BindingExpressionGrammar.AncestorNode ancestor: + nextNode = ParseFindAncestor(ancestor); + break; + case BindingExpressionGrammar.NameNode elementName: + nextNode = new ElementNameNode(elementName.Name); break; } - } - - if (state == State.BeforeMember) - { - throw new ExpressionParseException(r.Position, "Unexpected end of expression."); - } - - for (int n = 0; n < nodes.Count - 1; ++n) - { - nodes[n].Next = nodes[n + 1]; - } - - return (nodes.FirstOrDefault(), mode); - } - - private State ParseStart(ref CharacterReader r, IList nodes) - { - if (ParseNot(ref r)) - { - nodes.Add(new LogicalNotNode()); - return State.Start; - } - - else if (ParseSharp(ref r)) - { - return State.ElementName; - } - else if (ParseDollarSign(ref r)) - { - return State.RelativeSource; - } - else if (ParseOpenBrace(ref r)) - { - return State.AttachedProperty; - } - else if (PeekOpenBracket(ref r)) - { - return State.Indexer; - } - else if (ParseDot(ref r)) - { - nodes.Add(new EmptyExpressionNode()); - return State.End; - } - else - { - var identifier = r.ParseIdentifier(); - - if (!identifier.IsEmpty) + if (node is null) { - nodes.Add(new PropertyAccessorNode(identifier.ToString(), _enableValidation)); - return State.AfterMember; + node = nextNode; } - } - - return State.End; - } - - private static State ParseAfterMember(ref CharacterReader r, IList nodes) - { - if (ParseMemberAccessor(ref r)) - { - return State.BeforeMember; - } - else if (ParseStreamOperator(ref r)) - { - nodes.Add(new StreamNode()); - return State.AfterMember; - } - else if (PeekOpenBracket(ref r)) - { - return State.Indexer; - } - - return State.End; - } - - private State ParseBeforeMember(ref CharacterReader r, IList nodes) - { - if (ParseOpenBrace(ref r)) - { - return State.AttachedProperty; - } - else - { - var identifier = r.ParseIdentifier(); - - if (!identifier.IsEmpty) + else { - nodes.Add(new PropertyAccessorNode(identifier.ToString(), _enableValidation)); - return State.AfterMember; + node.Next = nextNode; } - - return State.End; - } - } - - private State ParseAttachedProperty(ref CharacterReader r, List nodes) - { - var (ns, owner) = ParseTypeName(ref r); - - if (r.End || !r.TakeIf('.')) - { - throw new ExpressionParseException(r.Position, "Invalid attached property name."); - } - - var name = r.ParseIdentifier(); - - if (r.End || !r.TakeIf(')')) - { - throw new ExpressionParseException(r.Position, "Expected ')'."); - } - - if (_typeResolver == null) - { - throw new InvalidOperationException("Cannot parse a binding path with an attached property without a type resolver. Maybe you can use a LINQ Expression binding path instead?"); - } - - var property = AvaloniaPropertyRegistry.Instance.FindRegistered(_typeResolver(ns.ToString(), owner.ToString()), name.ToString()); - - nodes.Add(new AvaloniaPropertyAccessorNode(property, _enableValidation)); - return State.AfterMember; - } - - private State ParseIndexer(ref CharacterReader r, List nodes) - { - var args = r.ParseArguments('[', ']'); - - if (args.Count == 0) - { - throw new ExpressionParseException(r.Position, "Indexer may not be empty."); } - nodes.Add(new StringIndexerNode(args)); - return State.AfterMember; + return (node, mode); } - private State ParseElementName(ref CharacterReader r, List nodes) + private FindAncestorNode ParseFindAncestor(BindingExpressionGrammar.AncestorNode node) { - var name = r.ParseIdentifier(); + Type ancestorType = null; + var ancestorLevel = node.Level; - if (name == null) + if (!(node.Namespace is null) && !(node.TypeName is null)) { - throw new ExpressionParseException(r.Position, "Element name expected after '#'."); - } - - nodes.Add(new ElementNameNode(name.ToString())); - return State.AfterMember; - } - - private State ParseRelativeSource(ref CharacterReader r, List nodes) - { - var mode = r.ParseIdentifier(); - - if (mode.Equals("self".AsSpan(), StringComparison.InvariantCulture)) - { - nodes.Add(new SelfNode()); - } - else if (mode.Equals("parent".AsSpan(), StringComparison.InvariantCulture)) - { - Type ancestorType = null; - var ancestorLevel = 0; - if (PeekOpenBracket(ref r)) + if (_typeResolver == null) { - var args = r.ParseArguments('[', ']', ';'); - if (args.Count > 2 || args.Count == 0) - { - throw new ExpressionParseException(r.Position, "Too many arguments in RelativeSource syntax sugar"); - } - else if (args.Count == 1) - { - if (int.TryParse(args[0], out int level)) - { - ancestorType = null; - ancestorLevel = level; - } - else - { - var reader = new CharacterReader(args[0].AsSpan()); - var typeName = ParseTypeName(ref reader); - ancestorType = _typeResolver(typeName.Namespace.ToString(), typeName.Type.ToString()); - } - } - else - { - var reader = new CharacterReader(args[0].AsSpan()); - var typeName = ParseTypeName(ref reader); - ancestorType = _typeResolver(typeName.Namespace.ToString(), typeName.Type.ToString()); - ancestorLevel = int.Parse(args[1]); - } + throw new InvalidOperationException("Cannot parse a binding path with a typed FindAncestor without a type resolver. Maybe you can use a LINQ Expression binding path instead?"); } - nodes.Add(new FindAncestorNode(ancestorType, ancestorLevel)); - } - else - { - throw new ExpressionParseException(r.Position, "Unknown RelativeSource mode."); - } - return State.AfterMember; - } - - private static TypeName ParseTypeName(ref CharacterReader r) - { - ReadOnlySpan ns, typeName; - ns = ReadOnlySpan.Empty; - var typeNameOrNamespace = r.ParseIdentifier(); - - if (!r.End && r.TakeIf(':')) - { - ns = typeNameOrNamespace; - typeName = r.ParseIdentifier(); + ancestorType = _typeResolver(node.Namespace, node.TypeName); } - else - { - typeName = typeNameOrNamespace; - } - - return new TypeName(ns, typeName); - } - - private static bool ParseNot(ref CharacterReader r) - { - return !r.End && r.TakeIf('!'); - } - - private static bool ParseMemberAccessor(ref CharacterReader r) - { - return !r.End && r.TakeIf('.'); - } - - private static bool ParseOpenBrace(ref CharacterReader r) - { - return !r.End && r.TakeIf('('); - } - - private static bool PeekOpenBracket(ref CharacterReader r) - { - return !r.End && r.PeekOneOrThrow == '['; - } - - private static bool ParseStreamOperator(ref CharacterReader r) - { - return !r.End && r.TakeIf('^'); - } - - private static bool ParseDollarSign(ref CharacterReader r) - { - return !r.End && r.TakeIf('$'); - } - - private static bool ParseSharp(ref CharacterReader r) - { - return !r.End && r.TakeIf('#'); - } - private static bool ParseDot(ref CharacterReader r) - { - return !r.End && r.TakeIf('.'); - } - - private enum State - { - Start, - RelativeSource, - ElementName, - AfterMember, - BeforeMember, - AttachedProperty, - Indexer, - End, + return new FindAncestorNode(ancestorType, ancestorLevel); } - private readonly ref struct TypeName + private AvaloniaPropertyAccessorNode ParseAttachedProperty(BindingExpressionGrammar.AttachedPropertyNameNode node) { - public TypeName(ReadOnlySpan ns, ReadOnlySpan typeName) + if (_typeResolver == null) { - Namespace = ns; - Type = typeName; + throw new InvalidOperationException("Cannot parse a binding path with an attached property without a type resolver. Maybe you can use a LINQ Expression binding path instead?"); } - public readonly ReadOnlySpan Namespace; - public readonly ReadOnlySpan Type; + var property = AvaloniaPropertyRegistry.Instance.FindRegistered(_typeResolver(node.Namespace, node.TypeName), node.PropertyName); - public void Deconstruct(out ReadOnlySpan ns, out ReadOnlySpan typeName) - { - ns = Namespace; - typeName = Type; - } + return new AvaloniaPropertyAccessorNode(property, _enableValidation); } } } From 1c380dc375a903806dced234f966f0ccfa2d8df4 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sun, 23 Jun 2019 23:14:52 -0500 Subject: [PATCH 007/693] Add new AST node generated from x:DataContextType. To be used as part of compiled bindings. --- .../Avalonia.Markup.Xaml.csproj | 3 +- .../AvaloniaXamlIlCompiler.cs | 3 +- ...valoniaXamlIlDataContextTypeTransformer.cs | 46 +++++++++++++++++++ .../AvaloniaXamlIlMetadataRemover.cs | 7 ++- 4 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index 040ed57383..dfa036b1f2 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -43,9 +43,10 @@ + - + diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs index eadad60cde..933d39bad8 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -48,7 +48,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions new AvaloniaXamlIlSetterTransformer(), new AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer(), new AvaloniaXamlIlConstructorServiceProviderTransformer(), - new AvaloniaXamlIlTransitionsTypeMetadataTransformer() + new AvaloniaXamlIlTransitionsTypeMetadataTransformer(), + new AvaloniaXamlIlDataContextTypeTransformer() ); // After everything else diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs new file mode 100644 index 0000000000..5d68a72514 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Text; +using XamlIl; +using XamlIl.Ast; +using XamlIl.Transform; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers +{ + class AvaloniaXamlIlDataContextTypeTransformer : IXamlIlAstTransformer + { + public IXamlIlAstNode Transform(XamlIlAstTransformationContext context, IXamlIlAstNode node) + { + if (node is XamlIlAstObjectNode on) + { + foreach (var child in on.Children) + { + if (child is XamlIlAstXmlDirective directive) + { + if (directive.Namespace == XamlNamespaces.Xaml2006 + && directive.Name == "DataContextType" + && directive.Values.Count == 1 + && directive.Values[0] is XamlIlTypeExtensionNode dataContextType) + { + on.Children.Remove(child); + return new AvaloniaXamlIlDataContextTypeMetadataNode(on, dataContextType.Value); + } + } + } + } + + return node; + } + } + + class AvaloniaXamlIlDataContextTypeMetadataNode : XamlIlValueWithSideEffectNodeBase + { + public IXamlIlAstTypeReference DataContextType { get; set; } + + public AvaloniaXamlIlDataContextTypeMetadataNode(IXamlIlAstValueNode value, IXamlIlAstTypeReference targetType) + : base(value, value) + { + DataContextType = targetType; + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlMetadataRemover.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlMetadataRemover.cs index 994928b7fe..608e68bf10 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlMetadataRemover.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlMetadataRemover.cs @@ -8,8 +8,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { public IXamlIlAstNode Transform(XamlIlAstTransformationContext context, IXamlIlAstNode node) { - if (node is AvaloniaXamlIlTargetTypeMetadataNode md) - return md.Value; + if (node is AvaloniaXamlIlTargetTypeMetadataNode targetTypeMetadata) + return targetTypeMetadata.Value; + + if (node is AvaloniaXamlIlDataContextTypeMetadataNode dataContextTypeMetadata) + return dataContextTypeMetadata.Value; return node; } From bb2c563416af1132039ec8c1da09e4228eb81935 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 28 Jun 2019 01:24:01 +0300 Subject: [PATCH 008/693] Headless platform WIP --- Avalonia.sln | 52 +++ .../ControlCatalog.NetCore.csproj | 3 +- samples/ControlCatalog.NetCore/Program.cs | 5 + .../Avalonia.Headless.Vnc.csproj | 12 + .../HeadlessVncFramebufferSource.cs | 96 +++++ .../HeadlessVncPlatformExtensions.cs | 48 +++ .../Avalonia.Headless.csproj | 8 + .../AvaloniaHeadlessPlatform.cs | 93 +++++ .../HeadlessPlatformRenderInterface.cs | 385 ++++++++++++++++++ .../HeadlessPlatformStubs.cs | 96 +++++ .../HeadlessPlatformThreadingInterface.cs | 86 ++++ src/Avalonia.Headless/HeadlessWindowImpl.cs | 251 ++++++++++++ src/Avalonia.Headless/IHeadlessWindow.cs | 17 + 13 files changed, 1151 insertions(+), 1 deletion(-) create mode 100644 src/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj create mode 100644 src/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs create mode 100644 src/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs create mode 100644 src/Avalonia.Headless/Avalonia.Headless.csproj create mode 100644 src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs create mode 100644 src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs create mode 100644 src/Avalonia.Headless/HeadlessPlatformStubs.cs create mode 100644 src/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs create mode 100644 src/Avalonia.Headless/HeadlessWindowImpl.cs create mode 100644 src/Avalonia.Headless/IHeadlessWindow.cs diff --git a/Avalonia.sln b/Avalonia.sln index ac678ba9ba..16fc4d0121 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -199,6 +199,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI.UnitTes EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Controls.DataGrid", "src\Avalonia.Controls.DataGrid\Avalonia.Controls.DataGrid.csproj", "{3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless", "src\Avalonia.Headless\Avalonia.Headless.csproj", "{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.Vnc", "src\Avalonia.Headless.Vnc\Avalonia.Headless.Vnc.csproj", "{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13 @@ -1842,6 +1846,54 @@ Global {3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Release|iPhone.Build.0 = Release|Any CPU {3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.AppStore|iPhone.Build.0 = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Debug|iPhone.Build.0 = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Release|Any CPU.Build.0 = Release|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Release|iPhone.ActiveCfg = Release|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Release|iPhone.Build.0 = Release|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.AppStore|iPhone.Build.0 = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Debug|iPhone.Build.0 = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Release|Any CPU.Build.0 = Release|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Release|iPhone.ActiveCfg = Release|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Release|iPhone.Build.0 = Release|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Release|iPhoneSimulator.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj index 6a40f7187d..d9ce776ffd 100644 --- a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj +++ b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj @@ -7,11 +7,12 @@ + - + diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index 40321496c0..91f05ca719 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -6,6 +6,7 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Skia; using Avalonia.ReactiveUI; +using Avalonia.Threading; namespace ControlCatalog.NetCore { @@ -32,6 +33,10 @@ namespace ControlCatalog.NetCore System.Threading.ThreadPool.QueueUserWorkItem(_ => ConsoleSilencer()); return builder.StartLinuxFramebuffer(args); } + else if (args.Contains("--vnc")) + { + return builder.StartWithHeadlessVncPlatform(null, 5901, args, ShutdownMode.OnMainWindowClose); + } else return builder.StartWithClassicDesktopLifetime(args); } diff --git a/src/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj b/src/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj new file mode 100644 index 0000000000..4aac862ae8 --- /dev/null +++ b/src/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj @@ -0,0 +1,12 @@ + + + + netstandard2.0 + + + + + + + + diff --git a/src/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs b/src/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs new file mode 100644 index 0000000000..df68807195 --- /dev/null +++ b/src/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs @@ -0,0 +1,96 @@ +using System; +using System.Runtime.InteropServices; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Threading; +using RemoteViewing.Vnc; +using RemoteViewing.Vnc.Server; + +namespace Avalonia.Headless.Vnc +{ + public class HeadlessVncFramebufferSource : IVncFramebufferSource + { + public IHeadlessWindow Window { get; set; } + private object _lock = new object(); + public VncFramebuffer _framebuffer = new VncFramebuffer("Avalonia", 1, 1, VncPixelFormat.RGB32); + + private VncButton _previousButtons; + public HeadlessVncFramebufferSource(VncServerSession session, Window window) + { + Window = (IHeadlessWindow)window.PlatformImpl; + session.PointerChanged += (_, args) => + { + var pt = new Point(args.X, args.Y); + + var buttons = (VncButton)args.PressedButtons; + + int TranslateButton(VncButton vncButton) => + vncButton == VncButton.Left ? 0 : vncButton == VncButton.Right ? 1 : 2; + + var modifiers = (InputModifiers)(((int)buttons & 7) << 4); + + Dispatcher.UIThread.Post(() => + { + Window?.MouseMove(pt); + foreach (var btn in CheckedButtons) + if (_previousButtons.HasFlag(btn) && !buttons.HasFlag(btn)) + Window?.MouseUp(pt, TranslateButton(btn), modifiers); + + foreach (var btn in CheckedButtons) + if (!_previousButtons.HasFlag(btn) && buttons.HasFlag(btn)) + Window?.MouseDown(pt, TranslateButton(btn), modifiers); + _previousButtons = buttons; + }, DispatcherPriority.Input); + }; + + } + + [Flags] + enum VncButton + { + Left = 1, + Middle = 2, + Right = 4, + ScrollUp = 8, + ScrollDown = 16 + } + + + private static VncButton[] CheckedButtons = new[] {VncButton.Left, VncButton.Middle, VncButton.Right}; + + public VncFramebuffer Capture() + { + lock (_lock) + { + using (var bmpRef = Window.GetLastRenderedFrame()) + { + if (bmpRef?.Item == null) + return _framebuffer; + var bmp = bmpRef.Item; + if (bmp.PixelSize.Width != _framebuffer.Width || bmp.PixelSize.Height != _framebuffer.Height) + { + _framebuffer = new VncFramebuffer("Avalonia", bmp.PixelSize.Width, bmp.PixelSize.Height, + VncPixelFormat.RGB32); + } + + using (var fb = bmp.Lock()) + { + var buf = _framebuffer.GetBuffer(); + if (_framebuffer.Stride == fb.RowBytes) + Marshal.Copy(fb.Address, buf, 0, buf.Length); + else + for (var y = 0; y < fb.Size.Height; y++) + { + var sourceStart = fb.RowBytes * y; + var dstStart = _framebuffer.Stride * y; + var row = fb.Size.Width * 4; + Marshal.Copy(new IntPtr(sourceStart + fb.Address.ToInt64()), buf, dstStart, row); + } + } + } + } + + return _framebuffer; + } + } +} diff --git a/src/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs b/src/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs new file mode 100644 index 0000000000..bd215296c8 --- /dev/null +++ b/src/Avalonia.Headless.Vnc/HeadlessVncPlatformExtensions.cs @@ -0,0 +1,48 @@ +using System.Net; +using System.Net.Sockets; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Headless; +using Avalonia.Headless.Vnc; +using RemoteViewing.Vnc; +using RemoteViewing.Vnc.Server; + +namespace Avalonia +{ + public static class HeadlessVncPlatformExtensions + { + public static int StartWithHeadlessVncPlatform( + this T builder, + string host, int port, + string[] args, ShutdownMode shutdownMode = ShutdownMode.OnLastWindowClose) + where T : AppBuilderBase, new() + { + var tcpServer = new TcpListener(host == null ? IPAddress.Loopback : IPAddress.Parse(host), port); + tcpServer.Start(); + return builder + .UseHeadless(false) + .AfterSetup(_ => + { + var lt = ((IClassicDesktopStyleApplicationLifetime)builder.Instance.ApplicationLifetime); + lt.Startup += async delegate + { + while (true) + { + var client = await tcpServer.AcceptTcpClientAsync(); + var options = new VncServerSessionOptions + { + AuthenticationMethod = AuthenticationMethod.None + }; + var session = new VncServerSession(); + + session.SetFramebufferSource(new HeadlessVncFramebufferSource( + session, lt.MainWindow)); + session.Connect(client.GetStream(), options); + } + + }; + }) + .StartWithClassicDesktopLifetime(args, shutdownMode); + } + } +} diff --git a/src/Avalonia.Headless/Avalonia.Headless.csproj b/src/Avalonia.Headless/Avalonia.Headless.csproj new file mode 100644 index 0000000000..b871316a2c --- /dev/null +++ b/src/Avalonia.Headless/Avalonia.Headless.csproj @@ -0,0 +1,8 @@ + + + netstandard2.0 + + + + + diff --git a/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs new file mode 100644 index 0000000000..5857d94182 --- /dev/null +++ b/src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs @@ -0,0 +1,93 @@ +using System; +using System.Diagnostics; +using System.Reactive.Disposables; +using Avalonia.Controls; +using Avalonia.Controls.Platform; +using Avalonia.Input; +using Avalonia.Input.Platform; +using Avalonia.Platform; +using Avalonia.Rendering; +using Avalonia.Threading; + +namespace Avalonia.Headless +{ + public static class AvaloniaHeadlessPlatform + { + class RenderTimer : DefaultRenderTimer + { + private readonly int _framesPerSecond; + private Action _forceTick; + protected override IDisposable StartCore(Action tick) + { + bool cancelled = false; + var st = Stopwatch.StartNew(); + _forceTick = () => tick(st.Elapsed); + DispatcherTimer.Run(() => + { + if (cancelled) + return false; + tick(st.Elapsed); + return !cancelled; + }, TimeSpan.FromSeconds(1.0 / _framesPerSecond), DispatcherPriority.Render); + return Disposable.Create(() => + { + _forceTick = null; + cancelled = true; + }); + } + + public RenderTimer(int framesPerSecond) : base(framesPerSecond) + { + _framesPerSecond = framesPerSecond; + } + + public void ForceTick() => _forceTick?.Invoke(); + } + + class HeadlessWindowingPlatform : IWindowingPlatform + { + public IWindowImpl CreateWindow() => new HeadlessWindowImpl(false); + + public IEmbeddableWindowImpl CreateEmbeddableWindow() => throw new PlatformNotSupportedException(); + + public IPopupImpl CreatePopup() => new HeadlessWindowImpl(true); + } + + internal static void Initialize() + { + AvaloniaLocator.CurrentMutable + .Bind().ToConstant(new HeadlessPlatformThreadingInterface()) + .Bind().ToSingleton() + .Bind().ToSingleton() + .Bind().ToConstant(new HeadlessPlatformSettingsStub()) + .Bind().ToSingleton() + .Bind().ToSingleton() + .Bind().ToConstant(new KeyboardDevice()) + .Bind().ToConstant(new RenderLoop()) + .Bind().ToConstant(new RenderTimer(60)) + + .Bind().ToConstant(new HeadlessWindowingPlatform()) + .Bind().ToSingleton(); + } + + + public static void ForceRenderTimerTick(int count = 1) + { + var timer = AvaloniaLocator.Current.GetService() as RenderTimer; + for (var c = 0; c < count; c++) + timer?.ForceTick(); + + } + } + + public static class AvaloniaHeadlessPlatformExtensions + { + public static T UseHeadless(this T builder, bool headlessDrawing = true) + where T : AppBuilderBase, new() + { + if (headlessDrawing) + builder.UseRenderingSubsystem(HeadlessPlatformRenderInterface.Initialize, "Headless"); + return builder.UseWindowingSubsystem(AvaloniaHeadlessPlatform.Initialize, "Headless"); + } + } +} diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs new file mode 100644 index 0000000000..381eccef08 --- /dev/null +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -0,0 +1,385 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Rendering; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Utilities; +using Avalonia.Visuals.Media.Imaging; + +namespace Avalonia.Headless +{ + internal class HeadlessPlatformRenderInterface : IPlatformRenderInterface + { + public static void Initialize() + { + AvaloniaLocator.CurrentMutable + .Bind().ToConstant(new HeadlessPlatformRenderInterface()); + } + + public IEnumerable InstalledFontNames { get; } = new[] {"Tahoma"}; + + public IFormattedTextImpl CreateFormattedText(string text, Typeface typeface, TextAlignment textAlignment, + TextWrapping wrapping, Size constraint, IReadOnlyList spans) + { + return new HeadlessFormattedTextStub(text, constraint); + } + + public IGeometryImpl CreateEllipseGeometry(Rect rect) => new HeadlessGeometryStub(rect); + + public IGeometryImpl CreateLineGeometry(Point p1, Point p2) + { + var tl = new Point(Math.Min(p1.X, p2.X), Math.Min(p1.Y, p2.Y)); + var br = new Point(Math.Max(p1.X, p2.X), Math.Max(p1.Y, p2.Y)); + return new HeadlessGeometryStub(new Rect(tl, br)); + } + + public IGeometryImpl CreateRectangleGeometry(Rect rect) + { + return new HeadlessGeometryStub(rect); + } + + public IStreamGeometryImpl CreateStreamGeometry() => new HeadlessStreamingGeometryStub(); + + public IRenderTarget CreateRenderTarget(IEnumerable surfaces) => new HeadlessRenderTarget(); + + public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi) + { + return new HeadlessBitmapStub(size, dpi); + } + + public IWriteableBitmapImpl CreateWriteableBitmap(PixelSize size, Vector dpi, PixelFormat? format = null) + { + return new HeadlessBitmapStub(size, dpi); + } + + public IBitmapImpl LoadBitmap(string fileName) + { + return new HeadlessBitmapStub(new Size(1, 1), new Vector(96, 96)); + } + + public IBitmapImpl LoadBitmap(Stream stream) + { + return new HeadlessBitmapStub(new Size(1, 1), new Vector(96, 96)); + } + + public IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, PixelSize size, Vector dpi, int stride) + { + return new HeadlessBitmapStub(new Size(1, 1), new Vector(96, 96)); + } + + class HeadlessGeometryStub : IGeometryImpl + { + public HeadlessGeometryStub(Rect bounds) + { + Bounds = bounds; + } + public Rect Bounds { get; set; } + public virtual Rect GetRenderBounds(Pen pen) => Bounds.Inflate(pen.Thickness / 2); + public virtual bool FillContains(Point point) => Bounds.Contains(point); + + public IGeometryImpl Intersect(IGeometryImpl geometry) + => new HeadlessGeometryStub(geometry.Bounds.Intersect(Bounds)); + + public bool StrokeContains(Pen pen, Point point) => false; + + public ITransformedGeometryImpl WithTransform(Matrix transform) => + new HeadlessTransformedGeometryStub(this, transform); + } + + class HeadlessTransformedGeometryStub : HeadlessGeometryStub, ITransformedGeometryImpl + { + public HeadlessTransformedGeometryStub(IGeometryImpl b, Matrix transform) : this(Fix(b, transform)) + { + + } + + static (IGeometryImpl, Matrix, Rect) Fix(IGeometryImpl b, Matrix transform) + { + if (b is HeadlessTransformedGeometryStub transformed) + { + b = transformed.SourceGeometry; + transform = transformed.Transform * transform; + } + + return (b, transform, b.Bounds.TransformToAABB(transform)); + } + + private HeadlessTransformedGeometryStub((IGeometryImpl b, Matrix transform, Rect bounds) fix) : base(fix.bounds) + { + SourceGeometry = fix.b; + Transform = fix.transform; + } + + + public IGeometryImpl SourceGeometry { get; } + public Matrix Transform { get; } + } + + class HeadlessStreamingGeometryStub : HeadlessGeometryStub, IStreamGeometryImpl + { + public HeadlessStreamingGeometryStub() : base(Rect.Empty) + { + } + + public IStreamGeometryImpl Clone() + { + return this; + } + + public IStreamGeometryContextImpl Open() + { + return new HeadlessStreamingGeometryContextStub(this); + } + + class HeadlessStreamingGeometryContextStub : IStreamGeometryContextImpl + { + private readonly HeadlessStreamingGeometryStub _parent; + private double _x1, _y1, _x2, _y2; + public HeadlessStreamingGeometryContextStub(HeadlessStreamingGeometryStub parent) + { + _parent = parent; + } + + void Track(Point pt) + { + if (_x1 > pt.X) + _x1 = pt.X; + if (_x2 < pt.X) + _x2 = pt.X; + if (_y1 > pt.Y) + _y1 = pt.Y; + if (_y2 < pt.Y) + _y2 = pt.Y; + } + + public void Dispose() + { + _parent.Bounds = new Rect(_x1, _y1, _x2 - _x1, _y2 - _y1); + } + + public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection) + => Track(point); + + public void BeginFigure(Point startPoint, bool isFilled = true) => Track(startPoint); + + public void CubicBezierTo(Point point1, Point point2, Point point3) + { + Track(point1); + Track(point2); + Track(point3); + } + + public void QuadraticBezierTo(Point control, Point endPoint) + { + Track(control); + Track(endPoint); + } + + public void LineTo(Point point) => Track(point); + + public void EndFigure(bool isClosed) + { + Dispose(); + } + + public void SetFillRule(FillRule fillRule) + { + + } + } + } + + class HeadlessBitmapStub : IBitmapImpl, IRenderTargetBitmapImpl, IWriteableBitmapImpl + { + public Size Size { get; } + + public HeadlessBitmapStub(Size size, Vector dpi) + { + Size = size; + Dpi = dpi; + var pixel = Size * (Dpi / 96); + PixelSize = new PixelSize(Math.Max(1, (int)pixel.Width), Math.Max(1, (int)pixel.Height)); + } + + public HeadlessBitmapStub(PixelSize size, Vector dpi) + { + PixelSize = size; + Dpi = dpi; + Size = PixelSize.ToSizeWithDpi(dpi); + } + + public void Dispose() + { + + } + + public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + { + return new HeadlessDrawingContextStub(); + } + + public Vector Dpi { get; } + public PixelSize PixelSize { get; } + public int Version { get; set; } + public void Save(string fileName) + { + + } + + public void Save(Stream stream) + { + + } + + public ILockedFramebuffer Lock() + { + Version++; + var mem = Marshal.AllocHGlobal(PixelSize.Width * PixelSize.Height * 4); + return new LockedFramebuffer(mem, PixelSize, PixelSize.Width * 4, Dpi, PixelFormat.Rgba8888, + () => Marshal.FreeHGlobal(mem)); + } + } + + class HeadlessDrawingContextStub : IDrawingContextImpl + { + public void Dispose() + { + + } + + public Matrix Transform { get; set; } + public void Clear(Color color) + { + + } + + public void DrawImage(IRef source, double opacity, Rect sourceRect, Rect destRect, + BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) + { + + } + + public void DrawImage(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) + { + + } + + public void DrawLine(Pen pen, Point p1, Point p2) + { + + } + + public void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry) + { + + } + + public void DrawRectangle(Pen pen, Rect rect, float cornerRadius = 0) + { + + } + + public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text) + { + + } + + public void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0) + { + + } + + public IRenderTargetBitmapImpl CreateLayer(Size size) + { + return new HeadlessBitmapStub(size, new Vector(96, 96)); + } + + public void PushClip(Rect clip) + { + + } + + public void PopClip() + { + + } + + public void PushOpacity(double opacity) + { + + } + + public void PopOpacity() + { + + } + + public void PushOpacityMask(IBrush mask, Rect bounds) + { + + } + + public void PopOpacityMask() + { + + } + + public void PushGeometryClip(IGeometryImpl clip) + { + + } + + public void PopGeometryClip() + { + + } + + public void Custom(ICustomDrawOperation custom) + { + + } + } + + class HeadlessRenderTarget : IRenderTarget + { + public void Dispose() + { + + } + + public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + { + return new HeadlessDrawingContextStub(); + } + } + + class HeadlessFormattedTextStub : IFormattedTextImpl + { + public HeadlessFormattedTextStub(string text, Size constraint) + { + Text = text; + Constraint = constraint; + Bounds = new Rect(Constraint.Constrain(new Size(50, 50))); + } + + public Size Constraint { get; } + public Rect Bounds { get; } + public string Text { get; } + + + public IEnumerable GetLines() + { + return new[] {new FormattedTextLine(Text.Length, 10)}; + } + + public TextHitTestResult HitTestPoint(Point point) => new TextHitTestResult(); + + public Rect HitTestTextPosition(int index) => new Rect(); + + public IEnumerable HitTestTextRange(int index, int length) => new Rect[length]; + } + } +} diff --git a/src/Avalonia.Headless/HeadlessPlatformStubs.cs b/src/Avalonia.Headless/HeadlessPlatformStubs.cs new file mode 100644 index 0000000000..debe77c1b6 --- /dev/null +++ b/src/Avalonia.Headless/HeadlessPlatformStubs.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Controls.Platform; +using Avalonia.Input; +using Avalonia.Input.Platform; +using Avalonia.Platform; + +namespace Avalonia.Headless +{ + class HeadlessClipboardStub : IClipboard + { + private string _text; + public Task GetTextAsync() + { + return Task.Run(() => _text); + } + + public Task SetTextAsync(string text) + { + return Task.Run(() => _text = text); + } + + public Task ClearAsync() + { + return Task.Run(() => _text = null); + } + } + + class HeadlessCursorFactoryStub : IStandardCursorFactory + { + + public IPlatformHandle GetCursor(StandardCursorType cursorType) + { + return new PlatformHandle(new IntPtr((int)cursorType), "STUB"); + } + } + + class HeadlessPlatformSettingsStub : IPlatformSettings + { + public Size DoubleClickSize { get; } = new Size(2, 2); + public TimeSpan DoubleClickTime { get; } = TimeSpan.FromMilliseconds(500); + } + + class HeadlessSystemDialogsStub : ISystemDialogImpl + { + public Task ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent) + { + return Task.Run(() => (string[])null); + } + + public Task ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent) + { + return Task.Run(() => (string)null); + } + } + + class HeadlessIconLoaderStub : IPlatformIconLoader + { + + class IconStub : IWindowIconImpl + { + public void Save(Stream outputStream) + { + + } + } + public IWindowIconImpl LoadIcon(string fileName) + { + return new IconStub(); + } + + public IWindowIconImpl LoadIcon(Stream stream) + { + return new IconStub(); + } + + public IWindowIconImpl LoadIcon(IBitmapImpl bitmap) + { + return new IconStub(); + } + } + + class HeadlessScreensStub : IScreenImpl + { + public int ScreenCount { get; } = 1; + + public IReadOnlyList AllScreens { get; } = new[] + { + new Screen(new PixelRect(0, 0, 1920, 1280), + new PixelRect(0, 0, 1920, 1280), true), + }; + } +} diff --git a/src/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs b/src/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs new file mode 100644 index 0000000000..e42a7b1a71 --- /dev/null +++ b/src/Avalonia.Headless/HeadlessPlatformThreadingInterface.cs @@ -0,0 +1,86 @@ +using System; +using System.Reactive.Disposables; +using System.Threading; +using Avalonia.Platform; +using Avalonia.Threading; + +namespace Avalonia.Headless +{ + class HeadlessPlatformThreadingInterface : IPlatformThreadingInterface + { + public HeadlessPlatformThreadingInterface() + { + _thread = Thread.CurrentThread; + } + + private AutoResetEvent _event = new AutoResetEvent(false); + private Thread _thread; + private object _lock = new object(); + private DispatcherPriority? _signaledPriority; + + public void RunLoop(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + DispatcherPriority? signaled = null; + lock (_lock) + { + signaled = _signaledPriority; + _signaledPriority = null; + } + if(signaled.HasValue) + Signaled?.Invoke(signaled); + WaitHandle.WaitAny(new[] {cancellationToken.WaitHandle, _event}, TimeSpan.FromMilliseconds(20)); + } + } + + public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick) + { + var cancelled = false; + var enqueued = false; + var l = new object(); + var timer = new Timer(_ => + { + lock (l) + { + if (cancelled || enqueued) + return; + enqueued = true; + Dispatcher.UIThread.Post(() => + { + lock (l) + { + enqueued = false; + if (cancelled) + return; + tick(); + } + }, priority); + } + }, null, interval, interval); + return Disposable.Create(() => + { + lock (l) + { + timer.Dispose(); + cancelled = true; + } + }); + } + + public void Signal(DispatcherPriority priority) + { + lock (_lock) + { + if (_signaledPriority == null || _signaledPriority.Value > priority) + { + _signaledPriority = priority; + } + _event.Set(); + } + } + + public bool CurrentThreadIsLoopThread => _thread == Thread.CurrentThread; + public event Action Signaled; + } +} diff --git a/src/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Avalonia.Headless/HeadlessWindowImpl.cs new file mode 100644 index 0000000000..550defa815 --- /dev/null +++ b/src/Avalonia.Headless/HeadlessWindowImpl.cs @@ -0,0 +1,251 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Avalonia.Controls; +using Avalonia.Controls.Platform.Surfaces; +using Avalonia.Input; +using Avalonia.Input.Raw; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using Avalonia.Rendering; +using Avalonia.Threading; +using Avalonia.Utilities; + +namespace Avalonia.Headless +{ + class HeadlessWindowImpl : IWindowImpl, IPopupImpl, IFramebufferPlatformSurface, IHeadlessWindow + { + private IKeyboardDevice _keyboard; + private Stopwatch _st = Stopwatch.StartNew(); + private Pointer _mousePointer; + private WriteableBitmap _lastRenderedFrame; + private object _sync = new object(); + public bool IsPopup { get; } + + public HeadlessWindowImpl(bool isPopup) + { + IsPopup = isPopup; + Surfaces = new object[] {this}; + _keyboard = AvaloniaLocator.Current.GetService(); + _mousePointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); + MouseDevice = new MouseDevice(_mousePointer); + ClientSize = new Size(1024, 768); + } + + public void Dispose() + { + Closed?.Invoke(); + _lastRenderedFrame?.Dispose(); + _lastRenderedFrame = null; + } + + public Size ClientSize { get; set; } + public double Scaling { get; } = 1; + public IEnumerable Surfaces { get; } + public Action Input { get; set; } + public Action Paint { get; set; } + public Action Resized { get; set; } + public Action ScalingChanged { get; set; } + + public IRenderer CreateRenderer(IRenderRoot root) + => new DeferredRenderer(root, AvaloniaLocator.Current.GetService()); + + public void Invalidate(Rect rect) + { + } + + public void SetInputRoot(IInputRoot inputRoot) + { + InputRoot = inputRoot; + } + + public IInputRoot InputRoot { get; set; } + + public Point PointToClient(PixelPoint point) => point.ToPoint(Scaling); + + public PixelPoint PointToScreen(Point point) => PixelPoint.FromPoint(point, Scaling); + + public void SetCursor(IPlatformHandle cursor) + { + + } + + public Action Closed { get; set; } + public IMouseDevice MouseDevice { get; } + + public void Show() + { + Dispatcher.UIThread.Post(() => Activated?.Invoke(), DispatcherPriority.Input); + } + + public void Hide() + { + Dispatcher.UIThread.Post(() => Deactivated?.Invoke(), DispatcherPriority.Input); + } + + public void BeginMoveDrag() + { + + } + + public void BeginResizeDrag(WindowEdge edge) + { + + } + + public PixelPoint Position { get; set; } + public Action PositionChanged { get; set; } + public void Activate() + { + Dispatcher.UIThread.Post(() => Activated?.Invoke(), DispatcherPriority.Input); + } + + public Action Deactivated { get; set; } + public Action Activated { get; set; } + public IPlatformHandle Handle { get; } = new PlatformHandle(IntPtr.Zero, "STUB"); + public Size MaxClientSize { get; } = new Size(1920, 1280); + public void Resize(Size clientSize) + { + // Emulate X11 behavior here + if (IsPopup) + DoResize(clientSize); + else + Dispatcher.UIThread.Post(() => + { + DoResize(clientSize); + }); + } + + void DoResize(Size clientSize) + { + ClientSize = clientSize; + Resized?.Invoke(clientSize); + } + + public void SetMinMaxSize(Size minSize, Size maxSize) + { + + } + + public void SetTopmost(bool value) + { + + } + + public IScreenImpl Screen { get; } = new HeadlessScreensStub(); + public WindowState WindowState { get; set; } + public Action WindowStateChanged { get; set; } + public void SetTitle(string title) + { + + } + + public void ShowDialog(IWindowImpl parent) + { + Show(); + } + + public void SetSystemDecorations(bool enabled) + { + + } + + public void SetIcon(IWindowIconImpl icon) + { + + } + + public void ShowTaskbarIcon(bool value) + { + + } + + public void CanResize(bool value) + { + + } + + public Func Closing { get; set; } + + class FramebufferProxy : ILockedFramebuffer + { + private readonly ILockedFramebuffer _fb; + private readonly Action _onDispose; + private bool _disposed; + + public FramebufferProxy(ILockedFramebuffer fb, Action onDispose) + { + _fb = fb; + _onDispose = onDispose; + } + public void Dispose() + { + if(_disposed) + return; + _disposed = true; + _fb.Dispose(); + _onDispose(); + } + + public IntPtr Address => _fb.Address; + public PixelSize Size => _fb.Size; + public int RowBytes => _fb.RowBytes; + public Vector Dpi => _fb.Dpi; + public PixelFormat Format => _fb.Format; + } + + public ILockedFramebuffer Lock() + { + var bmp = new WriteableBitmap(PixelSize.FromSize(ClientSize, Scaling), new Vector(96, 96) * Scaling); + var fb = bmp.Lock(); + return new FramebufferProxy(fb, () => + { + lock (_sync) + { + _lastRenderedFrame?.Dispose(); + _lastRenderedFrame = bmp; + } + }); + } + + public IRef GetLastRenderedFrame() + { + lock (_sync) + return _lastRenderedFrame?.PlatformImpl?.CloneAs(); + } + + private ulong Timestamp => (ulong)_st.ElapsedMilliseconds; + + void IHeadlessWindow.KeyPress(Key key, InputModifiers modifiers) + { + Input?.Invoke(new RawKeyEventArgs(_keyboard, Timestamp, RawKeyEventType.KeyDown, key, modifiers)); + } + + void IHeadlessWindow.KeyRelease(Key key, InputModifiers modifiers) + { + Input?.Invoke(new RawKeyEventArgs(_keyboard, Timestamp, RawKeyEventType.KeyUp, key, modifiers)); + } + + void IHeadlessWindow.MouseDown(Point point, int button, InputModifiers modifiers) + { + Input?.Invoke(new RawPointerEventArgs(MouseDevice, Timestamp, InputRoot, + button == 0 ? RawPointerEventType.LeftButtonDown : + button == 1 ? RawPointerEventType.MiddleButtonDown : RawPointerEventType.RightButtonDown, + point, modifiers)); + } + + void IHeadlessWindow.MouseMove(Point point, InputModifiers modifiers) + { + Input?.Invoke(new RawPointerEventArgs(MouseDevice, Timestamp, InputRoot, + RawPointerEventType.Move, point, modifiers)); + } + + void IHeadlessWindow.MouseUp(Point point, int button, InputModifiers modifiers) + { + Input?.Invoke(new RawPointerEventArgs(MouseDevice, Timestamp, InputRoot, + button == 0 ? RawPointerEventType.LeftButtonUp : + button == 1 ? RawPointerEventType.MiddleButtonUp : RawPointerEventType.RightButtonUp, + point, modifiers)); + } + } +} diff --git a/src/Avalonia.Headless/IHeadlessWindow.cs b/src/Avalonia.Headless/IHeadlessWindow.cs new file mode 100644 index 0000000000..aafad2e500 --- /dev/null +++ b/src/Avalonia.Headless/IHeadlessWindow.cs @@ -0,0 +1,17 @@ +using Avalonia.Input; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using Avalonia.Utilities; + +namespace Avalonia.Headless +{ + public interface IHeadlessWindow + { + IRef GetLastRenderedFrame(); + void KeyPress(Key key, InputModifiers modifiers); + void KeyRelease(Key key, InputModifiers modifiers); + void MouseDown(Point point, int button, InputModifiers modifiers = InputModifiers.None); + void MouseMove(Point point, InputModifiers modifiers = InputModifiers.None); + void MouseUp(Point point, int button, InputModifiers modifiers = InputModifiers.None); + } +} From a0ffb8ec9890d7217835403a79c42cd9aae567a0 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Fri, 28 Jun 2019 10:40:09 +0300 Subject: [PATCH 009/693] Added a "script" to load all control catalog pages in headless mode --- samples/ControlCatalog.NetCore/Program.cs | 46 +++++++++++++++++++++ src/Avalonia.Headless/HeadlessWindowImpl.cs | 8 +++- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index 91f05ca719..766f3c5afa 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -2,8 +2,12 @@ using System.Diagnostics; using System.Linq; using System.Threading; +using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Headless; +using Avalonia.LogicalTree; using Avalonia.Skia; using Avalonia.ReactiveUI; using Avalonia.Threading; @@ -37,6 +41,48 @@ namespace ControlCatalog.NetCore { return builder.StartWithHeadlessVncPlatform(null, 5901, args, ShutdownMode.OnMainWindowClose); } + else if (args.Contains("--full-headless")) + { + return builder + .UseHeadless(true) + .AfterSetup(_ => + { + DispatcherTimer.RunOnce(async () => + { + var window = ((IClassicDesktopStyleApplicationLifetime)Application.Current.ApplicationLifetime) + .MainWindow; + var tc = window.GetLogicalDescendants().OfType().First(); + foreach (var page in tc.Items.Cast().ToList()) + { + // Skip DatePicker because of some layout bug in grid + if (page.Header.ToString() == "DatePicker") + continue; + Console.WriteLine("Selecting " + page.Header); + tc.SelectedItem = page; + await Task.Delay(500); + } + Console.WriteLine("Selecting the first page"); + tc.SelectedItem = tc.Items.OfType().First(); + await Task.Delay(500); + Console.WriteLine("Clicked through all pages, triggering GC"); + for (var c = 0; c < 3; c++) + { + GC.Collect(2, GCCollectionMode.Forced); + await Task.Delay(500); + } + + void FormatMem(string metric, long bytes) + { + Console.WriteLine(metric + ": " + bytes / 1024 / 1024 + "MB"); + } + + FormatMem("GC allocated bytes", GC.GetTotalMemory(true)); + FormatMem("WorkingSet64", Process.GetCurrentProcess().WorkingSet64); + + }, TimeSpan.FromSeconds(1)); + }) + .StartWithClassicDesktopLifetime(args); + } else return builder.StartWithClassicDesktopLifetime(args); } diff --git a/src/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Avalonia.Headless/HeadlessWindowImpl.cs index 550defa815..b19abffc01 100644 --- a/src/Avalonia.Headless/HeadlessWindowImpl.cs +++ b/src/Avalonia.Headless/HeadlessWindowImpl.cs @@ -118,8 +118,12 @@ namespace Avalonia.Headless void DoResize(Size clientSize) { - ClientSize = clientSize; - Resized?.Invoke(clientSize); + // Uncomment this check and experience a weird bug in layout engine + if (ClientSize != clientSize) + { + ClientSize = clientSize; + Resized?.Invoke(clientSize); + } } public void SetMinMaxSize(Size minSize, Size maxSize) From e3bc5345aeb0b1e71f39b2100290d73bc9db033f Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sat, 6 Jul 2019 12:32:57 -0700 Subject: [PATCH 010/693] Finish initial implementation of compiled bindings. Still some bugs to work out, but we have something that works now. --- .../Data/Core/ClrPropertyInfo.cs | 7 +- src/Avalonia.Base/Data/Core/IPropertyInfo.cs | 3 + .../Data/Core/Plugins/TaskStreamPlugin.cs | 2 +- .../Data/Core/PropertyAccessorNode.cs | 22 +- src/Avalonia.Base/Data/Core/StreamNode.cs | 37 +- .../Avalonia.Build.Tasks.csproj | 8 +- .../Avalonia.Markup.Xaml.csproj | 13 +- .../CompiledBindingExtension.cs | 75 +++ .../CompiledBindings/CompiledBindingPath.cs | 177 +++++++ .../NotifyingPropertyInfoHelpers.cs | 255 ++++++++++ .../ObservableStreamPlugin.cs | 28 ++ .../PropertyInfoAccessorPlugin.cs | 112 +++++ .../CompiledBindings/TaskStreamPlugin.cs | 53 ++ .../AvaloniaXamlIlCompiler.cs | 14 +- .../Transformers/AddNameScopeRegistration.cs | 78 +-- .../AvaloniaXamlIlBindingPathTransformer.cs | 33 ++ ...valoniaXamlIlDataContextTypeTransformer.cs | 46 +- ...mlIlTransformInstanceAttachedProperties.cs | 5 + .../AvaloniaXamlIlWellKnownTypes.cs | 10 +- .../XamlIlBindingPathHelper.cs | 459 ++++++++++++++++++ .../XamlIlClrPropertyInfoHelper.cs | 11 +- src/Markup/Avalonia.Markup/Data/Binding.cs | 263 +--------- .../Avalonia.Markup/Data/BindingBase.cs | 275 +++++++++++ .../Markup/Parsers/ArgumentListParser.cs | 5 +- .../Parsers/BindingExpressionGrammar.cs | 11 +- .../Markup/Parsers/Nodes/ElementNameNode.cs | 2 +- .../Markup/Parsers/Nodes/FindAncestorNode.cs | 2 +- .../Markup/Parsers/Nodes/SelfNode.cs | 2 +- .../CompiledBindingExtensionTests.cs | 47 ++ .../Xaml/XamlIlTests.cs | 15 +- 30 files changed, 1743 insertions(+), 327 deletions(-) create mode 100644 src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs create mode 100644 src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs create mode 100644 src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/NotifyingPropertyInfoHelpers.cs create mode 100644 src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/ObservableStreamPlugin.cs create mode 100644 src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorPlugin.cs create mode 100644 src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/TaskStreamPlugin.cs create mode 100644 src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs create mode 100644 src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlBindingPathHelper.cs create mode 100644 src/Markup/Avalonia.Markup/Data/BindingBase.cs create mode 100644 tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs diff --git a/src/Avalonia.Base/Data/Core/ClrPropertyInfo.cs b/src/Avalonia.Base/Data/Core/ClrPropertyInfo.cs index 05059b6617..f66411c2c2 100644 --- a/src/Avalonia.Base/Data/Core/ClrPropertyInfo.cs +++ b/src/Avalonia.Base/Data/Core/ClrPropertyInfo.cs @@ -9,14 +9,17 @@ namespace Avalonia.Data.Core private readonly Func _getter; private readonly Action _setter; - public ClrPropertyInfo(string name, Func getter, Action setter) + public ClrPropertyInfo(string name, Func getter, Action setter, Type propertyType) { _getter = getter; _setter = setter; + PropertyType = propertyType; Name = name; } public string Name { get; } + public Type PropertyType { get; } + public object Get(object target) { if (_getter == null) @@ -62,7 +65,7 @@ namespace Avalonia.Data.Core } public ReflectionClrPropertyInfo(PropertyInfo info) : base(info.Name, - CreateGetter(info), CreateSetter(info)) + CreateGetter(info), CreateSetter(info), info.PropertyType) { } diff --git a/src/Avalonia.Base/Data/Core/IPropertyInfo.cs b/src/Avalonia.Base/Data/Core/IPropertyInfo.cs index 530b98d08b..2417d0ffc4 100644 --- a/src/Avalonia.Base/Data/Core/IPropertyInfo.cs +++ b/src/Avalonia.Base/Data/Core/IPropertyInfo.cs @@ -1,3 +1,5 @@ +using System; + namespace Avalonia.Data.Core { public interface IPropertyInfo @@ -7,5 +9,6 @@ namespace Avalonia.Data.Core void Set(object target, object value); bool CanSet { get; } bool CanGet { get; } + Type PropertyType { get; } } } diff --git a/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs index 16862f576d..35917226ef 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs @@ -57,7 +57,7 @@ namespace Avalonia.Data.Core.Plugins return Observable.Empty(); } - protected IObservable HandleCompleted(Task task) + private IObservable HandleCompleted(Task task) { var resultProperty = task.GetType().GetRuntimeProperty("Result"); diff --git a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs index df8f46a7d7..6cc7889eb6 100644 --- a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs @@ -11,6 +11,7 @@ namespace Avalonia.Data.Core public class PropertyAccessorNode : SettableNode { private readonly bool _enableValidation; + private IPropertyAccessorPlugin _customPlugin; private IPropertyAccessor _accessor; public PropertyAccessorNode(string propertyName, bool enableValidation) @@ -19,6 +20,13 @@ namespace Avalonia.Data.Core _enableValidation = enableValidation; } + public PropertyAccessorNode(string propertyName, bool enableValidation, IPropertyAccessorPlugin customPlugin) + { + PropertyName = propertyName; + _enableValidation = enableValidation; + _customPlugin = customPlugin; + } + public override string Description => PropertyName; public string PropertyName { get; } public override Type PropertyType => _accessor?.PropertyType; @@ -39,7 +47,7 @@ namespace Avalonia.Data.Core protected override void StartListeningCore(WeakReference reference) { - var plugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(reference.Target, PropertyName)); + var plugin = _customPlugin ?? GetPropertyAccessorPluginForObject(reference.Target); var accessor = plugin?.Start(reference, PropertyName); if (_enableValidation && Next == null) @@ -53,16 +61,16 @@ namespace Avalonia.Data.Core } } - if (accessor == null) - { - throw new NotSupportedException( + _accessor = accessor ?? throw new NotSupportedException( $"Could not find a matching property accessor for {PropertyName}."); - } - - _accessor = accessor; accessor.Subscribe(ValueChanged); } + private IPropertyAccessorPlugin GetPropertyAccessorPluginForObject(object target) + { + return ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(target, PropertyName)); + } + protected override void StopListeningCore() { _accessor.Dispose(); diff --git a/src/Avalonia.Base/Data/Core/StreamNode.cs b/src/Avalonia.Base/Data/Core/StreamNode.cs index 6fc178e7f8..17b5951ed8 100644 --- a/src/Avalonia.Base/Data/Core/StreamNode.cs +++ b/src/Avalonia.Base/Data/Core/StreamNode.cs @@ -3,36 +3,55 @@ using System; using System.Reactive.Linq; +using Avalonia.Data.Core.Plugins; namespace Avalonia.Data.Core { public class StreamNode : ExpressionNode { + private IStreamPlugin _customPlugin = null; private IDisposable _subscription; public override string Description => "^"; + public StreamNode() { } + + public StreamNode(IStreamPlugin customPlugin) + { + _customPlugin = customPlugin; + } + protected override void StartListeningCore(WeakReference reference) { + GetPlugin(reference)?.Start(reference).Subscribe(ValueChanged); + } + + protected override void StopListeningCore() + { + _subscription?.Dispose(); + _subscription = null; + } + + private IStreamPlugin GetPlugin(WeakReference reference) + { + if (_customPlugin != null) + { + return _customPlugin; + } + foreach (var plugin in ExpressionObserver.StreamHandlers) { if (plugin.Match(reference)) { - _subscription = plugin.Start(reference).Subscribe(ValueChanged); - return; + return plugin; } } - // TODO: Improve error. + // TODO: Improve error ValueChanged(new BindingNotification( new MarkupBindingChainException("Stream operator applied to unsupported type", Description), BindingErrorType.Error)); - } - - protected override void StopListeningCore() - { - _subscription?.Dispose(); - _subscription = null; + return null; } } } diff --git a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj index 1b4424a050..eb9b1c2fb5 100644 --- a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj +++ b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj @@ -45,9 +45,15 @@ Markup/%(RecursiveDir)%(FileName)%(Extension) + + Markup/%(RecursiveDir)%(FileName)%(Extension) + Markup/%(RecursiveDir)%(FileName)%(Extension) - + + + Markup/%(RecursiveDir)%(FileName)%(Extension) + diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index dfa036b1f2..472afab0ae 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -16,6 +16,12 @@ + + + + + + @@ -41,6 +47,7 @@ + @@ -58,6 +65,7 @@ + @@ -67,6 +75,7 @@ + @@ -78,7 +87,9 @@ - + + global,Markup,%(Aliases) + diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs new file mode 100644 index 0000000000..bc1e2cdd36 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs @@ -0,0 +1,75 @@ +extern alias Markup; +using System; +using Avalonia.Data; +using Avalonia.Controls; +using Avalonia.Styling; +using Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings; +using Avalonia.Data.Core; + +using SourceMode = Markup::Avalonia.Markup.Parsers.SourceMode; +using Avalonia.Data.Converters; + +namespace Avalonia.Markup.Xaml.MarkupExtensions +{ + public class CompiledBindingExtension : BindingBase + { + public CompiledBindingExtension() + { + Path = new CompiledBindingPath(); + } + + public CompiledBindingExtension(CompiledBindingPath path) + { + Path = path; + } + + public CompiledBindingExtension ProvideValue(IServiceProvider provider) + { + return new CompiledBindingExtension + { + Path = Path, + Converter = Converter, + FallbackValue = FallbackValue, + Mode = Mode, + Priority = Priority, + StringFormat = StringFormat, + DefaultAnchor = new WeakReference(GetDefaultAnchor(provider)) + }; + } + + private static object GetDefaultAnchor(IServiceProvider provider) + { + // If the target is not a control, so we need to find an anchor that will let us look + // up named controls and style resources. First look for the closest IControl in + // the context. + object anchor = provider.GetFirstParent(); + + // If a control was not found, then try to find the highest-level style as the XAML + // file could be a XAML file containing only styles. + return anchor ?? + provider.GetService()?.RootObject as IStyle ?? + provider.GetLastParent(); + } + + protected override ExpressionObserver CreateExpressionObserver(IAvaloniaObject target, AvaloniaProperty targetProperty, object anchor, bool enableDataValidation) + { + if (Path.SourceMode == SourceMode.Data) + { + return CreateDataContextObserver( + target, + Path.BuildExpression(enableDataValidation), + targetProperty == StyledElement.DataContextProperty, + anchor); + } + else + { + return CreateSourceObserver( + (target as IStyledElement) ?? (anchor as IStyledElement), + Path.BuildExpression(enableDataValidation)); + } + } + + [ConstructorArgument("path")] + public CompiledBindingPath Path { get; set; } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs new file mode 100644 index 0000000000..81b9bd29b9 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs @@ -0,0 +1,177 @@ +extern alias Markup; +using System; +using System.Collections.Generic; +using Avalonia.Data.Core; +using Avalonia.Data.Core.Plugins; +using Avalonia.Markup.Parsers.Nodes; +using SourceMode = Markup::Avalonia.Markup.Parsers.SourceMode; + +namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings +{ + public class CompiledBindingPath + { + private readonly List _elements = new List(); + + public CompiledBindingPath() { } + + internal CompiledBindingPath(IEnumerable bindingPath) + { + _elements = new List(bindingPath); + } + + public ExpressionNode BuildExpression(bool enableValidation) + { + ExpressionNode pathRoot = null; + ExpressionNode path = null; + foreach (var element in _elements) + { + ExpressionNode node = null; + switch (element) + { + case NotExpressionPathElement _: + node = new LogicalNotNode(); + break; + case PropertyElement prop: + node = new PropertyAccessorNode(prop.Property.Name, enableValidation, new PropertyInfoAccessorPlugin(prop.Property)); + break; + case AncestorPathElement ancestor: + node = new FindAncestorNode(ancestor.AncestorType, ancestor.Level); + break; + case SelfPathElement _: + node = new SelfNode(); + break; + case ElementNameElement name: + node = new ElementNameNode(name.Name); + break; + case IStronglyTypedStreamElement stream: + node = new StreamNode(stream.CreatePlugin()); + break; + default: + throw new InvalidOperationException($"Unknown binding path element type {element.GetType().FullName}"); + } + + path = pathRoot is null ? (pathRoot = node) : path.Next = node; + } + + return pathRoot ?? new EmptyExpressionNode(); + } + + public SourceMode SourceMode => _elements.Count > 0 && _elements[0] is IControlSourceBindingPathElement ? SourceMode.Control : SourceMode.Data; + } + + public class CompiledBindingPathBuilder + { + private List _elements = new List(); + + public CompiledBindingPathBuilder Not() + { + _elements.Add(new NotExpressionPathElement()); + return this; + } + + public CompiledBindingPathBuilder Property(INotifyingPropertyInfo info) + { + _elements.Add(new PropertyElement(info)); + return this; + } + + public CompiledBindingPathBuilder StreamTask() + { + _elements.Add(new TaskStreamPathElement()); + return this; + } + + public CompiledBindingPathBuilder StreamObservable() + { + _elements.Add(new ObservableStreamPathElement()); + return this; + } + + public CompiledBindingPathBuilder Self() + { + _elements.Add(new SelfPathElement()); + return this; + } + + public CompiledBindingPathBuilder Ancestor(Type ancestorType, int level) + { + _elements.Add(new AncestorPathElement(ancestorType, level)); + return this; + } + + public CompiledBindingPathBuilder ElementName(string name) + { + _elements.Add(new ElementNameElement(name)); + return this; + } + + public CompiledBindingPath Build() => new CompiledBindingPath(_elements); + } + + public interface ICompiledBindingPathElement + { + } + + internal interface IControlSourceBindingPathElement { } + + internal class NotExpressionPathElement : ICompiledBindingPathElement + { + public static readonly NotExpressionPathElement Instance = new NotExpressionPathElement(); + } + + internal class PropertyElement : ICompiledBindingPathElement + { + public PropertyElement(INotifyingPropertyInfo property) + { + Property = property; + } + + public INotifyingPropertyInfo Property { get; } + } + + internal interface IStronglyTypedStreamElement : ICompiledBindingPathElement + { + IStreamPlugin CreatePlugin(); + } + + internal class TaskStreamPathElement : IStronglyTypedStreamElement + { + public static readonly TaskStreamPathElement Instance = new TaskStreamPathElement(); + + public IStreamPlugin CreatePlugin() => new TaskStreamPlugin(); + } + + internal class ObservableStreamPathElement : IStronglyTypedStreamElement + { + public static readonly ObservableStreamPathElement Instance = new ObservableStreamPathElement(); + + public IStreamPlugin CreatePlugin() => new ObservableStreamPlugin(); + } + + internal class SelfPathElement : ICompiledBindingPathElement, IControlSourceBindingPathElement + { + public static readonly SelfPathElement Instance = new SelfPathElement(); + } + + internal class AncestorPathElement : ICompiledBindingPathElement, IControlSourceBindingPathElement + { + public AncestorPathElement(Type ancestorType, int level) + { + AncestorType = ancestorType; + Level = level; + } + + public Type AncestorType { get; } + public int Level { get; } + } + + internal class ElementNameElement : ICompiledBindingPathElement, IControlSourceBindingPathElement + { + public ElementNameElement(string name) + { + Name = name; + } + + public string Name { get; } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/NotifyingPropertyInfoHelpers.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/NotifyingPropertyInfoHelpers.cs new file mode 100644 index 0000000000..0af201ade8 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/NotifyingPropertyInfoHelpers.cs @@ -0,0 +1,255 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Text; +using Avalonia.Data.Core; +using Avalonia.Reactive; +using Avalonia.Utilities; + +namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings +{ + public static class NotifyingPropertyInfoHelpers + { + public static INotifyingPropertyInfo CreateINPCPropertyInfo(IPropertyInfo basePropertyInfo) + => new INPCPropertyInfo(basePropertyInfo); + + public static INotifyingPropertyInfo CreateAvaloniaPropertyInfo(AvaloniaProperty property) + => new AvaloniaPropertyInfo(property); + + public static INotifyingPropertyInfo CreateIndexerPropertyInfo(IPropertyInfo basePropertyInfo, int argument) + => new IndexerInfo(basePropertyInfo, argument); + } + + public interface INotifyingPropertyInfo : IPropertyInfo + { + void OnPropertyChanged(object target, EventHandler handler); + void RemoveListener(object target, EventHandler handler); + } + + internal abstract class NotifyingPropertyInfoBase : INotifyingPropertyInfo + { + private readonly IPropertyInfo _base; + protected readonly ConditionalWeakTable _changedHandlers = new ConditionalWeakTable(); + + public NotifyingPropertyInfoBase(IPropertyInfo baseProperty) + { + _base = baseProperty; + } + + public string Name => _base.Name; + + public bool CanSet => _base.CanSet; + + public bool CanGet => _base.CanGet; + + public Type PropertyType => _base.PropertyType; + + public object Get(object target) + { + return _base.Get(target); + } + + public void Set(object target, object value) + { + _base.Set(target, value); + } + + public void OnPropertyChanged(object target, EventHandler handler) + { + if (ValidateTargetType(target)) + { + return; + } + + if (_changedHandlers.TryGetValue(target, out var value)) + { + _changedHandlers.Remove(target); + _changedHandlers.Add(target, (EventHandler)Delegate.Combine(value, handler)); + } + else + { + _changedHandlers.Add(target, handler); + SubscribeToChangesForNewTarget(target); + } + } + + protected abstract bool ValidateTargetType(object target); + + protected abstract void SubscribeToChangesForNewTarget(object target); + + protected abstract void UnsubscribeToChangesForTarget(object target); + + protected bool TryGetHandlersForTarget(object target, out EventHandler handlers) + => _changedHandlers.TryGetValue(target, out handlers); + + public void RemoveListener(object target, EventHandler handler) + { + if (!ValidateTargetType(target)) + { + return; + } + + if (_changedHandlers.TryGetValue(target, out var value)) + { + _changedHandlers.Remove(target); + EventHandler modified = (EventHandler)Delegate.Remove(value, handler); + if (modified != null) + { + _changedHandlers.Add(target, modified); + } + else + { + UnsubscribeToChangesForTarget(target); + } + } + } + } + + internal class INPCPropertyInfo : NotifyingPropertyInfoBase + { + public INPCPropertyInfo(IPropertyInfo baseProperty) + :base(baseProperty) + { + } + + void OnNotifyPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (Name == e.PropertyName && TryGetHandlersForTarget(sender, out var handlers)) + { + handlers(sender, EventArgs.Empty); + } + } + + protected override bool ValidateTargetType(object target) + { + return target is INotifyPropertyChanged; + } + + protected override void SubscribeToChangesForNewTarget(object target) + { + if (target is INotifyPropertyChanged inpc) + { + WeakEventHandlerManager.Subscribe( + inpc, + nameof(INotifyPropertyChanged.PropertyChanged), + OnNotifyPropertyChanged); + } + } + + protected override void UnsubscribeToChangesForTarget(object target) + { + if (target is INotifyPropertyChanged) + { + WeakEventHandlerManager.Unsubscribe( + target, + nameof(INotifyPropertyChanged.PropertyChanged), + OnNotifyPropertyChanged); + } + } + } + + internal class AvaloniaPropertyInfo : NotifyingPropertyInfoBase + { + private readonly AvaloniaProperty _base; + + public AvaloniaPropertyInfo(AvaloniaProperty baseProperty) + :base(baseProperty) + { + _base = baseProperty; + } + + protected override void SubscribeToChangesForNewTarget(object target) + { + IAvaloniaObject obj = (IAvaloniaObject)target; + obj.PropertyChanged += OnPropertyChanged; + } + + private void OnPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + { + if (_base == e.Property && TryGetHandlersForTarget(sender, out var handlers)) + { + handlers(sender, EventArgs.Empty); + } + } + + protected override void UnsubscribeToChangesForTarget(object target) + { + ((IAvaloniaObject)target).PropertyChanged -= OnPropertyChanged; + } + + protected override bool ValidateTargetType(object target) + { + return target is IAvaloniaObject; + } + } + + internal class IndexerInfo : INPCPropertyInfo + { + private int _index; + + public IndexerInfo(IPropertyInfo baseProperty, int indexerArgument) : base(baseProperty) + { + _index = indexerArgument; + } + + protected override void SubscribeToChangesForNewTarget(object target) + { + base.SubscribeToChangesForNewTarget(target); + if (target is INotifyCollectionChanged incc) + { + WeakEventHandlerManager.Subscribe( + incc, + nameof(INotifyCollectionChanged.CollectionChanged), + OnNotifyCollectionChanged); + } + } + + protected override void UnsubscribeToChangesForTarget(object target) + { + base.UnsubscribeToChangesForTarget(target); + if (target is INotifyCollectionChanged) + { + WeakEventHandlerManager.Unsubscribe( + target, + nameof(INotifyCollectionChanged.CollectionChanged), + OnNotifyCollectionChanged); + } + } + + void OnNotifyCollectionChanged(object sender, NotifyCollectionChangedEventArgs args) + { + if (ShouldNotifyListeners(args) && TryGetHandlersForTarget(sender, out var handlers)) + { + handlers(sender, EventArgs.Empty); + } + } + + bool ShouldNotifyListeners(NotifyCollectionChangedEventArgs e) + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + return _index >= e.NewStartingIndex; + case NotifyCollectionChangedAction.Remove: + return _index >= e.OldStartingIndex; + case NotifyCollectionChangedAction.Replace: + return _index >= e.NewStartingIndex && + _index < e.NewStartingIndex + e.NewItems.Count; + case NotifyCollectionChangedAction.Move: + return (_index >= e.NewStartingIndex && + _index < e.NewStartingIndex + e.NewItems.Count) || + (_index >= e.OldStartingIndex && + _index < e.OldStartingIndex + e.OldItems.Count); + case NotifyCollectionChangedAction.Reset: + return true; + } + return false; + } + + protected override bool ValidateTargetType(object target) + => base.ValidateTargetType(target) || target is INotifyCollectionChanged; + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/ObservableStreamPlugin.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/ObservableStreamPlugin.cs new file mode 100644 index 0000000000..361cbef740 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/ObservableStreamPlugin.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Reactive.Linq; +using System.Text; +using Avalonia.Data.Core.Plugins; + +namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings +{ + class ObservableStreamPlugin : IStreamPlugin + { + public bool Match(WeakReference reference) + { + return reference is IObservable; + } + + public IObservable Start(WeakReference reference) + { + var target = reference.Target as IObservable; + + if (target is IObservable obj) + { + return obj; + } + + return target.Select(x => (object)x); + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorPlugin.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorPlugin.cs new file mode 100644 index 0000000000..ac3108e847 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorPlugin.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using Avalonia.Data; +using Avalonia.Data.Core.Plugins; + +namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings +{ + class PropertyInfoAccessorPlugin : IPropertyAccessorPlugin + { + private readonly INotifyingPropertyInfo _propertyInfo; + + public PropertyInfoAccessorPlugin(INotifyingPropertyInfo propertyInfo) + { + _propertyInfo = propertyInfo; + } + + public bool Match(object obj, string propertyName) + { + throw new InvalidOperationException("The PropertyInfoAccessorPlugin does not support dynamic matching"); + } + + public IPropertyAccessor Start(WeakReference reference, string propertyName) + { + Debug.Assert(_propertyInfo.Name == propertyName); + return new Accessor(reference, _propertyInfo); + } + + class Accessor : PropertyAccessorBase + { + private WeakReference _reference; + private INotifyingPropertyInfo _propertyInfo; + private bool _eventRaised; + + public Accessor(WeakReference reference, INotifyingPropertyInfo propertyInfo) + { + _reference = reference; + _propertyInfo = propertyInfo; + } + + public override Type PropertyType => _propertyInfo.PropertyType; + + public override object Value + { + get + { + var o = _reference.Target; + return (o != null) ? _propertyInfo.Get(o) : null; + } + } + + public override bool SetValue(object value, BindingPriority priority) + { + if (_propertyInfo.CanSet) + { + _eventRaised = false; + _propertyInfo.Set(_reference.Target, value); + + if (!_eventRaised) + { + SendCurrentValue(); + } + + return true; + } + + return false; + } + + void OnChanged(object sender, EventArgs e) + { + _eventRaised = true; + SendCurrentValue(); + } + + protected override void SubscribeCore() + { + SendCurrentValue(); + SubscribeToChanges(); + } + + protected override void UnsubscribeCore() + { + var target = _reference.Target; + if (target != null) + { + _propertyInfo.RemoveListener(target, OnChanged); + } + } + + private void SendCurrentValue() + { + try + { + var value = Value; + PublishValue(value); + } + catch { } + } + + private void SubscribeToChanges() + { + var target = _reference.Target; + if (target != null) + { + _propertyInfo.OnPropertyChanged(target, OnChanged); + } + } + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/TaskStreamPlugin.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/TaskStreamPlugin.cs new file mode 100644 index 0000000000..30fd1fdaa2 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/TaskStreamPlugin.cs @@ -0,0 +1,53 @@ +using System; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using System.Threading.Tasks; +using Avalonia.Data; +using Avalonia.Data.Core.Plugins; + +namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings +{ + class TaskStreamPlugin : IStreamPlugin + { + public bool Match(WeakReference reference) + { + return reference.Target is Task; + } + + public IObservable Start(WeakReference reference) + { + if (!(reference.Target is Task task)) + { + return Observable.Empty(); + } + + switch (task.Status) + { + case TaskStatus.RanToCompletion: + case TaskStatus.Faulted: + return HandleCompleted(task); + default: + var subject = new Subject(); + task.ContinueWith( + x => HandleCompleted(task).Subscribe(subject), + TaskScheduler.FromCurrentSynchronizationContext()) + .ConfigureAwait(false); + return subject; + } + } + + + private static IObservable HandleCompleted(Task task) + { + switch (task.Status) + { + case TaskStatus.RanToCompletion: + return Observable.Return((object)task.Result); + case TaskStatus.Faulted: + return Observable.Return(new BindingNotification(task.Exception, BindingErrorType.Error)); + default: + throw new AvaloniaInternalException("HandleCompleted called for non-completed Task."); + } + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs index 933d39bad8..9be4c0aa03 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -48,13 +48,17 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions new AvaloniaXamlIlSetterTransformer(), new AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer(), new AvaloniaXamlIlConstructorServiceProviderTransformer(), - new AvaloniaXamlIlTransitionsTypeMetadataTransformer(), - new AvaloniaXamlIlDataContextTypeTransformer() + new AvaloniaXamlIlTransitionsTypeMetadataTransformer() ); - + // After everything else - - Transformers.Add(new AddNameScopeRegistration()); + + InsertBefore( + new AddNameScopeRegistration(), + new AvaloniaXamlIlDataContextTypeTransformer(), + new AvaloniaXamlIlBindingPathTransformer() + ); + Transformers.Add(new AvaloniaXamlIlMetadataRemover()); } diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AddNameScopeRegistration.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AddNameScopeRegistration.cs index 33056fa3e8..87b5ec72fe 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AddNameScopeRegistration.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AddNameScopeRegistration.cs @@ -36,59 +36,65 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers } if (value != null) + { + var objectType = context.ParentNodes().OfType().FirstOrDefault()?.Type.GetClrType(); return new XamlIlManipulationGroupNode(pa) { Children = { pa, - new ScopeRegistrationNode(value) + new ScopeRegistrationNode(value, objectType) } }; + } } return node; } + } - class ScopeRegistrationNode : XamlIlAstNode, IXamlIlAstManipulationNode, IXamlIlAstEmitableNode + class ScopeRegistrationNode : XamlIlAstNode, IXamlIlAstManipulationNode, IXamlIlAstEmitableNode + { + public IXamlIlAstValueNode Value { get; set; } + public IXamlIlType ControlType { get; } + + public ScopeRegistrationNode(IXamlIlAstValueNode value, IXamlIlType controlType) : base(value) { - public IXamlIlAstValueNode Value { get; set; } - public ScopeRegistrationNode(IXamlIlAstValueNode value) : base(value) - { - Value = value; - } + Value = value; + ControlType = controlType; + } - public override void VisitChildren(IXamlIlAstVisitor visitor) - => Value = (IXamlIlAstValueNode)Value.Visit(visitor); + public override void VisitChildren(IXamlIlAstVisitor visitor) + => Value = (IXamlIlAstValueNode)Value.Visit(visitor); - public XamlIlNodeEmitResult Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + public XamlIlNodeEmitResult Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + { + var exts = context.Configuration.TypeSystem.GetType("Avalonia.Controls.NameScopeExtensions"); + var findNameScope = exts.FindMethod(m => m.Name == "FindNameScope"); + var registerMethod = findNameScope.ReturnType.FindMethod(m => m.Name == "Register"); + using (var targetLoc = context.GetLocal(context.Configuration.WellKnownTypes.Object)) + using (var nameScopeLoc = context.GetLocal(findNameScope.ReturnType)) { - var exts = context.Configuration.TypeSystem.GetType("Avalonia.Controls.NameScopeExtensions"); - var findNameScope = exts.FindMethod(m => m.Name == "FindNameScope"); - var registerMethod = findNameScope.ReturnType.FindMethod(m => m.Name == "Register"); - using (var targetLoc = context.GetLocal(context.Configuration.WellKnownTypes.Object)) - using (var nameScopeLoc = context.GetLocal(findNameScope.ReturnType)) - { - var exit = codeGen.DefineLabel(); - codeGen - // var target = {pop} - .Stloc(targetLoc.Local) - // var scope = target.FindNameScope() - .Ldloc(targetLoc.Local) - .Castclass(findNameScope.Parameters[0]) - .EmitCall(findNameScope) - .Stloc(nameScopeLoc.Local) - // if({scope} != null) goto call; - .Ldloc(nameScopeLoc.Local) - .Brfalse(exit) - .Ldloc(nameScopeLoc.Local); - context.Emit(Value, codeGen, Value.Type.GetClrType()); - codeGen - .Ldloc(targetLoc.Local) - .EmitCall(registerMethod) - .MarkLabel(exit); - } - return XamlIlNodeEmitResult.Void(1); + var exit = codeGen.DefineLabel(); + codeGen + // var target = {pop} + .Stloc(targetLoc.Local) + // var scope = target.FindNameScope() + .Ldloc(targetLoc.Local) + .Castclass(findNameScope.Parameters[0]) + .EmitCall(findNameScope) + .Stloc(nameScopeLoc.Local) + // if({scope} != null) goto call; + .Ldloc(nameScopeLoc.Local) + .Brfalse(exit) + .Ldloc(nameScopeLoc.Local); + context.Emit(Value, codeGen, Value.Type.GetClrType()); + codeGen + .Ldloc(targetLoc.Local) + .EmitCall(registerMethod) + .MarkLabel(exit); } + return XamlIlNodeEmitResult.Void(1); } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs new file mode 100644 index 0000000000..cd7d9f5d33 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using XamlIl; +using XamlIl.Ast; +using XamlIl.Transform; +using XamlIl.TypeSystem; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers +{ + class AvaloniaXamlIlBindingPathTransformer : IXamlIlAstTransformer + { + public IXamlIlAstNode Transform(XamlIlAstTransformationContext context, IXamlIlAstNode node) + { + if (node is XamlIlAstObjectNode binding && binding.Type.GetClrType().Equals(context.GetAvaloniaTypes().CompiledBindingExtension)) + { + IXamlIlType startType; + var parentDataContextNode = context.ParentNodes().OfType().FirstOrDefault(); + if (parentDataContextNode is null) + { + throw new XamlIlParseException("Cannot parse a compiled binding without an explicit x:DataContextType directive to give a starting data type for bindings.", binding); + } + + startType = parentDataContextNode.DataContextType; + + XamlIlBindingPathHelper.UpdateCompiledBindingExtension(context, binding, startType); + } + + return node; + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs index 5d68a72514..7e13b8a18a 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs @@ -1,18 +1,25 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Text; +using Avalonia.Markup.Parsers; +using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; +using Avalonia.Utilities; using XamlIl; using XamlIl.Ast; using XamlIl.Transform; +using XamlIl.TypeSystem; namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { class AvaloniaXamlIlDataContextTypeTransformer : IXamlIlAstTransformer { + private const string AvaloniaNs = "https://github.com/avaloniaui"; public IXamlIlAstNode Transform(XamlIlAstTransformationContext context, IXamlIlAstNode node) { if (node is XamlIlAstObjectNode on) { + AvaloniaXamlIlDataContextTypeMetadataNode calculatedDataContextTypeNode = null; foreach (var child in on.Children) { if (child is XamlIlAstXmlDirective directive) @@ -23,11 +30,44 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers && directive.Values[0] is XamlIlTypeExtensionNode dataContextType) { on.Children.Remove(child); - return new AvaloniaXamlIlDataContextTypeMetadataNode(on, dataContextType.Value); + return new AvaloniaXamlIlDataContextTypeMetadataNode(on, dataContextType.Value.GetClrType()); } } + else if (child is XamlIlAstXamlPropertyValueNode pv + && pv.Property is XamlIlAstNamePropertyReference pref + && pref.Name == "DataContext" + && pref.DeclaringType is XamlIlAstXmlTypeReference tref + && tref.Name == "StyledElement" + && tref.XmlNamespace == AvaloniaNs) + { + var bindingType = context.GetAvaloniaTypes().IBinding; + if (!pv.Values[0].Type.GetClrType().GetAllInterfaces().Contains(bindingType)) + { + calculatedDataContextTypeNode = new AvaloniaXamlIlDataContextTypeMetadataNode(on, pv.Values[0].Type.GetClrType()); + } + else if(pv.Values[0].Type.GetClrType().Equals(context.GetAvaloniaTypes().CompiledBindingExtension) + && pv.Values[0] is XamlIlAstObjectNode binding) + { + IXamlIlType startType; + var parentDataContextNode = context.ParentNodes().OfType().FirstOrDefault(); + if (parentDataContextNode is null) + { + throw new XamlIlParseException("Cannot parse a compiled binding without an explicit x:DataContextType directive to give a starting data type for bindings.", binding); + } + + startType = parentDataContextNode.DataContextType; + + var bindingResultType = XamlIlBindingPathHelper.UpdateCompiledBindingExtension(context, binding, startType); + calculatedDataContextTypeNode = new AvaloniaXamlIlDataContextTypeMetadataNode(on, bindingResultType); + } + } + } + if (!(calculatedDataContextTypeNode is null)) + { + return calculatedDataContextTypeNode; } } + // TODO: Add node for DataTemplate scope. return node; } @@ -35,9 +75,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers class AvaloniaXamlIlDataContextTypeMetadataNode : XamlIlValueWithSideEffectNodeBase { - public IXamlIlAstTypeReference DataContextType { get; set; } + public IXamlIlType DataContextType { get; set; } - public AvaloniaXamlIlDataContextTypeMetadataNode(IXamlIlAstValueNode value, IXamlIlAstTypeReference targetType) + public AvaloniaXamlIlDataContextTypeMetadataNode(IXamlIlAstValueNode value, IXamlIlType targetType) : base(value, value) { DataContextType = targetType; diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs index bbacef43dd..0adbba72b1 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs @@ -187,6 +187,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers emitter.Unbox_Any(Parent.PropertyType); } + + public IXamlIlMethod MakeGenericMethod(IReadOnlyList typeArguments) + { + throw new System.InvalidOperationException(); + } } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index f9f59dfb87..740d62e85e 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -23,7 +23,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlIlType ClrPropertyInfo { get; } public IXamlIlType PropertyPath { get; } public IXamlIlType PropertyPathBuilder { get; } - + public IXamlIlType NotifyingPropertyInfoHelpers { get; } + public IXamlIlType CompiledBindingPathBuilder { get; } + public IXamlIlType CompiledBindingPath { get; } + public IXamlIlType CompiledBindingExtension { get; } + public AvaloniaXamlIlWellKnownTypes(XamlIlTransformerConfiguration cfg) { XamlIlTypes = cfg.WellKnownTypes; @@ -47,6 +51,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers ClrPropertyInfo = cfg.TypeSystem.GetType("Avalonia.Data.Core.ClrPropertyInfo"); PropertyPath = cfg.TypeSystem.GetType("Avalonia.Data.Core.PropertyPath"); PropertyPathBuilder = cfg.TypeSystem.GetType("Avalonia.Data.Core.PropertyPathBuilder"); + NotifyingPropertyInfoHelpers = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings.NotifyingPropertyInfoHelpers"); + CompiledBindingPathBuilder = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings.CompiledBindingPathBuilder"); + CompiledBindingPath = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings.CompiledBindingPath"); + CompiledBindingExtension = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindingExtension"); } } diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlBindingPathHelper.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlBindingPathHelper.cs new file mode 100644 index 0000000000..c98e2029c4 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlBindingPathHelper.cs @@ -0,0 +1,459 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Linq; +using Avalonia.Markup.Parsers; +using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; +using XamlIl.Ast; +using XamlIl.Transform; +using XamlIl.Transform.Transformers; +using XamlIl.TypeSystem; +using XamlIl; +using Avalonia.Utilities; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions +{ + static class XamlIlBindingPathHelper + { + public static IXamlIlType UpdateCompiledBindingExtension(XamlIlAstTransformationContext context, XamlIlAstObjectNode binding, IXamlIlType startType) + { + IXamlIlType bindingResultType = null; + if (binding.Arguments.Count > 0 && binding.Arguments[0] is XamlIlAstTextNode bindingPathText) + { + var reader = new CharacterReader(bindingPathText.Text.AsSpan()); + var grammar = BindingExpressionGrammar.Parse(ref reader); + + var transformed = TransformBindingPath( + context, + bindingPathText, + startType, + grammar.Nodes); + + bindingResultType = transformed.BindingResultType; + binding.Arguments[0] = transformed; + } + else + { + var bindingPathAssignment = binding.Children.OfType() + .FirstOrDefault(v => v.Property.GetClrProperty().Name == "Path"); + + if (bindingPathAssignment is null) + { + return startType; + } + + if (bindingPathAssignment.Values[0] is XamlIlAstTextNode pathValue) + { + var reader = new CharacterReader(pathValue.Text.AsSpan()); + var grammar = BindingExpressionGrammar.Parse(ref reader); + + var transformed = TransformBindingPath( + context, + pathValue, + startType, + grammar.Nodes); + + bindingResultType = transformed.BindingResultType; + bindingPathAssignment.Values[0] = transformed; + } + else + { + throw new InvalidOperationException(); + } + } + + return bindingResultType; + } + + private static IXamlIlBindingPathNode TransformBindingPath(XamlIlAstTransformationContext context, IXamlIlLineInfo lineInfo, IXamlIlType startType, IEnumerable bindingExpression) + { + bool appendNotNode = false; + List nodes = new List(); + foreach (var astNode in bindingExpression) + { + var targetType = nodes.Count == 0 ? startType : nodes[nodes.Count - 1].Type; + switch (astNode) + { + case BindingExpressionGrammar.EmptyExpressionNode _: + break; + case BindingExpressionGrammar.NotNode _: + appendNotNode = !appendNotNode; + break; + case BindingExpressionGrammar.StreamNode _: + var observableType = targetType.GetAllInterfaces().FirstOrDefault(i => i.GenericTypeDefinition.Equals(context.Configuration.TypeSystem.FindType("System.IObservable`1"))); + if (observableType != null) + { + nodes.Add(new XamlIlStreamObservablePathElementNode(observableType.GenericArguments[0])); + break; + } + bool foundTask = false; + for (var currentType = targetType; currentType != null; currentType = currentType.BaseType) + { + if (currentType.GenericTypeDefinition.Equals(context.Configuration.TypeSystem.GetType("System.Threading.Tasks.Task`1"))) + { + foundTask = true; + nodes.Add(new XamlIlStreamTaskPathElementNode(currentType.GenericArguments[0])); + break; + } + } + if (foundTask) + { + break; + } + throw new XamlIlParseException($"Compiled bindings do not support stream bindings for objects of type {targetType.FullName}.", lineInfo); + case BindingExpressionGrammar.PropertyNameNode propName: + var avaloniaPropertyFieldNameMaybe = propName.PropertyName + "Property"; + var avaloniaPropertyFieldMaybe = targetType.GetAllFields().FirstOrDefault(f => + f.IsStatic && f.IsPublic && f.Name == avaloniaPropertyFieldNameMaybe); + + if (avaloniaPropertyFieldMaybe != null) + { + nodes.Add(new XamlIlAvaloniaPropertyPropertyPathElementNode(avaloniaPropertyFieldMaybe, + XamlIlAvaloniaPropertyHelper.GetAvaloniaPropertyType(avaloniaPropertyFieldMaybe, context.GetAvaloniaTypes(), lineInfo))); + } + else + { + var clrProperty = targetType.GetAllProperties().FirstOrDefault(p => p.Name == propName.PropertyName); + nodes.Add(new XamlIlClrPropertyPathElementNode(clrProperty)); + } + break; + case BindingExpressionGrammar.IndexerNode indexer: + { + IXamlIlProperty property = null; + for (var currentType = targetType; currentType != null; currentType = currentType.BaseType) + { + var defaultMemberAttribute = currentType.CustomAttributes.FirstOrDefault(x => x.Type.GetFullName() == "System.Reflection.DefaultMemberAttribute"); + if (defaultMemberAttribute != null) + { + property = targetType.GetAllProperties().FirstOrDefault(x => x.Name == (string)defaultMemberAttribute.Parameters[0]); + break; + } + + }; + if (property is null) + { + throw new XamlIlParseException($"The type '${targetType}' does not have an indexer.", lineInfo); + } + + IEnumerable parameters = property.IndexerParameters; + + List values = new List(); + int currentParamIndex = 0; + foreach (var param in parameters) + { + var textNode = new XamlIlAstTextNode(lineInfo, indexer.Arguments[currentParamIndex]); + if (!XamlIlTransformHelpers.TryGetCorrectlyTypedValue(context, textNode, + param, out var converted)) + throw new XamlIlParseException( + $"Unable to convert indexer parameter value of '{indexer.Arguments[currentParamIndex]}' to {param.GetFqn()}", + textNode); + + values.Add(converted); + currentParamIndex++; + } + + bool isNotifyingCollection = targetType.GetAllInterfaces().Any(i => i.FullName == "System.Collections.Specialized.INotifyCollectionChanged"); + + nodes.Add(new XamlIlClrIndexerPathElementNode(property, values, isNotifyingCollection)); + break; + } + case BindingExpressionGrammar.AttachedPropertyNameNode attachedProp: + var avaloniaPropertyFieldName = attachedProp.PropertyName + "Property"; + var avaloniaPropertyField = GetType(attachedProp.Namespace, attachedProp.TypeName).GetAllFields().FirstOrDefault(f => + f.IsStatic && f.IsPublic && f.Name == avaloniaPropertyFieldName); + nodes.Add(new XamlIlAvaloniaPropertyPropertyPathElementNode(avaloniaPropertyField, + XamlIlAvaloniaPropertyHelper.GetAvaloniaPropertyType(avaloniaPropertyField, context.GetAvaloniaTypes(), lineInfo))); + break; + case BindingExpressionGrammar.SelfNode _: + nodes.Add(new SelfPathElementNode(targetType)); + break; + case BindingExpressionGrammar.AncestorNode ancestor: + nodes.Add(new FindAncestorPathElementNode(GetType(ancestor.Namespace, ancestor.TypeName), ancestor.Level)); + break; + case BindingExpressionGrammar.NameNode elementName: + var elementType = ScopeRegistrationFinder.GetControlType(context, context.RootObject, elementName.Name); + if (elementType is null) + { + throw new XamlIlParseException($"Unable to find element '{elementName.Name}' in the current namescope. Unable to use a compiled binding with a name binding if the name cannot be found at compile time.", lineInfo); + } + nodes.Add(new ElementNamePathElementNode(elementName.Name, elementType)); + break; + } + + } + + if (appendNotNode) + { + // TODO: Fix Not behavior + nodes.Add(new XamlIlNotPathElementNode(context.Configuration.WellKnownTypes.Boolean)); + } + + return new XamlIlBindingPathNode(lineInfo, context.GetAvaloniaTypes().CompiledBindingPath, nodes); + + IXamlIlType GetType(string ns, string name) + { + return XamlIlTypeReferenceResolver.ResolveType(context, $"{ns}:{name}", false, + lineInfo, true).GetClrType(); + } + } + + class ScopeRegistrationFinder : IXamlIlAstTransformer + { + private ScopeRegistrationFinder(string name) + { + Name = name; + } + + string Name { get; } + + IXamlIlType ControlType { get; set; } + + public static IXamlIlType GetControlType(XamlIlAstTransformationContext context, IXamlIlAstNode namescopeRoot, string name) + { + var finder = new ScopeRegistrationFinder(name); + context.Visit(namescopeRoot, finder); + return finder.ControlType; + } + + IXamlIlAstNode IXamlIlAstTransformer.Transform(XamlIlAstTransformationContext context, IXamlIlAstNode node) + { + if (node is ScopeRegistrationNode registration) + { + if (registration.Value is XamlIlAstTextNode text && text.Text == Name) + { + ControlType = registration.ControlType; + } + } + return node; + } + } + + interface IXamlIlBindingPathElementNode + { + IXamlIlType Type { get; } + + void Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen); + } + + class XamlIlNotPathElementNode : IXamlIlBindingPathElementNode + { + public XamlIlNotPathElementNode(IXamlIlType boolType) + { + Type = boolType; + } + + public IXamlIlType Type { get; } + + public void Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + { + codeGen.EmitCall(context.GetAvaloniaTypes().CompiledBindingPathBuilder.FindMethod(m => m.Name == "Not")); + } + } + + class XamlIlStreamObservablePathElementNode : IXamlIlBindingPathElementNode + { + public XamlIlStreamObservablePathElementNode(IXamlIlType type) + { + Type = type; + } + + public IXamlIlType Type { get; } + + public void Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + { + codeGen.EmitCall(context.GetAvaloniaTypes().CompiledBindingPathBuilder.FindMethod(m => m.Name == "StreamObservable").MakeGenericMethod(new[] { Type })); + } + } + + class XamlIlStreamTaskPathElementNode : IXamlIlBindingPathElementNode + { + public XamlIlStreamTaskPathElementNode(IXamlIlType type) + { + Type = type; + } + + public IXamlIlType Type { get; } + + public void Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + { + codeGen.EmitCall(context.GetAvaloniaTypes().CompiledBindingPathBuilder.FindMethod(m => m.Name == "StreamTask").MakeGenericMethod(new[] { Type })); + } + } + + class SelfPathElementNode : IXamlIlBindingPathElementNode + { + public SelfPathElementNode(IXamlIlType type) + { + Type = type; + } + + public IXamlIlType Type { get; } + + public void Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + { + codeGen.EmitCall(context.GetAvaloniaTypes().CompiledBindingPathBuilder.FindMethod(m => m.Name == "Self")); + } + } + + class FindAncestorPathElementNode : IXamlIlBindingPathElementNode + { + private readonly int _level; + + public FindAncestorPathElementNode(IXamlIlType ancestorType, int level) + { + Type = ancestorType; + _level = level; + } + + public IXamlIlType Type { get; } + + public void Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + { + codeGen.Ldtype(Type) + .Ldc_I4(_level) + .EmitCall(context.GetAvaloniaTypes().CompiledBindingPathBuilder.FindMethod(m => m.Name == "FindAncestor")); + } + } + + class ElementNamePathElementNode : IXamlIlBindingPathElementNode + { + private readonly string _name; + + public ElementNamePathElementNode(string name, IXamlIlType elementType) + { + _name = name; + Type = elementType; + } + + public IXamlIlType Type { get; } + + public void Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + { + codeGen.Ldstr(_name) + .EmitCall(context.GetAvaloniaTypes().CompiledBindingPathBuilder.FindMethod(m => m.Name == "ElementName")); + } + } + + class XamlIlAvaloniaPropertyPropertyPathElementNode : IXamlIlBindingPathElementNode + { + private readonly IXamlIlField _field; + + public XamlIlAvaloniaPropertyPropertyPathElementNode(IXamlIlField field, IXamlIlType propertyType) + { + _field = field; + Type = propertyType; + } + + public void Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + => codeGen + .Ldsfld(_field) + .EmitCall(context.GetAvaloniaTypes() + .NotifyingPropertyInfoHelpers.FindMethod(m => m.Name == "CreateAvaloniaPropertyInfo")) + .EmitCall(context.GetAvaloniaTypes() + .CompiledBindingPathBuilder.FindMethod(m => m.Name == "Property")); + + public IXamlIlType Type { get; } + } + + class XamlIlClrPropertyPathElementNode : IXamlIlBindingPathElementNode + { + private readonly IXamlIlProperty _property; + + public XamlIlClrPropertyPathElementNode(IXamlIlProperty property) + { + _property = property; + } + + public void Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + { + context.Configuration.GetExtra() + .Emit(context, codeGen, _property); + + codeGen + .EmitCall(context.GetAvaloniaTypes() + .NotifyingPropertyInfoHelpers.FindMethod(m => m.Name == "CreateINPCPropertyInfo")) + .EmitCall(context.GetAvaloniaTypes() + .CompiledBindingPathBuilder.FindMethod(m => m.Name == "Property")); + } + + public IXamlIlType Type => _property.Getter?.ReturnType ?? _property.Setter?.Parameters[0]; + } + + class XamlIlClrIndexerPathElementNode : IXamlIlBindingPathElementNode + { + private readonly IXamlIlProperty _property; + private readonly List _values; + private readonly bool _isNotifyingCollection; + + public XamlIlClrIndexerPathElementNode(IXamlIlProperty property, List values, bool isNotifyingCollection) + { + _property = property; + _values = values; + _isNotifyingCollection = isNotifyingCollection; + } + + public void Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + { + var intType = context.Configuration.TypeSystem.GetType("System.Int32"); + context.Configuration.GetExtra() + .Emit(context, codeGen, _property, _values); + + if (_isNotifyingCollection + && + _values.Count == 1 + && _values[0].Type.GetClrType().Equals(intType)) + { + context.Emit(_values[0], codeGen, intType); + codeGen.EmitCall(context.GetAvaloniaTypes() + .NotifyingPropertyInfoHelpers.FindMethod(m => m.Name == "CreateIndexerPropertyInfo")); + } + else + { + codeGen.EmitCall(context.GetAvaloniaTypes() + .NotifyingPropertyInfoHelpers.FindMethod(m => m.Name == "CreateINPCPropertyInfo")); + } + + codeGen.EmitCall(context.GetAvaloniaTypes() + .CompiledBindingPathBuilder.FindMethod(m => m.Name == "Property")); + } + + public IXamlIlType Type => _property.Getter?.ReturnType ?? _property.Setter?.Parameters[0]; + } + + class XamlIlBindingPathNode : XamlIlAstNode, IXamlIlBindingPathNode, IXamlIlAstEmitableNode + { + private readonly List _elements; + + public XamlIlBindingPathNode(IXamlIlLineInfo lineInfo, + IXamlIlType bindingPathType, + List elements) : base(lineInfo) + { + Type = new XamlIlAstClrTypeReference(lineInfo, bindingPathType, false); + _elements = elements; + } + + public IXamlIlType BindingResultType => _elements[_elements.Count - 1].Type; + + public IXamlIlAstTypeReference Type { get; } + + public XamlIlNodeEmitResult Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + { + var types = context.GetAvaloniaTypes(); + codeGen.Newobj(types.CompiledBindingPathBuilder.FindConstructor()); + + foreach (var element in _elements) + { + element.Emit(context, codeGen); + } + + codeGen.EmitCall(types.CompiledBindingPathBuilder.FindMethod(m => m.Name == "Build")); + return XamlIlNodeEmitResult.Type(0, types.CompiledBindingPath); + } + } + } + + interface IXamlIlBindingPathNode : IXamlIlAstValueNode + { + IXamlIlType BindingResultType { get; } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlClrPropertyInfoHelper.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlClrPropertyInfoHelper.cs index 95ec8536fc..43d5a4eba1 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlClrPropertyInfoHelper.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlClrPropertyInfoHelper.cs @@ -23,8 +23,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions static string GetKey(IXamlIlProperty property) => property.Getter.DeclaringType.GetFullName() + "." + property.Name; - public IXamlIlType Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen, IXamlIlProperty property) + public IXamlIlType Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen, IXamlIlProperty property, IEnumerable indexerArguments = null) { + indexerArguments = indexerArguments ?? Enumerable.Empty(); var types = context.GetAvaloniaTypes(); IXamlIlMethod Get() { @@ -55,6 +56,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions cg.Unbox(m.DeclaringType); else cg.Castclass(m.DeclaringType); + + foreach (var indexerArg in indexerArguments) + { + context.Emit(indexerArg, cg, indexerArg.Type.GetClrType()); + } } var getter = property.Getter == null ? @@ -95,7 +101,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions var ctor = types.ClrPropertyInfo.Constructors.First(c => - c.Parameters.Count == 3 && c.IsStatic == false); + c.Parameters.Count == 4 && c.IsStatic == false); var cacheMiss = get.Generator.DefineLabel(); get.Generator @@ -124,6 +130,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions EmitFunc(get.Generator, getter, ctor.Parameters[1]); EmitFunc(get.Generator, setter, ctor.Parameters[2]); get.Generator + .Ldtype(property.PropertyType) .Newobj(ctor) .Stsfld(field) .Ldsfld(field) diff --git a/src/Markup/Avalonia.Markup/Data/Binding.cs b/src/Markup/Avalonia.Markup/Data/Binding.cs index 0b85e3224d..4b62b87199 100644 --- a/src/Markup/Avalonia.Markup/Data/Binding.cs +++ b/src/Markup/Avalonia.Markup/Data/Binding.cs @@ -17,14 +17,14 @@ namespace Avalonia.Data /// /// A XAML binding. /// - public class Binding : IBinding + public class Binding : BindingBase { /// /// Initializes a new instance of the class. /// public Binding() + :base() { - FallbackValue = AvaloniaProperty.UnsetValue; } /// @@ -33,47 +33,16 @@ namespace Avalonia.Data /// The binding path. /// The binding mode. public Binding(string path, BindingMode mode = BindingMode.Default) - : this() + : base(mode) { Path = path; - Mode = mode; } - /// - /// Gets or sets the to use. - /// - public IValueConverter Converter { get; set; } - - /// - /// Gets or sets a parameter to pass to . - /// - public object ConverterParameter { get; set; } - /// /// Gets or sets the name of the element to use as the binding source. /// public string ElementName { get; set; } - /// - /// Gets or sets the value to use when the binding is unable to produce a value. - /// - public object FallbackValue { get; set; } - - /// - /// Gets or sets the binding mode. - /// - public BindingMode Mode { get; set; } - - /// - /// Gets or sets the binding path. - /// - public string Path { get; set; } = ""; - - /// - /// Gets or sets the binding priority. - /// - public BindingPriority Priority { get; set; } - /// /// Gets or sets the relative source for the binding. /// @@ -85,64 +54,50 @@ namespace Avalonia.Data public object Source { get; set; } /// - /// Gets or sets the string format. + /// Gets or sets the binding path. /// - public string StringFormat { get; set; } - - public WeakReference DefaultAnchor { get; set; } + public string Path { get; set; } = ""; /// /// Gets or sets a function used to resolve types from names in the binding path. /// public Func TypeResolver { get; set; } - /// - public InstancedBinding Initiate( - IAvaloniaObject target, - AvaloniaProperty targetProperty, - object anchor = null, - bool enableDataValidation = false) + protected override ExpressionObserver CreateExpressionObserver(IAvaloniaObject target, AvaloniaProperty targetProperty, object anchor, bool enableDataValidation) { - Contract.Requires(target != null); - anchor = anchor ?? DefaultAnchor?.Target; - - enableDataValidation = enableDataValidation && Priority == BindingPriority.LocalValue; - - ExpressionObserver observer; - - var (node, mode) = ExpressionObserverBuilder.Parse(Path, enableDataValidation, TypeResolver); + var (node, mode) = ExpressionObserverBuilder.Parse(Path, enableDataValidation, TypeResolver); if (ElementName != null) { - observer = CreateElementObserver( + return CreateElementObserver( (target as IStyledElement) ?? (anchor as IStyledElement), ElementName, node); } else if (Source != null) { - observer = CreateSourceObserver(Source, node); + return CreateSourceObserver(Source, node); } else if (RelativeSource == null) { if (mode == SourceMode.Data) { - observer = CreateDataContextObserver( + return CreateDataContextObserver( target, node, targetProperty == StyledElement.DataContextProperty, - anchor); + anchor); } else { - observer = new ExpressionObserver( + return CreateSourceObserver( (target as IStyledElement) ?? (anchor as IStyledElement), node); } } else if (RelativeSource.Mode == RelativeSourceMode.DataContext) { - observer = CreateDataContextObserver( + return CreateDataContextObserver( target, node, targetProperty == StyledElement.DataContextProperty, @@ -150,13 +105,13 @@ namespace Avalonia.Data } else if (RelativeSource.Mode == RelativeSourceMode.Self) { - observer = CreateSourceObserver( + return CreateSourceObserver( (target as IStyledElement) ?? (anchor as IStyledElement), node); } else if (RelativeSource.Mode == RelativeSourceMode.TemplatedParent) { - observer = CreateTemplatedParentObserver( + return CreateTemplatedParentObserver( (target as IStyledElement) ?? (anchor as IStyledElement), node); } @@ -167,7 +122,7 @@ namespace Avalonia.Data throw new InvalidOperationException("AncestorType must be set for RelativeSourceMode.FindAncestor when searching the visual tree."); } - observer = CreateFindAncestorObserver( + return CreateFindAncestorObserver( (target as IStyledElement) ?? (anchor as IStyledElement), RelativeSource, node); @@ -177,192 +132,6 @@ namespace Avalonia.Data throw new NotSupportedException(); } - var fallback = FallbackValue; - - // If we're binding to DataContext and our fallback is UnsetValue then override - // the fallback value to null, as broken bindings to DataContext must reset the - // DataContext in order to not propagate incorrect DataContexts to child controls. - // See Avalonia.Markup.UnitTests.Data.DataContext_Binding_Should_Produce_Correct_Results. - if (targetProperty == StyledElement.DataContextProperty && fallback == AvaloniaProperty.UnsetValue) - { - fallback = null; - } - - var converter = Converter; - var targetType = targetProperty?.PropertyType ?? typeof(object); - - // We only respect `StringFormat` if the type of the property we're assigning to will - // accept a string. Note that this is slightly different to WPF in that WPF only applies - // `StringFormat` for target type `string` (not `object`). - if (!string.IsNullOrWhiteSpace(StringFormat) && - (targetType == typeof(string) || targetType == typeof(object))) - { - converter = new StringFormatValueConverter(StringFormat, converter); - } - - var subject = new BindingExpression( - observer, - targetType, - fallback, - converter ?? DefaultValueConverter.Instance, - ConverterParameter, - Priority); - - return new InstancedBinding(subject, Mode, Priority); - } - - private ExpressionObserver CreateDataContextObserver( - IAvaloniaObject target, - ExpressionNode node, - bool targetIsDataContext, - object anchor) - { - Contract.Requires(target != null); - - if (!(target is IStyledElement)) - { - target = anchor as IStyledElement; - - if (target == null) - { - throw new InvalidOperationException("Cannot find a DataContext to bind to."); - } - } - - if (!targetIsDataContext) - { - var result = new ExpressionObserver( - () => target.GetValue(StyledElement.DataContextProperty), - node, - new UpdateSignal(target, StyledElement.DataContextProperty), - null); - - return result; - } - else - { - return new ExpressionObserver( - GetParentDataContext(target), - node, - null); - } - } - - private ExpressionObserver CreateElementObserver( - IStyledElement target, - string elementName, - ExpressionNode node) - { - Contract.Requires(target != null); - - var result = new ExpressionObserver( - ControlLocator.Track(target, elementName), - node, - null); - return result; - } - - private ExpressionObserver CreateFindAncestorObserver( - IStyledElement target, - RelativeSource relativeSource, - ExpressionNode node) - { - Contract.Requires(target != null); - - IObservable controlLocator; - - switch (relativeSource.Tree) - { - case TreeType.Logical: - controlLocator = ControlLocator.Track( - (ILogical)target, - relativeSource.AncestorLevel - 1, - relativeSource.AncestorType); - break; - case TreeType.Visual: - controlLocator = VisualLocator.Track( - (IVisual)target, - relativeSource.AncestorLevel - 1, - relativeSource.AncestorType); - break; - default: - throw new InvalidOperationException("Invalid tree to traverse."); - } - - return new ExpressionObserver( - controlLocator, - node, - null); - } - - private ExpressionObserver CreateSourceObserver( - object source, - ExpressionNode node) - { - Contract.Requires(source != null); - - return new ExpressionObserver(source, node); - } - - private ExpressionObserver CreateTemplatedParentObserver( - IAvaloniaObject target, - ExpressionNode node) - { - Contract.Requires(target != null); - - var result = new ExpressionObserver( - () => target.GetValue(StyledElement.TemplatedParentProperty), - node, - new UpdateSignal(target, StyledElement.TemplatedParentProperty), - null); - - return result; - } - - private IObservable GetParentDataContext(IAvaloniaObject target) - { - // The DataContext is based on the visual parent and not the logical parent: this may - // seem counter intuitive considering the fact that property inheritance works on the logical - // tree, but consider a ContentControl with a ContentPresenter. The ContentControl's - // Content property is bound to a value which becomes the ContentPresenter's - // DataContext - it is from this that the child hosted by the ContentPresenter needs to - // inherit its DataContext. - return target.GetObservable(Visual.VisualParentProperty) - .Select(x => - { - return (x as IAvaloniaObject)?.GetObservable(StyledElement.DataContextProperty) ?? - Observable.Return((object)null); - }).Switch(); - } - - private class UpdateSignal : SingleSubscriberObservableBase - { - private readonly IAvaloniaObject _target; - private readonly AvaloniaProperty _property; - - public UpdateSignal(IAvaloniaObject target, AvaloniaProperty property) - { - _target = target; - _property = property; - } - - protected override void Subscribed() - { - _target.PropertyChanged += PropertyChanged; - } - - protected override void Unsubscribed() - { - _target.PropertyChanged -= PropertyChanged; - } - - private void PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) - { - if (e.Property == _property) - { - PublishNext(Unit.Default); - } - } } } } diff --git a/src/Markup/Avalonia.Markup/Data/BindingBase.cs b/src/Markup/Avalonia.Markup/Data/BindingBase.cs new file mode 100644 index 0000000000..9c4ea27fda --- /dev/null +++ b/src/Markup/Avalonia.Markup/Data/BindingBase.cs @@ -0,0 +1,275 @@ + +using System; +using System.Linq; +using System.Reactive; +using System.Reactive.Linq; +using Avalonia.Data.Converters; +using Avalonia.Data.Core; +using Avalonia.LogicalTree; +using Avalonia.Markup.Parsers; +using Avalonia.Reactive; +using Avalonia.VisualTree; + + +namespace Avalonia.Data +{ + public abstract class BindingBase + { + /// + /// Initializes a new instance of the class. + /// + public BindingBase() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The binding mode. + public BindingBase(BindingMode mode = BindingMode.Default) + { + FallbackValue = AvaloniaProperty.UnsetValue; + Mode = mode; + } + + /// + /// Gets or sets the to use. + /// + public IValueConverter Converter { get; set; } + + /// + /// Gets or sets a parameter to pass to . + /// + public object ConverterParameter { get; set; } + + /// + /// Gets or sets the value to use when the binding is unable to produce a value. + /// + public object FallbackValue { get; set; } + + /// + /// Gets or sets the binding mode. + /// + public BindingMode Mode { get; set; } + + /// + /// Gets or sets the binding priority. + /// + public BindingPriority Priority { get; set; } + + /// + /// Gets or sets the string format. + /// + public string StringFormat { get; set; } + + public WeakReference DefaultAnchor { get; set; } + + protected abstract ExpressionObserver CreateExpressionObserver( + IAvaloniaObject target, + AvaloniaProperty targetProperty, + object anchor, + bool enableDataValidation); + + /// + public InstancedBinding Initiate( + IAvaloniaObject target, + AvaloniaProperty targetProperty, + object anchor = null, + bool enableDataValidation = false) + { + Contract.Requires(target != null); + anchor = anchor ?? DefaultAnchor?.Target; + + enableDataValidation = enableDataValidation && Priority == BindingPriority.LocalValue; + + var observer = CreateExpressionObserver(target, targetProperty, anchor, enableDataValidation); + + var fallback = FallbackValue; + + // If we're binding to DataContext and our fallback is UnsetValue then override + // the fallback value to null, as broken bindings to DataContext must reset the + // DataContext in order to not propagate incorrect DataContexts to child controls. + // See Avalonia.Markup.UnitTests.Data.DataContext_Binding_Should_Produce_Correct_Results. + if (targetProperty == StyledElement.DataContextProperty && fallback == AvaloniaProperty.UnsetValue) + { + fallback = null; + } + + var converter = Converter; + var targetType = targetProperty?.PropertyType ?? typeof(object); + + // We only respect `StringFormat` if the type of the property we're assigning to will + // accept a string. Note that this is slightly different to WPF in that WPF only applies + // `StringFormat` for target type `string` (not `object`). + if (!string.IsNullOrWhiteSpace(StringFormat) && + (targetType == typeof(string) || targetType == typeof(object))) + { + converter = new StringFormatValueConverter(StringFormat, converter); + } + + var subject = new BindingExpression( + observer, + targetType, + fallback, + converter ?? DefaultValueConverter.Instance, + ConverterParameter, + Priority); + + return new InstancedBinding(subject, Mode, Priority); + } + + protected ExpressionObserver CreateDataContextObserver( + IAvaloniaObject target, + ExpressionNode node, + bool targetIsDataContext, + object anchor) + { + Contract.Requires(target != null); + + if (!(target is IStyledElement)) + { + target = anchor as IStyledElement; + + if (target == null) + { + throw new InvalidOperationException("Cannot find a DataContext to bind to."); + } + } + + if (!targetIsDataContext) + { + var result = new ExpressionObserver( + () => target.GetValue(StyledElement.DataContextProperty), + node, + new UpdateSignal(target, StyledElement.DataContextProperty), + null); + + return result; + } + else + { + return new ExpressionObserver( + GetParentDataContext(target), + node, + null); + } + } + + protected ExpressionObserver CreateElementObserver( + IStyledElement target, + string elementName, + ExpressionNode node) + { + Contract.Requires(target != null); + + var result = new ExpressionObserver( + ControlLocator.Track(target, elementName), + node, + null); + return result; + } + + protected ExpressionObserver CreateFindAncestorObserver( + IStyledElement target, + RelativeSource relativeSource, + ExpressionNode node) + { + Contract.Requires(target != null); + + IObservable controlLocator; + + switch (relativeSource.Tree) + { + case TreeType.Logical: + controlLocator = ControlLocator.Track( + (ILogical)target, + relativeSource.AncestorLevel - 1, + relativeSource.AncestorType); + break; + case TreeType.Visual: + controlLocator = VisualLocator.Track( + (IVisual)target, + relativeSource.AncestorLevel - 1, + relativeSource.AncestorType); + break; + default: + throw new InvalidOperationException("Invalid tree to traverse."); + } + + return new ExpressionObserver( + controlLocator, + node, + null); + } + + protected ExpressionObserver CreateSourceObserver( + object source, + ExpressionNode node) + { + Contract.Requires(source != null); + + return new ExpressionObserver(source, node); + } + + protected ExpressionObserver CreateTemplatedParentObserver( + IAvaloniaObject target, + ExpressionNode node) + { + Contract.Requires(target != null); + + var result = new ExpressionObserver( + () => target.GetValue(StyledElement.TemplatedParentProperty), + node, + new UpdateSignal(target, StyledElement.TemplatedParentProperty), + null); + + return result; + } + + protected IObservable GetParentDataContext(IAvaloniaObject target) + { + // The DataContext is based on the visual parent and not the logical parent: this may + // seem counter intuitive considering the fact that property inheritance works on the logical + // tree, but consider a ContentControl with a ContentPresenter. The ContentControl's + // Content property is bound to a value which becomes the ContentPresenter's + // DataContext - it is from this that the child hosted by the ContentPresenter needs to + // inherit its DataContext. + return target.GetObservable(Visual.VisualParentProperty) + .Select(x => + { + return (x as IAvaloniaObject)?.GetObservable(StyledElement.DataContextProperty) ?? + Observable.Return((object)null); + }).Switch(); + } + + private class UpdateSignal : SingleSubscriberObservableBase + { + private readonly IAvaloniaObject _target; + private readonly AvaloniaProperty _property; + + public UpdateSignal(IAvaloniaObject target, AvaloniaProperty property) + { + _target = target; + _property = property; + } + + protected override void Subscribed() + { + _target.PropertyChanged += PropertyChanged; + } + + protected override void Unsubscribed() + { + _target.PropertyChanged -= PropertyChanged; + } + + private void PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == _property) + { + PublishNext(Unit.Default); + } + } + } + } +} diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs index fd6f7b20bd..4d88b34659 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs @@ -7,7 +7,10 @@ using Avalonia.Utilities; namespace Avalonia.Markup.Parsers { - internal static class ArgumentListParser +#if !BUILDTASK + public +#endif + static class ArgumentListParser { public static IList ParseArguments(this ref CharacterReader r, char open, char close, char delimiter = ',') { diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs index b8308155fe..7181bbaa69 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs @@ -2,19 +2,18 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using Avalonia.Data.Core; -using Avalonia.Markup.Parsers.Nodes; using Avalonia.Utilities; using System; using System.Collections.Generic; -using System.Linq; namespace Avalonia.Markup.Parsers { - internal enum SourceMode + public enum SourceMode { Data, Control } + internal static class BindingExpressionGrammar { public static (IList Nodes, SourceMode Mode) Parse(ref CharacterReader r) @@ -191,7 +190,7 @@ namespace Avalonia.Markup.Parsers { var name = r.ParseIdentifier(); - if (name == null) + if (name.IsEmpty) { throw new ExpressionParseException(r.Position, "Element name expected after '#'."); } @@ -204,11 +203,11 @@ namespace Avalonia.Markup.Parsers { var mode = r.ParseIdentifier(); - if (mode.Equals("self".AsSpan(), StringComparison.InvariantCulture)) + if (mode.SequenceEqual("self".AsSpan())) { nodes.Add(new SelfNode()); } - else if (mode.Equals("parent".AsSpan(), StringComparison.InvariantCulture)) + else if (mode.SequenceEqual("parent".AsSpan())) { string ancestorNamespace = null; string ancestorType = null; diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs index f09efca7d0..7803a7227d 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs @@ -4,7 +4,7 @@ using Avalonia.LogicalTree; namespace Avalonia.Markup.Parsers.Nodes { - internal class ElementNameNode : ExpressionNode + public class ElementNameNode : ExpressionNode { private readonly string _name; private IDisposable _subscription; diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs index 221df44327..9f441ad360 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs @@ -4,7 +4,7 @@ using Avalonia.LogicalTree; namespace Avalonia.Markup.Parsers.Nodes { - internal class FindAncestorNode : ExpressionNode + public class FindAncestorNode : ExpressionNode { private readonly int _level; private readonly Type _ancestorType; diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/SelfNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/SelfNode.cs index 2cb87efa65..1cd233c68a 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/SelfNode.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/SelfNode.cs @@ -2,7 +2,7 @@ namespace Avalonia.Markup.Parsers.Nodes { - internal class SelfNode : ExpressionNode + public class SelfNode : ExpressionNode { public override string Description => "$self"; } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs new file mode 100644 index 0000000000..59daf95637 --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Avalonia.Controls; +using Avalonia.Markup.Data; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions +{ + public class CompiledBindingExtensionTests + { + [Fact] + public void ResolvesClrPropertyBasedOnDataContextType() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + DelayedBinding.ApplyBindings(textBlock); + + var dataContext = new TestDataContext + { + StringProperty = "foobar" + }; + + window.DataContext = dataContext; + + Assert.Equal(dataContext.StringProperty, textBlock.Text); + } + } + } + + public class TestDataContext + { + public string StringProperty { get; set; } + } +} diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs index 6bc7f33039..8eaa391a5f 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs @@ -290,8 +290,19 @@ namespace Avalonia.Markup.Xaml.UnitTests Assert.Equal(typeof(ScaleTransform), ((EnsureTypePropertyPathElement)s3e[1]).Type); Assert.IsType(s3e[2]); Assert.Equal("ScaleX", ((AvaloniaProperty)((PropertyPropertyPathElement)s3e[3]).Property).Name); - - + } + } + + [Fact] + public void DataContextType_Resolution() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var parsed = AvaloniaXamlLoader.Parse(@" +"); } } } From b1a2ea21c586b0334a89f0ddd9c252075c9c9a4e Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sat, 6 Jul 2019 12:42:54 -0700 Subject: [PATCH 011/693] Fix stream bindings. --- .../XamlIlBindingPathHelper.cs | 2 +- .../Avalonia.Markup/Data/BindingBase.cs | 2 +- .../CompiledBindingExtensionTests.cs | 32 +++++++++++++++++++ 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlBindingPathHelper.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlBindingPathHelper.cs index c98e2029c4..dfa90d1989 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlBindingPathHelper.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlBindingPathHelper.cs @@ -80,7 +80,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions appendNotNode = !appendNotNode; break; case BindingExpressionGrammar.StreamNode _: - var observableType = targetType.GetAllInterfaces().FirstOrDefault(i => i.GenericTypeDefinition.Equals(context.Configuration.TypeSystem.FindType("System.IObservable`1"))); + var observableType = targetType.GetAllInterfaces().FirstOrDefault(i => i.GenericTypeDefinition?.Equals(context.Configuration.TypeSystem.FindType("System.IObservable`1")) ?? false); if (observableType != null) { nodes.Add(new XamlIlStreamObservablePathElementNode(observableType.GenericArguments[0])); diff --git a/src/Markup/Avalonia.Markup/Data/BindingBase.cs b/src/Markup/Avalonia.Markup/Data/BindingBase.cs index 9c4ea27fda..16ada21f56 100644 --- a/src/Markup/Avalonia.Markup/Data/BindingBase.cs +++ b/src/Markup/Avalonia.Markup/Data/BindingBase.cs @@ -13,7 +13,7 @@ using Avalonia.VisualTree; namespace Avalonia.Data { - public abstract class BindingBase + public abstract class BindingBase : IBinding { /// /// Initializes a new instance of the class. diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index 59daf95637..3d1a5cb566 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Text; +using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Markup.Data; using Avalonia.UnitTests; @@ -38,10 +39,41 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions Assert.Equal(dataContext.StringProperty, textBlock.Text); } } + + [Fact] + public void ResolvesStreamBindingCorrectly() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + DelayedBinding.ApplyBindings(textBlock); + + var dataContext = new TestDataContext + { + TaskProperty = Task.FromResult("foobar") + }; + + window.DataContext = dataContext; + + Assert.Equal(dataContext.TaskProperty.Result, textBlock.Text); + } + } } public class TestDataContext { public string StringProperty { get; set; } + + public Task TaskProperty { get; set; } } } From 590f5923a85d9ae72e649517282f190f233dc68a Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sat, 6 Jul 2019 21:13:42 -0700 Subject: [PATCH 012/693] Traverse transform nodes in the correct order when setting a binding source value. --- .../Data/Core/ExpressionObserver.cs | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs index 65f26df011..9b62686ad5 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs @@ -57,6 +57,7 @@ namespace Avalonia.Data.Core private object _root; private IDisposable _rootSubscription; private WeakReference _value; + private IReadOnlyList _transformNodes; /// /// Initializes a new instance of the class. @@ -191,6 +192,24 @@ namespace Avalonia.Data.Core description ?? expression.ToString()); } + private IReadOnlyList GetTransformNodesFromChain() + { + LinkedList transforms = new LinkedList(); + var node = _node; + while (node != null) + { + if (node is ITransformNode transform) + { + transforms.AddFirst(transform); + } + node = node.Next; + } + + return new List(transforms); + } + + private IReadOnlyList TransformNodes => (_transformNodes ?? (_transformNodes = GetTransformNodesFromChain())); + /// /// Attempts to set the value of a property expression. /// @@ -206,18 +225,13 @@ namespace Avalonia.Data.Core { if (Leaf is SettableNode settable) { - var node = _node; - while (node != null) + foreach (var transform in TransformNodes) { - if (node is ITransformNode transform) + value = transform.Transform(value); + if (value is BindingNotification) { - value = transform.Transform(value); - if (value is BindingNotification) - { - return false; - } + return false; } - node = node.Next; } return settable.SetTargetValue(value, priority); } From ac3be6c662d56a3bb467227b6b3f29bfd94d66cf Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sat, 6 Jul 2019 21:18:08 -0700 Subject: [PATCH 013/693] Fix handling of Not nodes. --- .../XamlIlBindingPathHelper.cs | 37 +++++++++++++------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlBindingPathHelper.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlBindingPathHelper.cs index dfa90d1989..598f9175e8 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlBindingPathHelper.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlBindingPathHelper.cs @@ -67,7 +67,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions private static IXamlIlBindingPathNode TransformBindingPath(XamlIlAstTransformationContext context, IXamlIlLineInfo lineInfo, IXamlIlType startType, IEnumerable bindingExpression) { - bool appendNotNode = false; + List transformNodes = new List(); List nodes = new List(); foreach (var astNode in bindingExpression) { @@ -77,10 +77,19 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions case BindingExpressionGrammar.EmptyExpressionNode _: break; case BindingExpressionGrammar.NotNode _: - appendNotNode = !appendNotNode; + transformNodes.Add(new XamlIlNotPathElementNode(context.Configuration.WellKnownTypes.Boolean)); break; case BindingExpressionGrammar.StreamNode _: - var observableType = targetType.GetAllInterfaces().FirstOrDefault(i => i.GenericTypeDefinition?.Equals(context.Configuration.TypeSystem.FindType("System.IObservable`1")) ?? false); + IXamlIlType observableType; + if (targetType.GenericTypeDefinition?.Equals(context.Configuration.TypeSystem.FindType("System.IObservable`1")) == true) + { + observableType = targetType; + } + else + { + observableType = targetType.GetAllInterfaces().FirstOrDefault(i => i.GenericTypeDefinition?.Equals(context.Configuration.TypeSystem.FindType("System.IObservable`1")) ?? false); + } + if (observableType != null) { nodes.Add(new XamlIlStreamObservablePathElementNode(observableType.GenericArguments[0])); @@ -179,16 +188,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions nodes.Add(new ElementNamePathElementNode(elementName.Name, elementType)); break; } - } - if (appendNotNode) - { - // TODO: Fix Not behavior - nodes.Add(new XamlIlNotPathElementNode(context.Configuration.WellKnownTypes.Boolean)); - } - - return new XamlIlBindingPathNode(lineInfo, context.GetAvaloniaTypes().CompiledBindingPath, nodes); + return new XamlIlBindingPathNode(lineInfo, context.GetAvaloniaTypes().CompiledBindingPath, transformNodes, nodes); IXamlIlType GetType(string ns, string name) { @@ -422,17 +424,23 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions class XamlIlBindingPathNode : XamlIlAstNode, IXamlIlBindingPathNode, IXamlIlAstEmitableNode { + private readonly List _transformElements; private readonly List _elements; public XamlIlBindingPathNode(IXamlIlLineInfo lineInfo, IXamlIlType bindingPathType, + List transformElements, List elements) : base(lineInfo) { Type = new XamlIlAstClrTypeReference(lineInfo, bindingPathType, false); + _transformElements = transformElements; _elements = elements; } - public IXamlIlType BindingResultType => _elements[_elements.Count - 1].Type; + public IXamlIlType BindingResultType + => _transformElements.Count > 0 + ? _transformElements[0].Type + : _elements[_elements.Count - 1].Type; public IXamlIlAstTypeReference Type { get; } @@ -441,6 +449,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions var types = context.GetAvaloniaTypes(); codeGen.Newobj(types.CompiledBindingPathBuilder.FindConstructor()); + foreach (var transform in _transformElements) + { + transform.Emit(context, codeGen); + } + foreach (var element in _elements) { element.Emit(context, codeGen); From c836c7700220e786ece045a868003eefdae49932 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sat, 6 Jul 2019 21:18:45 -0700 Subject: [PATCH 014/693] Make SourceMode internal since it doesn't need to be public. --- .../MarkupExtensions/CompiledBindingExtension.cs | 7 ++----- .../CompiledBindings/CompiledBindingPath.cs | 7 +++---- .../Markup/Parsers/BindingExpressionGrammar.cs | 2 +- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs index bc1e2cdd36..5c3628fd22 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs @@ -1,13 +1,10 @@ -extern alias Markup; -using System; +using System; using Avalonia.Data; using Avalonia.Controls; using Avalonia.Styling; using Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings; using Avalonia.Data.Core; - -using SourceMode = Markup::Avalonia.Markup.Parsers.SourceMode; -using Avalonia.Data.Converters; +using Avalonia.Markup.Parsers; namespace Avalonia.Markup.Xaml.MarkupExtensions { diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs index 81b9bd29b9..7daa90ec44 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs @@ -1,10 +1,9 @@ -extern alias Markup; -using System; +using System; using System.Collections.Generic; using Avalonia.Data.Core; using Avalonia.Data.Core.Plugins; +using Avalonia.Markup.Parsers; using Avalonia.Markup.Parsers.Nodes; -using SourceMode = Markup::Avalonia.Markup.Parsers.SourceMode; namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings { @@ -56,7 +55,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings return pathRoot ?? new EmptyExpressionNode(); } - public SourceMode SourceMode => _elements.Count > 0 && _elements[0] is IControlSourceBindingPathElement ? SourceMode.Control : SourceMode.Data; + internal SourceMode SourceMode => _elements.Count > 0 && _elements[0] is IControlSourceBindingPathElement ? SourceMode.Control : SourceMode.Data; } public class CompiledBindingPathBuilder diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs index 7181bbaa69..a58068cc5b 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs @@ -8,7 +8,7 @@ using System.Collections.Generic; namespace Avalonia.Markup.Parsers { - public enum SourceMode + internal enum SourceMode { Data, Control From ee46a69cefe644b0d507501d2004f51c76ec895b Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sat, 6 Jul 2019 21:19:49 -0700 Subject: [PATCH 015/693] Add additional test for IObservable compiled bindings. --- .../CompiledBindingExtensionTests.cs | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index 3d1a5cb566..34bb071aef 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Reactive.Subjects; using System.Text; using System.Threading.Tasks; using Avalonia.Controls; @@ -41,7 +42,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions } [Fact] - public void ResolvesStreamBindingCorrectly() + public void ResolvesStreamTaskBindingCorrectly() { using (UnitTestApplication.Start(TestServices.StyledWindow)) { @@ -68,6 +69,38 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions Assert.Equal(dataContext.TaskProperty.Result, textBlock.Text); } } + + [Fact] + public void ResolvesStreamObservableBindingCorrectly() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + DelayedBinding.ApplyBindings(textBlock); + + var subject = new Subject(); + var dataContext = new TestDataContext + { + ObservableProperty = subject + }; + + window.DataContext = dataContext; + + subject.OnNext("foobar"); + + Assert.Equal("foobar", textBlock.Text); + } + } } public class TestDataContext @@ -75,5 +108,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions public string StringProperty { get; set; } public Task TaskProperty { get; set; } + + public IObservable ObservableProperty { get; set; } } } From b173d5b5fb3e4d7346c8ec7248ef9d75bec75fbf Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Sat, 6 Jul 2019 21:20:12 -0700 Subject: [PATCH 016/693] Change the observable stream binding in BindingDemo to be a compiled binding. --- samples/BindingDemo/MainWindow.xaml | 5 +++-- samples/BindingDemo/ViewModels/MainWindowViewModel.cs | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/samples/BindingDemo/MainWindow.xaml b/samples/BindingDemo/MainWindow.xaml index a232a06383..6c4a071214 100644 --- a/samples/BindingDemo/MainWindow.xaml +++ b/samples/BindingDemo/MainWindow.xaml @@ -5,7 +5,8 @@ xmlns:local="clr-namespace:BindingDemo" Title="AvaloniaUI Bindings Test" Width="800" - Height="600"> + Height="600" + x:DataContextType="{x:Type vm:MainWindowViewModel}"> + - - + + + + diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs index 6fa9fc515e..8f5d380078 100644 --- a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs @@ -70,6 +70,26 @@ namespace ControlCatalog.Pages MinItemHeight = 200, }; break; + case 4: + _scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto; + _scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Disabled; + _repeater.Layout = new WrapLayout + { + Orientation = Orientation.Vertical, + HorizontalSpacing = 20, + VerticalSpacing = 20 + }; + break; + case 5: + _scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled; + _scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Auto; + _repeater.Layout = new WrapLayout + { + Orientation = Orientation.Horizontal, + HorizontalSpacing = 20, + VerticalSpacing = 20 + }; + break; } } From 1c0e981343281705ffee4c692bf5036befb55f01 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sun, 24 May 2020 21:53:44 -0300 Subject: [PATCH 050/693] import sources. --- .../RelativePanel.AttachedProperties.cs | 511 ++++++++++++++++++ src/Avalonia.Controls/RelativePanel.cs | 424 +++++++++++++++ 2 files changed, 935 insertions(+) create mode 100644 src/Avalonia.Controls/RelativePanel.AttachedProperties.cs create mode 100644 src/Avalonia.Controls/RelativePanel.cs diff --git a/src/Avalonia.Controls/RelativePanel.AttachedProperties.cs b/src/Avalonia.Controls/RelativePanel.AttachedProperties.cs new file mode 100644 index 0000000000..21baff87ab --- /dev/null +++ b/src/Avalonia.Controls/RelativePanel.AttachedProperties.cs @@ -0,0 +1,511 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Avalonia.Controls +{ + public partial class RelativePanel + { + private static void OnAlignPropertiesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var elm = d as FrameworkElement; + if (elm.Parent is FrameworkElement) + ((FrameworkElement)elm.Parent).InvalidateArrange(); + } + + /// + /// Gets the value of the RelativePanel.Above XAML attached property for the target element. + /// + /// The object from which the property value is read. + /// + /// The RelativePanel.Above XAML attached property value of the specified object. + /// (The element to position this element above.) + /// + [TypeConverter(typeof(NameReferenceConverter))] + public static object GetAbove(DependencyObject obj) + { + return (object)obj.GetValue(AboveProperty); + } + + /// + /// Sets the value of the RelativePanel.Above XAML attached property for a target element. + /// + /// The object to which the property value is written. + /// The value to set. (The element to position this element above.) + public static void SetAbove(DependencyObject obj, object value) + { + obj.SetValue(AboveProperty, value); + } + + /// + /// Identifies the XAML attached property. + /// + public static readonly DependencyProperty AboveProperty = + DependencyProperty.RegisterAttached("Above", typeof(object), typeof(RelativePanel), new PropertyMetadata(null, OnAlignPropertiesChanged)); + + + /// + /// Gets the value of the RelativePanel.AlignBottomWithPanel XAML attached property for the target element. + /// + /// The object from which the property value is read. + /// + /// The RelativePanel.AlignBottomWithPanel XAML attached property value of the specified + /// object. (true to align this element's bottom edge with the panel's bottom edge; + /// otherwise, false.) + /// + public static bool GetAlignBottomWithPanel(DependencyObject obj) + { + return (bool)obj.GetValue(AlignBottomWithPanelProperty); + } + + /// + /// Sets the value of the RelativePanel.Above XAML attached property for a target element. + /// + /// The object to which the property value is written. + /// + /// The value to set. (true to align this element's bottom edge with the panel's + /// bottom edge; otherwise, false.) + /// + public static void SetAlignBottomWithPanel(DependencyObject obj, bool value) + { + obj.SetValue(AlignBottomWithPanelProperty, value); + } + + /// + /// Identifies the XAML attached property. + /// + public static readonly DependencyProperty AlignBottomWithPanelProperty = + DependencyProperty.RegisterAttached("AlignBottomWithPanel", typeof(bool), typeof(RelativePanel), new PropertyMetadata(false, OnAlignPropertiesChanged)); + + /// + /// Gets the value of the RelativePanel.AlignBottomWith XAML attached property for the target element. + /// + /// The object from which the property value is read. + /// + /// The RelativePanel.AlignBottomWith XAML attached property value of the specified object. + /// (The element to align this element's bottom edge with.) + /// + [TypeConverter(typeof(NameReferenceConverter))] + public static object GetAlignBottomWith(DependencyObject obj) + { + return (object)obj.GetValue(AlignBottomWithProperty); + } + + /// + /// Sets the value of the RelativePanel.Above XAML attached property for a target element. + /// + /// The object to which the property value is written. + /// The value to set. (The element to align this element's bottom edge with.) + public static void SetAlignBottomWith(DependencyObject obj, object value) + { + obj.SetValue(AlignBottomWithProperty, value); + } + + /// + /// Identifies the XAML attached property. + /// + public static readonly DependencyProperty AlignBottomWithProperty = + DependencyProperty.RegisterAttached("AlignBottomWith", typeof(object), typeof(RelativePanel), new PropertyMetadata(null, OnAlignPropertiesChanged)); + + /// + /// Gets the value of the RelativePanel.AlignHorizontalCenterWithPanel XAML attached property for the target element. + /// + /// The object from which the property value is read. + /// + /// The RelativePanel.AlignHorizontalCenterWithPanel XAML attached property value + /// of the specified object. (true to horizontally center this element in the panel; + /// otherwise, false.) + /// + public static bool GetAlignHorizontalCenterWithPanel(DependencyObject obj) + { + return (bool)obj.GetValue(AlignHorizontalCenterWithPanelProperty); + } + + /// + /// Sets the value of the RelativePanel.Above XAML attached property for a target element. + /// + /// The object to which the property value is written. + /// + /// The value to set. (true to horizontally center this element in the panel; otherwise, + /// false.) + /// + public static void SetAlignHorizontalCenterWithPanel(DependencyObject obj, bool value) + { + obj.SetValue(AlignHorizontalCenterWithPanelProperty, value); + } + + /// + /// Identifies the XAML attached property. + /// + public static readonly DependencyProperty AlignHorizontalCenterWithPanelProperty = + DependencyProperty.RegisterAttached("AlignHorizontalCenterWithPanel", typeof(bool), typeof(RelativePanel), new PropertyMetadata(false, OnAlignPropertiesChanged)); + + /// + /// Gets the value of the RelativePanel.AlignHorizontalCenterWith XAML attached property for the target element. + /// + /// The object from which the property value is read. + /// + /// The RelativePanel.AlignHorizontalCenterWith XAML attached property value of the + /// specified object. (The element to align this element's horizontal center with.) + /// + [TypeConverter(typeof(NameReferenceConverter))] + public static object GetAlignHorizontalCenterWith(DependencyObject obj) + { + return (object)obj.GetValue(AlignHorizontalCenterWithProperty); + } + + /// + /// Sets the value of the RelativePanel.Above XAML attached property for a target element. + /// + /// The object to which the property value is written. + /// The value to set. (The element to align this element's horizontal center with.) + public static void SetAlignHorizontalCenterWith(DependencyObject obj, object value) + { + obj.SetValue(AlignHorizontalCenterWithProperty, value); + } + + /// + /// Identifies the XAML attached property. + /// + public static readonly DependencyProperty AlignHorizontalCenterWithProperty = + DependencyProperty.RegisterAttached("AlignHorizontalCenterWith", typeof(object), typeof(RelativePanel), new PropertyMetadata(null, OnAlignPropertiesChanged)); + + /// + /// Gets the value of the RelativePanel.AlignLeftWithPanel XAML attached property for the target element. + /// + /// The object from which the property value is read. + /// + /// The RelativePanel.AlignLeftWithPanel XAML attached property value of the specified + /// object. (true to align this element's left edge with the panel's left edge; otherwise, + /// false.) + /// + public static bool GetAlignLeftWithPanel(DependencyObject obj) + { + return (bool)obj.GetValue(AlignLeftWithPanelProperty); + } + + /// + /// Sets the value of the RelativePanel.Above XAML attached property for a target element. + /// + /// The object to which the property value is written. + /// + /// The value to set. (true to align this element's left edge with the panel's left + /// edge; otherwise, false.) + /// + public static void SetAlignLeftWithPanel(DependencyObject obj, bool value) + { + obj.SetValue(AlignLeftWithPanelProperty, value); + } + + /// + /// Identifies the XAML attached property. + /// + public static readonly DependencyProperty AlignLeftWithPanelProperty = + DependencyProperty.RegisterAttached("AlignLeftWithPanel", typeof(bool), typeof(RelativePanel), new PropertyMetadata(false, OnAlignPropertiesChanged)); + + + /// + /// Gets the value of the RelativePanel.AlignLeftWith XAML attached property for the target element. + /// + /// The object from which the property value is read. + /// + /// The RelativePanel.AlignLeftWith XAML attached property value of the specified + /// object. (The element to align this element's left edge with.) + /// + [TypeConverter(typeof(NameReferenceConverter))] + public static object GetAlignLeftWith(DependencyObject obj) + { + return (object)obj.GetValue(AlignLeftWithProperty); + } + + /// + /// Sets the value of the RelativePanel.Above XAML attached property for a target element. + /// + /// The object to which the property value is written. + /// The value to set. (The element to align this element's left edge with.) + public static void SetAlignLeftWith(DependencyObject obj, object value) + { + obj.SetValue(AlignLeftWithProperty, value); + } + + /// + /// Identifies the XAML attached property. + /// + public static readonly DependencyProperty AlignLeftWithProperty = + DependencyProperty.RegisterAttached("AlignLeftWith", typeof(object), typeof(RelativePanel), new PropertyMetadata(null, OnAlignPropertiesChanged)); + + + /// + /// Gets the value of the RelativePanel.AlignRightWithPanel XAML attached property for the target element. + /// + /// The object from which the property value is read. + /// + /// The RelativePanel.AlignRightWithPanel XAML attached property value of the specified + /// object. (true to align this element's right edge with the panel's right edge; + /// otherwise, false.) + /// + public static bool GetAlignRightWithPanel(DependencyObject obj) + { + return (bool)obj.GetValue(AlignRightWithPanelProperty); + } + + /// + /// Sets the value of the RelativePanel.Above XAML attached property for a target element. + /// + /// The object to which the property value is written. + /// + /// The value to set. (true to align this element's right edge with the panel's right + /// edge; otherwise, false.) + /// + public static void SetAlignRightWithPanel(DependencyObject obj, bool value) + { + obj.SetValue(AlignRightWithPanelProperty, value); + } + + /// + /// Identifies the XAML attached property. + /// + public static readonly DependencyProperty AlignRightWithPanelProperty = + DependencyProperty.RegisterAttached("AlignRightWithPanel", typeof(bool), typeof(RelativePanel), new PropertyMetadata(false, OnAlignPropertiesChanged)); + + /// + /// Gets the value of the RelativePanel.AlignRightWith XAML attached property for the target element. + /// + /// The object from which the property value is read. + /// + /// The RelativePanel.AlignRightWith XAML attached property value of the specified + /// object. (The element to align this element's right edge with.) + /// + [TypeConverter(typeof(NameReferenceConverter))] + public static object GetAlignRightWith(DependencyObject obj) + { + return (object)obj.GetValue(AlignRightWithProperty); + } + + /// + /// Sets the value of the RelativePanel.AlignRightWith XAML attached property for a target element. + /// + /// The object to which the property value is written. + /// The value to set. (The element to align this element's right edge with.) + public static void SetAlignRightWith(DependencyObject obj, object value) + { + obj.SetValue(AlignRightWithProperty, value); + } + + /// + /// Identifies the XAML attached property. + /// + public static readonly DependencyProperty AlignRightWithProperty = + DependencyProperty.RegisterAttached("AlignRightWith", typeof(object), typeof(RelativePanel), new PropertyMetadata(null, OnAlignPropertiesChanged)); + + /// + /// Gets the value of the RelativePanel.AlignTopWithPanel XAML attached property for the target element. + /// + /// The object from which the property value is read. + /// + /// The RelativePanel.AlignTopWithPanel XAML attached property value of the specified + /// object. (true to align this element's top edge with the panel's top edge; otherwise, + /// false.) + /// + public static bool GetAlignTopWithPanel(DependencyObject obj) + { + return (bool)obj.GetValue(AlignTopWithPanelProperty); + } + + /// + /// Sets the value of the RelativePanel.AlignTopWithPanel XAML attached property for a target element. + /// + /// The object to which the property value is written. + /// + /// The value to set. (true to align this element's top edge with the panel's top + /// edge; otherwise, false.) + /// + public static void SetAlignTopWithPanel(DependencyObject obj, bool value) + { + obj.SetValue(AlignTopWithPanelProperty, value); + } + + /// + /// Identifies the XAML attached property. + /// + public static readonly DependencyProperty AlignTopWithPanelProperty = + DependencyProperty.RegisterAttached("AlignTopWithPanel", typeof(bool), typeof(RelativePanel), new PropertyMetadata(false, OnAlignPropertiesChanged)); + + /// + /// Gets the value of the RelativePanel.AlignTopWith XAML attached property for the target element. + /// + /// The object from which the property value is read. + /// The value to set. (The element to align this element's top edge with.) + [TypeConverter(typeof(NameReferenceConverter))] + public static object GetAlignTopWith(DependencyObject obj) + { + return (object)obj.GetValue(AlignTopWithProperty); + } + + /// + /// Sets the value of the RelativePanel.AlignTopWith XAML attached property for a target element. + /// + /// The object to which the property value is written. + /// The value to set. (The element to align this element's top edge with.) + public static void SetAlignTopWith(DependencyObject obj, object value) + { + obj.SetValue(AlignTopWithProperty, value); + } + + /// + /// Identifies the XAML attached property. + /// + public static readonly DependencyProperty AlignTopWithProperty = + DependencyProperty.RegisterAttached("AlignTopWith", typeof(object), typeof(RelativePanel), new PropertyMetadata(null, OnAlignPropertiesChanged)); + + /// + /// Gets the value of the RelativePanel.AlignVerticalCenterWithPanel XAML attached property for the target element. + /// + /// The object from which the property value is read. + /// + /// The RelativePanel.AlignVerticalCenterWithPanel XAML attached property value of + /// the specified object. (true to vertically center this element in the panel; otherwise, + /// false.) + /// + public static bool GetAlignVerticalCenterWithPanel(DependencyObject obj) + { + return (bool)obj.GetValue(AlignVerticalCenterWithPanelProperty); + } + + /// + /// Sets the value of the RelativePanel.AlignVerticalCenterWithPanel XAML attached property for a target element. + /// + /// The object to which the property value is written. + /// + /// The value to set. (true to vertically center this element in the panel; otherwise, + /// false.) + /// + public static void SetAlignVerticalCenterWithPanel(DependencyObject obj, bool value) + { + obj.SetValue(AlignVerticalCenterWithPanelProperty, value); + } + + /// + /// Identifies the XAML attached property. + /// + public static readonly DependencyProperty AlignVerticalCenterWithPanelProperty = + DependencyProperty.RegisterAttached("AlignVerticalCenterWithPanel", typeof(bool), typeof(RelativePanel), new PropertyMetadata(false, OnAlignPropertiesChanged)); + + /// + /// Gets the value of the RelativePanel.AlignVerticalCenterWith XAML attached property for the target element. + /// + /// The object from which the property value is read. + /// The value to set. (The element to align this element's vertical center with.) + [TypeConverter(typeof(NameReferenceConverter))] + public static object GetAlignVerticalCenterWith(DependencyObject obj) + { + return (object)obj.GetValue(AlignVerticalCenterWithProperty); + } + + /// + /// Sets the value of the RelativePanel.AlignVerticalCenterWith XAML attached property for a target element. + /// + /// The object to which the property value is written. + /// The value to set. (The element to align this element's horizontal center with.) + public static void SetAlignVerticalCenterWith(DependencyObject obj, object value) + { + obj.SetValue(AlignVerticalCenterWithProperty, value); + } + + /// + /// Identifies the XAML attached property. + /// + public static readonly DependencyProperty AlignVerticalCenterWithProperty = + DependencyProperty.RegisterAttached("AlignVerticalCenterWith", typeof(object), typeof(RelativePanel), new PropertyMetadata(null, OnAlignPropertiesChanged)); + + /// + /// Gets the value of the RelativePanel.Below XAML attached property for the target element. + /// + /// The object from which the property value is read. + /// + /// The RelativePanel.Below XAML attached property value of the specified object. + /// (The element to position this element below.) + /// + [TypeConverter(typeof(NameReferenceConverter))] + public static object GetBelow(DependencyObject obj) + { + return (object)obj.GetValue(BelowProperty); + } + + /// + /// Sets the value of the RelativePanel.Above XAML attached property for a target element. + /// + /// The object to which the property value is written. + /// The value to set. (The element to position this element below.) + public static void SetBelow(DependencyObject obj, object value) + { + obj.SetValue(BelowProperty, value); + } + + /// + /// Identifies the XAML attached property. + /// + public static readonly DependencyProperty BelowProperty = + DependencyProperty.RegisterAttached("Below", typeof(object), typeof(RelativePanel), new PropertyMetadata(null, OnAlignPropertiesChanged)); + + /// + /// Gets the value of the RelativePanel.LeftOf XAML attached property for the target element. + /// + /// The object from which the property value is read. + /// + /// The RelativePanel.LeftOf XAML attached property value of the specified object. + /// (The element to position this element to the left of.) + /// + [TypeConverter(typeof(NameReferenceConverter))] + public static object GetLeftOf(DependencyObject obj) + { + return (object)obj.GetValue(LeftOfProperty); + } + + /// + /// Sets the value of the RelativePanel.LeftOf XAML attached property for a target element. + /// + /// The object to which the property value is written. + /// The value to set. (The element to position this element to the left of.) + public static void SetLeftOf(DependencyObject obj, object value) + { + obj.SetValue(LeftOfProperty, value); + } + + /// + /// Identifies the XAML attached property. + /// + public static readonly DependencyProperty LeftOfProperty = + DependencyProperty.RegisterAttached("LeftOf", typeof(object), typeof(RelativePanel), new PropertyMetadata(null, OnAlignPropertiesChanged)); + + /// + /// Gets the value of the RelativePanel.RightOf XAML attached property for the target element. + /// + /// The object from which the property value is read. + /// + /// The RelativePanel.RightOf XAML attached property value of the specified object. + /// (The element to position this element to the right of.) + /// + [TypeConverter(typeof(NameReferenceConverter))] + public static object GetRightOf(DependencyObject obj) + { + return (object)obj.GetValue(RightOfProperty); + } + + /// + /// Sets the value of the RelativePanel.RightOf XAML attached property for a target element. + /// + /// The object to which the property value is written. + /// The value to set. (The element to position this element to the right of.) + public static void SetRightOf(DependencyObject obj, object value) + { + obj.SetValue(RightOfProperty, value); + } + + /// + /// Identifies the XAML attached property. + /// + public static readonly DependencyProperty RightOfProperty = + DependencyProperty.RegisterAttached("RightOf", typeof(object), typeof(RelativePanel), new PropertyMetadata(null, OnAlignPropertiesChanged)); + } +} diff --git a/src/Avalonia.Controls/RelativePanel.cs b/src/Avalonia.Controls/RelativePanel.cs new file mode 100644 index 0000000000..aecd4b7c98 --- /dev/null +++ b/src/Avalonia.Controls/RelativePanel.cs @@ -0,0 +1,424 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Avalonia.Controls +{ + /// + /// Defines an area within which you can position and align child objects in relation + /// to each other or the parent panel. + /// + /// + /// Default position + /// By default, any unconstrained element declared as a child of the RelativePanel is given the entire + /// available space and positioned at the(0, 0) coordinates(upper left corner) of the panel.So, if you + /// are positioning a second element relative to an unconstrained element, keep in mind that the second + /// element might get pushed out of the panel. + /// + ///Conflicting relationships + /// + /// If you set multiple relationships that target the same edge of an element, you might have conflicting + /// relationships in your layout as a result.When this happens, the relationships are applied in the + /// following order of priority: + /// • Panel alignment relationships (AlignTopWithPanel, AlignLeftWithPanel, …) are applied first. + /// • Sibling alignment relationships(AlignTopWith, AlignLeftWith, …) are applied second. + /// • Sibling positional relationships(Above, Below, RightOf, LeftOf) are applied last. + /// + /// + /// The panel-center alignment properties(AlignVerticalCenterWith, AlignHorizontalCenterWithPanel, ...) are + /// typically used independently of other constraints and are applied if there is no conflict. + /// + /// + /// The HorizontalAlignment and VerticalAlignment properties on UI elements are applied after relationship + /// properties are evaluated and applied. These properties control the placement of the element within the + /// available size for the element, if the desired size is smaller than the available size. + /// + /// + public partial class RelativePanel : Panel + { + // Dependency property for storing intermediate arrange state on the children + private static readonly DependencyProperty ArrangeStateProperty = + DependencyProperty.Register("ArrangeState", typeof(double[]), typeof(RelativePanel), new PropertyMetadata(null)); + + /// + /// When overridden in a derived class, measures the size in layout required for + /// child elements and determines a size for the System.Windows.FrameworkElement-derived + /// class. + /// + /// The available size that this element can give to child elements. Infinity can + /// be specified as a value to indicate that the element will size to whatever content + /// is available. + /// + /// + /// The size that this element determines it needs during layout, based on its calculations + /// of child element sizes. + /// + protected override Size MeasureOverride(Size availableSize) + { + foreach (var child in Children.OfType()) + { + child.Measure(availableSize); + } + foreach (var item in CalculateLocations(availableSize)) + { + if (item.Item2.Size.Width < item.Item1.DesiredSize.Width || item.Item2.Size.Height < item.Item1.DesiredSize.Height) + item.Item1.Measure(item.Item2.Size); + } + return base.MeasureOverride(availableSize); + } + + /// + /// When overridden in a derived class, positions child elements and determines a + /// size for a System.Windows.FrameworkElement derived class. + /// + /// + /// The final area within the parent that this element should use to arrange itself + /// and its children. + /// + /// The actual size used. + protected override Size ArrangeOverride(Size finalSize) + { + foreach (var item in CalculateLocations(finalSize)) + item.Item1.Arrange(item.Item2); + return base.ArrangeOverride(finalSize); + } + + private IEnumerable> CalculateLocations(Size finalSize) + { + //List of margins for each element between the element and panel (left, top, right, bottom) + List arranges = new List(Children.Count); + //First pass aligns all sides that aren't constrained by other elements + int arrangedCount = 0; + foreach (var child in Children.OfType()) + { + //NaN means the arrange value is not constrained yet for that side + double[] rect = new[] { double.NaN, double.NaN, double.NaN, double.NaN }; + arranges.Add(rect); + child.SetValue(ArrangeStateProperty, rect); + + //Align with panels always wins, so do these first, or if no constraints are set at all set margin to 0 + + //Left side + if (GetAlignLeftWithPanel(child)) + { + rect[0] = 0; + } + else if ( + child.GetValue(AlignLeftWithProperty) == null && + child.GetValue(RightOfProperty) == null && + child.GetValue(AlignHorizontalCenterWithProperty) == null && + !GetAlignHorizontalCenterWithPanel(child)) + { + if (GetAlignRightWithPanel(child)) + rect[0] = finalSize.Width - child.DesiredSize.Width; + else if (child.GetValue(AlignRightWithProperty) == null && child.GetValue(AlignHorizontalCenterWithProperty) == null && child.GetValue(LeftOfProperty) == null) + rect[0] = 0; //default fallback to 0 + } + + //Top side + if (GetAlignTopWithPanel(child)) + { + rect[1] = 0; + } + else if ( + child.GetValue(AlignTopWithProperty) == null && + child.GetValue(BelowProperty) == null && + child.GetValue(AlignVerticalCenterWithProperty) == null && + !GetAlignVerticalCenterWithPanel(child)) + { + if (GetAlignBottomWithPanel(child)) + rect[1] = finalSize.Height - child.DesiredSize.Height; + else if (child.GetValue(AlignBottomWithProperty) == null && child.GetValue(AlignVerticalCenterWithProperty) == null && child.GetValue(AboveProperty) == null) + rect[1] = 0; //default fallback to 0 + } + + //Right side + if (GetAlignRightWithPanel(child)) + { + rect[2] = 0; + } + else if (!double.IsNaN(rect[0]) && + child.GetValue(AlignRightWithProperty) == null && + child.GetValue(LeftOfProperty) == null && + child.GetValue(AlignHorizontalCenterWithProperty) == null && + !GetAlignHorizontalCenterWithPanel(child)) + { + rect[2] = finalSize.Width - rect[0] - child.DesiredSize.Width; + } + + //Bottom side + if (GetAlignBottomWithPanel(child)) + rect[3] = 0; + else if (!double.IsNaN(rect[1]) && + (child.GetValue(AlignBottomWithProperty) == null && + child.GetValue(AboveProperty) == null) && + child.GetValue(AlignVerticalCenterWithProperty) == null && + !GetAlignVerticalCenterWithPanel(child)) + { + rect[3] = finalSize.Height - rect[1] - child.DesiredSize.Height; + } + + if (!double.IsNaN(rect[0]) && !double.IsNaN(rect[1]) && + !double.IsNaN(rect[2]) && !double.IsNaN(rect[3])) + arrangedCount++; + } + int i = 0; + //Run iterative layout passes + while (arrangedCount < Children.Count) + { + bool valueChanged = false; + i = 0; + foreach (var child in Children.OfType()) + { + double[] rect = arranges[i++]; + + if (!double.IsNaN(rect[0]) && !double.IsNaN(rect[1]) && + !double.IsNaN(rect[2]) && !double.IsNaN(rect[3])) + continue; //Control is fully arranged + + //Calculate left side + if (double.IsNaN(rect[0])) + { + var alignLeftWith = GetDependencyElement(RelativePanel.AlignLeftWithProperty, child); + if (alignLeftWith != null) + { + double[] r = (double[])alignLeftWith.GetValue(ArrangeStateProperty); + if (!double.IsNaN(r[0])) + { + rect[0] = r[0]; + valueChanged = true; + } + } + else + { + var rightOf = GetDependencyElement(RelativePanel.RightOfProperty, child); + if (rightOf != null) + { + double[] r = (double[])rightOf.GetValue(ArrangeStateProperty); + if (!double.IsNaN(r[2])) + { + rect[0] = finalSize.Width - r[2]; + valueChanged = true; + } + } + else if (!double.IsNaN(rect[2])) + { + rect[0] = finalSize.Width - rect[2] - child.DesiredSize.Width; + valueChanged = true; + } + } + } + //Calculate top side + if (double.IsNaN(rect[1])) + { + var alignTopWith = GetDependencyElement(RelativePanel.AlignTopWithProperty, child); + if (alignTopWith != null) + { + double[] r = (double[])alignTopWith.GetValue(ArrangeStateProperty); + if (!double.IsNaN(r[1])) + { + rect[1] = r[1]; + valueChanged = true; + } + } + else + { + var below = GetDependencyElement(RelativePanel.BelowProperty, child); + if (below != null) + { + double[] r = (double[])below.GetValue(ArrangeStateProperty); + if (!double.IsNaN(r[3])) + { + rect[1] = finalSize.Height - r[3]; + valueChanged = true; + } + } + else if (!double.IsNaN(rect[3])) + { + rect[1] = finalSize.Height - rect[3] - child.DesiredSize.Height; + valueChanged = true; + } + } + } + //Calculate right side + if (double.IsNaN(rect[2])) + { + var alignRightWith = GetDependencyElement(RelativePanel.AlignRightWithProperty, child); + if (alignRightWith != null) + { + double[] r = (double[])alignRightWith.GetValue(ArrangeStateProperty); + if (!double.IsNaN(r[2])) + { + rect[2] = r[2]; + if (double.IsNaN(rect[0])) + { + if (child.GetValue(RelativePanel.AlignLeftWithProperty) == null) + { + rect[0] = rect[2] + child.DesiredSize.Width; + valueChanged = true; + } + } + } + } + else + { + var leftOf = GetDependencyElement(RelativePanel.LeftOfProperty, child); + if (leftOf != null) + { + double[] r = (double[])leftOf.GetValue(ArrangeStateProperty); + if (!double.IsNaN(r[0])) + { + rect[2] = finalSize.Width - r[0]; + valueChanged = true; + } + } + else if (!double.IsNaN(rect[0])) + { + rect[2] = finalSize.Width - rect[0] - child.DesiredSize.Width; + valueChanged = true; + } + } + } + //Calculate bottom side + if (double.IsNaN(rect[3])) + { + var alignBottomWith = GetDependencyElement(RelativePanel.AlignBottomWithProperty, child); + if (alignBottomWith != null) + { + double[] r = (double[])alignBottomWith.GetValue(ArrangeStateProperty); + if (!double.IsNaN(r[3])) + { + rect[3] = r[3]; + valueChanged = true; + if (double.IsNaN(rect[1])) + { + if (child.GetValue(RelativePanel.AlignTopWithProperty) == null) + { + rect[1] = finalSize.Height - rect[3] - child.DesiredSize.Height; + } + } + } + } + else + { + var above = GetDependencyElement(RelativePanel.AboveProperty, child); + if (above != null) + { + double[] r = (double[])above.GetValue(ArrangeStateProperty); + if (!double.IsNaN(r[1])) + { + rect[3] = finalSize.Height - r[1]; + valueChanged = true; + } + } + else if (!double.IsNaN(rect[1])) + { + rect[3] = finalSize.Height - rect[1] - child.DesiredSize.Height; + valueChanged = true; + } + } + } + //Calculate horizontal alignment + if (double.IsNaN(rect[0]) && double.IsNaN(rect[2])) + { + var alignHorizontalCenterWith = GetDependencyElement(RelativePanel.AlignHorizontalCenterWithProperty, child); + if (alignHorizontalCenterWith != null) + { + double[] r = (double[])alignHorizontalCenterWith.GetValue(ArrangeStateProperty); + if (!double.IsNaN(r[0]) && !double.IsNaN(r[2])) + { + rect[0] = r[0] + (finalSize.Width - r[2] - r[0]) * .5 - child.DesiredSize.Width * .5; + rect[2] = finalSize.Width - rect[0] - child.DesiredSize.Width; + valueChanged = true; + } + } + else + { + if (GetAlignHorizontalCenterWithPanel(child)) + { + var roomToSpare = finalSize.Width - child.DesiredSize.Width; + rect[0] = roomToSpare * .5; + rect[2] = roomToSpare * .5; + valueChanged = true; + } + } + } + + //Calculate vertical alignment + if (double.IsNaN(rect[1]) && double.IsNaN(rect[3])) + { + var alignVerticalCenterWith = GetDependencyElement(RelativePanel.AlignVerticalCenterWithProperty, child); + if (alignVerticalCenterWith != null) + { + double[] r = (double[])alignVerticalCenterWith.GetValue(ArrangeStateProperty); + if (!double.IsNaN(r[1]) && !double.IsNaN(r[3])) + { + rect[1] = r[1] + (finalSize.Height - r[3] - r[1]) * .5 - child.DesiredSize.Height * .5; + rect[3] = finalSize.Height - rect[1] - child.DesiredSize.Height; + valueChanged = true; + } + } + else + { + if (GetAlignVerticalCenterWithPanel(child)) + { + var roomToSpare = finalSize.Height - child.DesiredSize.Height; + rect[1] = roomToSpare * .5; + rect[3] = roomToSpare * .5; + valueChanged = true; + } + } + } + + + //if panel is now fully arranged, increase the counter + if (!double.IsNaN(rect[0]) && !double.IsNaN(rect[1]) && + !double.IsNaN(rect[2]) && !double.IsNaN(rect[3])) + arrangedCount++; + } + if (!valueChanged) + { + //If a layout pass didn't increase number of arranged elements, + //there must be a circular dependency + throw new ArgumentException("RelativePanel error: Circular dependency detected. Layout could not complete"); + } + } + + i = 0; + //Arrange iterations complete - Apply the results to the child elements + foreach (var child in Children.OfType()) + { + double[] rect = arranges[i++]; + //Measure child again with the new calculated available size + //this helps for instance textblocks to reflow the text wrapping + //We should probably have done this during the measure step but it would cause a more expensive + //measure+arrange layout cycle + //if(child is TextBlock) + // child.Measure(new Size(Math.Max(0, finalSize.Width - rect[2] - rect[0]), Math.Max(0, finalSize.Height - rect[3] - rect[1]))); + + //if(child is TextBlock tb) + //{ + // tb.ArrangeOverride(new Rect(rect[0], rect[1], Math.Max(0, finalSize.Width - rect[2] - rect[0]), Math.Max(0, finalSize.Height - rect[3] - rect[1]))); + //} + //else + yield return new Tuple(child, new Rect(rect[0], rect[1], Math.Max(0, finalSize.Width - rect[2] - rect[0]), Math.Max(0, finalSize.Height - rect[3] - rect[1]))); + } + } + + //Gets the element that's referred to in the alignment attached properties + private UIElement GetDependencyElement(DependencyProperty property, DependencyObject child) + { + var dependency = child.GetValue(property); + if (dependency == null) + return null; + if (dependency is UIElement) + { + if (Children.Contains((UIElement)dependency)) + return (UIElement)dependency; + throw new ArgumentException(string.Format("RelativePanel error: Element does not exist in the current context", property.Name)); + } + + throw new ArgumentException("RelativePanel error: Value must be of type UIElement"); + } + } +} From b633b063bdb9c1cb3445354b40f4756c2867e351 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sun, 24 May 2020 21:55:56 -0300 Subject: [PATCH 051/693] fix wrap layout. --- src/Avalonia.Layout/WrapLayout/WrapLayout.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Layout/WrapLayout/WrapLayout.cs b/src/Avalonia.Layout/WrapLayout/WrapLayout.cs index 4fc8035e8e..dccbb2205e 100644 --- a/src/Avalonia.Layout/WrapLayout/WrapLayout.cs +++ b/src/Avalonia.Layout/WrapLayout/WrapLayout.cs @@ -313,16 +313,15 @@ namespace Avalonia.Layout return finalSize; } - protected override void OnPropertyChanged(AvaloniaProperty property, Optional oldValue, BindingValue newValue, BindingPriority priority) + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - base.OnPropertyChanged(property, oldValue, newValue, priority); + base.OnPropertyChanged(change); - if(property == OrientationProperty || property == HorizontalSpacingProperty || property == VerticalSpacingProperty) + if (change.Property == OrientationProperty || change.Property == HorizontalSpacingProperty || change.Property == VerticalSpacingProperty) { InvalidateMeasure(); InvalidateArrange(); } - } } } From 289463e5e15e2e277d795bfef34564d830861a8b Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sun, 24 May 2020 22:45:56 -0300 Subject: [PATCH 052/693] port relativepanel. --- .../RelativePanel.AttachedProperties.cs | 159 ++++++++++-------- src/Avalonia.Controls/RelativePanel.cs | 29 ++-- .../Avalonia.Markup.Xaml.csproj | 1 + .../Converters/NameReferenceConverter.cs | 82 +++++++++ 4 files changed, 189 insertions(+), 82 deletions(-) create mode 100644 src/Markup/Avalonia.Markup.Xaml/Converters/NameReferenceConverter.cs diff --git a/src/Avalonia.Controls/RelativePanel.AttachedProperties.cs b/src/Avalonia.Controls/RelativePanel.AttachedProperties.cs index 21baff87ab..a42eb6d428 100644 --- a/src/Avalonia.Controls/RelativePanel.AttachedProperties.cs +++ b/src/Avalonia.Controls/RelativePanel.AttachedProperties.cs @@ -1,16 +1,39 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Text; +using Avalonia.Layout; +using Avalonia.Markup.Xaml.Converters; namespace Avalonia.Controls { public partial class RelativePanel { - private static void OnAlignPropertiesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + private static void OnAlignPropertiesChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e) { - var elm = d as FrameworkElement; - if (elm.Parent is FrameworkElement) - ((FrameworkElement)elm.Parent).InvalidateArrange(); + var elm = d as Layoutable; + if (elm.Parent is Layoutable) + ((Layoutable)elm.Parent).InvalidateArrange(); + } + + static RelativePanel() + { + AboveProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + AlignBottomWithPanelProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + AlignBottomWithProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + AlignHorizontalCenterWithPanelProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + AlignHorizontalCenterWithProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + AlignLeftWithPanelProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + AlignLeftWithProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + AlignRightWithPanelProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + AlignRightWithProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + AlignTopWithPanelProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + AlignTopWithProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + AlignVerticalCenterWithPanelProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + AlignVerticalCenterWithProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + BelowProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + LeftOfProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + LeftOfProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); } /// @@ -22,7 +45,7 @@ namespace Avalonia.Controls /// (The element to position this element above.) /// [TypeConverter(typeof(NameReferenceConverter))] - public static object GetAbove(DependencyObject obj) + public static object GetAbove(AvaloniaObject obj) { return (object)obj.GetValue(AboveProperty); } @@ -32,7 +55,7 @@ namespace Avalonia.Controls /// /// The object to which the property value is written. /// The value to set. (The element to position this element above.) - public static void SetAbove(DependencyObject obj, object value) + public static void SetAbove(AvaloniaObject obj, object value) { obj.SetValue(AboveProperty, value); } @@ -40,8 +63,8 @@ namespace Avalonia.Controls /// /// Identifies the XAML attached property. /// - public static readonly DependencyProperty AboveProperty = - DependencyProperty.RegisterAttached("Above", typeof(object), typeof(RelativePanel), new PropertyMetadata(null, OnAlignPropertiesChanged)); + public static readonly AttachedProperty AboveProperty = + AvaloniaProperty.RegisterAttached("Above", typeof(RelativePanel)); /// @@ -53,7 +76,7 @@ namespace Avalonia.Controls /// object. (true to align this element's bottom edge with the panel's bottom edge; /// otherwise, false.) /// - public static bool GetAlignBottomWithPanel(DependencyObject obj) + public static bool GetAlignBottomWithPanel(AvaloniaObject obj) { return (bool)obj.GetValue(AlignBottomWithPanelProperty); } @@ -66,7 +89,7 @@ namespace Avalonia.Controls /// The value to set. (true to align this element's bottom edge with the panel's /// bottom edge; otherwise, false.) /// - public static void SetAlignBottomWithPanel(DependencyObject obj, bool value) + public static void SetAlignBottomWithPanel(AvaloniaObject obj, bool value) { obj.SetValue(AlignBottomWithPanelProperty, value); } @@ -74,8 +97,8 @@ namespace Avalonia.Controls /// /// Identifies the XAML attached property. /// - public static readonly DependencyProperty AlignBottomWithPanelProperty = - DependencyProperty.RegisterAttached("AlignBottomWithPanel", typeof(bool), typeof(RelativePanel), new PropertyMetadata(false, OnAlignPropertiesChanged)); + public static readonly AttachedProperty AlignBottomWithPanelProperty = + AvaloniaProperty.RegisterAttached("AlignBottomWithPanel", typeof(RelativePanel)); /// /// Gets the value of the RelativePanel.AlignBottomWith XAML attached property for the target element. @@ -86,7 +109,7 @@ namespace Avalonia.Controls /// (The element to align this element's bottom edge with.) /// [TypeConverter(typeof(NameReferenceConverter))] - public static object GetAlignBottomWith(DependencyObject obj) + public static object GetAlignBottomWith(AvaloniaObject obj) { return (object)obj.GetValue(AlignBottomWithProperty); } @@ -96,7 +119,7 @@ namespace Avalonia.Controls /// /// The object to which the property value is written. /// The value to set. (The element to align this element's bottom edge with.) - public static void SetAlignBottomWith(DependencyObject obj, object value) + public static void SetAlignBottomWith(AvaloniaObject obj, object value) { obj.SetValue(AlignBottomWithProperty, value); } @@ -104,8 +127,8 @@ namespace Avalonia.Controls /// /// Identifies the XAML attached property. /// - public static readonly DependencyProperty AlignBottomWithProperty = - DependencyProperty.RegisterAttached("AlignBottomWith", typeof(object), typeof(RelativePanel), new PropertyMetadata(null, OnAlignPropertiesChanged)); + public static readonly AttachedProperty AlignBottomWithProperty = + AvaloniaProperty.RegisterAttached("AlignBottomWith", typeof(RelativePanel)); /// /// Gets the value of the RelativePanel.AlignHorizontalCenterWithPanel XAML attached property for the target element. @@ -116,7 +139,7 @@ namespace Avalonia.Controls /// of the specified object. (true to horizontally center this element in the panel; /// otherwise, false.) /// - public static bool GetAlignHorizontalCenterWithPanel(DependencyObject obj) + public static bool GetAlignHorizontalCenterWithPanel(AvaloniaObject obj) { return (bool)obj.GetValue(AlignHorizontalCenterWithPanelProperty); } @@ -129,7 +152,7 @@ namespace Avalonia.Controls /// The value to set. (true to horizontally center this element in the panel; otherwise, /// false.) /// - public static void SetAlignHorizontalCenterWithPanel(DependencyObject obj, bool value) + public static void SetAlignHorizontalCenterWithPanel(AvaloniaObject obj, bool value) { obj.SetValue(AlignHorizontalCenterWithPanelProperty, value); } @@ -137,8 +160,8 @@ namespace Avalonia.Controls /// /// Identifies the XAML attached property. /// - public static readonly DependencyProperty AlignHorizontalCenterWithPanelProperty = - DependencyProperty.RegisterAttached("AlignHorizontalCenterWithPanel", typeof(bool), typeof(RelativePanel), new PropertyMetadata(false, OnAlignPropertiesChanged)); + public static readonly AttachedProperty AlignHorizontalCenterWithPanelProperty = + AvaloniaProperty.RegisterAttached("AlignHorizontalCenterWithPanel", typeof(RelativePanel), false); /// /// Gets the value of the RelativePanel.AlignHorizontalCenterWith XAML attached property for the target element. @@ -149,7 +172,7 @@ namespace Avalonia.Controls /// specified object. (The element to align this element's horizontal center with.) /// [TypeConverter(typeof(NameReferenceConverter))] - public static object GetAlignHorizontalCenterWith(DependencyObject obj) + public static object GetAlignHorizontalCenterWith(AvaloniaObject obj) { return (object)obj.GetValue(AlignHorizontalCenterWithProperty); } @@ -159,7 +182,7 @@ namespace Avalonia.Controls /// /// The object to which the property value is written. /// The value to set. (The element to align this element's horizontal center with.) - public static void SetAlignHorizontalCenterWith(DependencyObject obj, object value) + public static void SetAlignHorizontalCenterWith(AvaloniaObject obj, object value) { obj.SetValue(AlignHorizontalCenterWithProperty, value); } @@ -167,8 +190,8 @@ namespace Avalonia.Controls /// /// Identifies the XAML attached property. /// - public static readonly DependencyProperty AlignHorizontalCenterWithProperty = - DependencyProperty.RegisterAttached("AlignHorizontalCenterWith", typeof(object), typeof(RelativePanel), new PropertyMetadata(null, OnAlignPropertiesChanged)); + public static readonly AttachedProperty AlignHorizontalCenterWithProperty = + AvaloniaProperty.RegisterAttached("AlignHorizontalCenterWith", typeof(object), typeof(RelativePanel)); /// /// Gets the value of the RelativePanel.AlignLeftWithPanel XAML attached property for the target element. @@ -179,7 +202,7 @@ namespace Avalonia.Controls /// object. (true to align this element's left edge with the panel's left edge; otherwise, /// false.) /// - public static bool GetAlignLeftWithPanel(DependencyObject obj) + public static bool GetAlignLeftWithPanel(AvaloniaObject obj) { return (bool)obj.GetValue(AlignLeftWithPanelProperty); } @@ -192,7 +215,7 @@ namespace Avalonia.Controls /// The value to set. (true to align this element's left edge with the panel's left /// edge; otherwise, false.) /// - public static void SetAlignLeftWithPanel(DependencyObject obj, bool value) + public static void SetAlignLeftWithPanel(AvaloniaObject obj, bool value) { obj.SetValue(AlignLeftWithPanelProperty, value); } @@ -200,8 +223,8 @@ namespace Avalonia.Controls /// /// Identifies the XAML attached property. /// - public static readonly DependencyProperty AlignLeftWithPanelProperty = - DependencyProperty.RegisterAttached("AlignLeftWithPanel", typeof(bool), typeof(RelativePanel), new PropertyMetadata(false, OnAlignPropertiesChanged)); + public static readonly AttachedProperty AlignLeftWithPanelProperty = + AvaloniaProperty.RegisterAttached("AlignLeftWithPanel", typeof(RelativePanel), false); /// @@ -213,7 +236,7 @@ namespace Avalonia.Controls /// object. (The element to align this element's left edge with.) /// [TypeConverter(typeof(NameReferenceConverter))] - public static object GetAlignLeftWith(DependencyObject obj) + public static object GetAlignLeftWith(AvaloniaObject obj) { return (object)obj.GetValue(AlignLeftWithProperty); } @@ -223,7 +246,7 @@ namespace Avalonia.Controls /// /// The object to which the property value is written. /// The value to set. (The element to align this element's left edge with.) - public static void SetAlignLeftWith(DependencyObject obj, object value) + public static void SetAlignLeftWith(AvaloniaObject obj, object value) { obj.SetValue(AlignLeftWithProperty, value); } @@ -231,8 +254,8 @@ namespace Avalonia.Controls /// /// Identifies the XAML attached property. /// - public static readonly DependencyProperty AlignLeftWithProperty = - DependencyProperty.RegisterAttached("AlignLeftWith", typeof(object), typeof(RelativePanel), new PropertyMetadata(null, OnAlignPropertiesChanged)); + public static readonly AttachedProperty AlignLeftWithProperty = + AvaloniaProperty.RegisterAttached("AlignLeftWith"); /// @@ -244,7 +267,7 @@ namespace Avalonia.Controls /// object. (true to align this element's right edge with the panel's right edge; /// otherwise, false.) /// - public static bool GetAlignRightWithPanel(DependencyObject obj) + public static bool GetAlignRightWithPanel(AvaloniaObject obj) { return (bool)obj.GetValue(AlignRightWithPanelProperty); } @@ -257,7 +280,7 @@ namespace Avalonia.Controls /// The value to set. (true to align this element's right edge with the panel's right /// edge; otherwise, false.) /// - public static void SetAlignRightWithPanel(DependencyObject obj, bool value) + public static void SetAlignRightWithPanel(AvaloniaObject obj, bool value) { obj.SetValue(AlignRightWithPanelProperty, value); } @@ -265,8 +288,8 @@ namespace Avalonia.Controls /// /// Identifies the XAML attached property. /// - public static readonly DependencyProperty AlignRightWithPanelProperty = - DependencyProperty.RegisterAttached("AlignRightWithPanel", typeof(bool), typeof(RelativePanel), new PropertyMetadata(false, OnAlignPropertiesChanged)); + public static readonly AttachedProperty AlignRightWithPanelProperty = + AvaloniaProperty.RegisterAttached("AlignRightWithPanel", false); /// /// Gets the value of the RelativePanel.AlignRightWith XAML attached property for the target element. @@ -277,7 +300,7 @@ namespace Avalonia.Controls /// object. (The element to align this element's right edge with.) /// [TypeConverter(typeof(NameReferenceConverter))] - public static object GetAlignRightWith(DependencyObject obj) + public static object GetAlignRightWith(AvaloniaObject obj) { return (object)obj.GetValue(AlignRightWithProperty); } @@ -287,7 +310,7 @@ namespace Avalonia.Controls /// /// The object to which the property value is written. /// The value to set. (The element to align this element's right edge with.) - public static void SetAlignRightWith(DependencyObject obj, object value) + public static void SetAlignRightWith(AvaloniaObject obj, object value) { obj.SetValue(AlignRightWithProperty, value); } @@ -295,8 +318,8 @@ namespace Avalonia.Controls /// /// Identifies the XAML attached property. /// - public static readonly DependencyProperty AlignRightWithProperty = - DependencyProperty.RegisterAttached("AlignRightWith", typeof(object), typeof(RelativePanel), new PropertyMetadata(null, OnAlignPropertiesChanged)); + public static readonly AttachedProperty AlignRightWithProperty = + AvaloniaProperty.RegisterAttached("AlignRightWith"); /// /// Gets the value of the RelativePanel.AlignTopWithPanel XAML attached property for the target element. @@ -307,7 +330,7 @@ namespace Avalonia.Controls /// object. (true to align this element's top edge with the panel's top edge; otherwise, /// false.) /// - public static bool GetAlignTopWithPanel(DependencyObject obj) + public static bool GetAlignTopWithPanel(AvaloniaObject obj) { return (bool)obj.GetValue(AlignTopWithPanelProperty); } @@ -320,7 +343,7 @@ namespace Avalonia.Controls /// The value to set. (true to align this element's top edge with the panel's top /// edge; otherwise, false.) /// - public static void SetAlignTopWithPanel(DependencyObject obj, bool value) + public static void SetAlignTopWithPanel(AvaloniaObject obj, bool value) { obj.SetValue(AlignTopWithPanelProperty, value); } @@ -328,8 +351,8 @@ namespace Avalonia.Controls /// /// Identifies the XAML attached property. /// - public static readonly DependencyProperty AlignTopWithPanelProperty = - DependencyProperty.RegisterAttached("AlignTopWithPanel", typeof(bool), typeof(RelativePanel), new PropertyMetadata(false, OnAlignPropertiesChanged)); + public static readonly AttachedProperty AlignTopWithPanelProperty = + AvaloniaProperty.RegisterAttached("AlignTopWithPanel", false); /// /// Gets the value of the RelativePanel.AlignTopWith XAML attached property for the target element. @@ -337,7 +360,7 @@ namespace Avalonia.Controls /// The object from which the property value is read. /// The value to set. (The element to align this element's top edge with.) [TypeConverter(typeof(NameReferenceConverter))] - public static object GetAlignTopWith(DependencyObject obj) + public static object GetAlignTopWith(AvaloniaObject obj) { return (object)obj.GetValue(AlignTopWithProperty); } @@ -347,7 +370,7 @@ namespace Avalonia.Controls /// /// The object to which the property value is written. /// The value to set. (The element to align this element's top edge with.) - public static void SetAlignTopWith(DependencyObject obj, object value) + public static void SetAlignTopWith(AvaloniaObject obj, object value) { obj.SetValue(AlignTopWithProperty, value); } @@ -355,8 +378,8 @@ namespace Avalonia.Controls /// /// Identifies the XAML attached property. /// - public static readonly DependencyProperty AlignTopWithProperty = - DependencyProperty.RegisterAttached("AlignTopWith", typeof(object), typeof(RelativePanel), new PropertyMetadata(null, OnAlignPropertiesChanged)); + public static readonly AttachedProperty AlignTopWithProperty = + AvaloniaProperty.RegisterAttached("AlignTopWith"); /// /// Gets the value of the RelativePanel.AlignVerticalCenterWithPanel XAML attached property for the target element. @@ -367,7 +390,7 @@ namespace Avalonia.Controls /// the specified object. (true to vertically center this element in the panel; otherwise, /// false.) /// - public static bool GetAlignVerticalCenterWithPanel(DependencyObject obj) + public static bool GetAlignVerticalCenterWithPanel(AvaloniaObject obj) { return (bool)obj.GetValue(AlignVerticalCenterWithPanelProperty); } @@ -380,7 +403,7 @@ namespace Avalonia.Controls /// The value to set. (true to vertically center this element in the panel; otherwise, /// false.) /// - public static void SetAlignVerticalCenterWithPanel(DependencyObject obj, bool value) + public static void SetAlignVerticalCenterWithPanel(AvaloniaObject obj, bool value) { obj.SetValue(AlignVerticalCenterWithPanelProperty, value); } @@ -388,8 +411,8 @@ namespace Avalonia.Controls /// /// Identifies the XAML attached property. /// - public static readonly DependencyProperty AlignVerticalCenterWithPanelProperty = - DependencyProperty.RegisterAttached("AlignVerticalCenterWithPanel", typeof(bool), typeof(RelativePanel), new PropertyMetadata(false, OnAlignPropertiesChanged)); + public static readonly AttachedProperty AlignVerticalCenterWithPanelProperty = + AvaloniaProperty.RegisterAttached("AlignVerticalCenterWithPanel", false); /// /// Gets the value of the RelativePanel.AlignVerticalCenterWith XAML attached property for the target element. @@ -397,7 +420,7 @@ namespace Avalonia.Controls /// The object from which the property value is read. /// The value to set. (The element to align this element's vertical center with.) [TypeConverter(typeof(NameReferenceConverter))] - public static object GetAlignVerticalCenterWith(DependencyObject obj) + public static object GetAlignVerticalCenterWith(AvaloniaObject obj) { return (object)obj.GetValue(AlignVerticalCenterWithProperty); } @@ -407,7 +430,7 @@ namespace Avalonia.Controls /// /// The object to which the property value is written. /// The value to set. (The element to align this element's horizontal center with.) - public static void SetAlignVerticalCenterWith(DependencyObject obj, object value) + public static void SetAlignVerticalCenterWith(AvaloniaObject obj, object value) { obj.SetValue(AlignVerticalCenterWithProperty, value); } @@ -415,8 +438,8 @@ namespace Avalonia.Controls /// /// Identifies the XAML attached property. /// - public static readonly DependencyProperty AlignVerticalCenterWithProperty = - DependencyProperty.RegisterAttached("AlignVerticalCenterWith", typeof(object), typeof(RelativePanel), new PropertyMetadata(null, OnAlignPropertiesChanged)); + public static readonly AttachedProperty AlignVerticalCenterWithProperty = + AvaloniaProperty.RegisterAttached("AlignVerticalCenterWith"); /// /// Gets the value of the RelativePanel.Below XAML attached property for the target element. @@ -427,7 +450,7 @@ namespace Avalonia.Controls /// (The element to position this element below.) /// [TypeConverter(typeof(NameReferenceConverter))] - public static object GetBelow(DependencyObject obj) + public static object GetBelow(AvaloniaObject obj) { return (object)obj.GetValue(BelowProperty); } @@ -437,7 +460,7 @@ namespace Avalonia.Controls /// /// The object to which the property value is written. /// The value to set. (The element to position this element below.) - public static void SetBelow(DependencyObject obj, object value) + public static void SetBelow(AvaloniaObject obj, object value) { obj.SetValue(BelowProperty, value); } @@ -445,8 +468,8 @@ namespace Avalonia.Controls /// /// Identifies the XAML attached property. /// - public static readonly DependencyProperty BelowProperty = - DependencyProperty.RegisterAttached("Below", typeof(object), typeof(RelativePanel), new PropertyMetadata(null, OnAlignPropertiesChanged)); + public static readonly AttachedProperty BelowProperty = + AvaloniaProperty.RegisterAttached("Below"); /// /// Gets the value of the RelativePanel.LeftOf XAML attached property for the target element. @@ -457,7 +480,7 @@ namespace Avalonia.Controls /// (The element to position this element to the left of.) /// [TypeConverter(typeof(NameReferenceConverter))] - public static object GetLeftOf(DependencyObject obj) + public static object GetLeftOf(AvaloniaObject obj) { return (object)obj.GetValue(LeftOfProperty); } @@ -467,7 +490,7 @@ namespace Avalonia.Controls /// /// The object to which the property value is written. /// The value to set. (The element to position this element to the left of.) - public static void SetLeftOf(DependencyObject obj, object value) + public static void SetLeftOf(AvaloniaObject obj, object value) { obj.SetValue(LeftOfProperty, value); } @@ -475,8 +498,8 @@ namespace Avalonia.Controls /// /// Identifies the XAML attached property. /// - public static readonly DependencyProperty LeftOfProperty = - DependencyProperty.RegisterAttached("LeftOf", typeof(object), typeof(RelativePanel), new PropertyMetadata(null, OnAlignPropertiesChanged)); + public static readonly AttachedProperty LeftOfProperty = + AvaloniaProperty.RegisterAttached("LeftOf"); /// /// Gets the value of the RelativePanel.RightOf XAML attached property for the target element. @@ -487,7 +510,7 @@ namespace Avalonia.Controls /// (The element to position this element to the right of.) /// [TypeConverter(typeof(NameReferenceConverter))] - public static object GetRightOf(DependencyObject obj) + public static object GetRightOf(AvaloniaObject obj) { return (object)obj.GetValue(RightOfProperty); } @@ -497,7 +520,7 @@ namespace Avalonia.Controls /// /// The object to which the property value is written. /// The value to set. (The element to position this element to the right of.) - public static void SetRightOf(DependencyObject obj, object value) + public static void SetRightOf(AvaloniaObject obj, object value) { obj.SetValue(RightOfProperty, value); } @@ -505,7 +528,7 @@ namespace Avalonia.Controls /// /// Identifies the XAML attached property. /// - public static readonly DependencyProperty RightOfProperty = - DependencyProperty.RegisterAttached("RightOf", typeof(object), typeof(RelativePanel), new PropertyMetadata(null, OnAlignPropertiesChanged)); + public static readonly AttachedProperty RightOfProperty = + AvaloniaProperty.RegisterAttached("RightOf"); } } diff --git a/src/Avalonia.Controls/RelativePanel.cs b/src/Avalonia.Controls/RelativePanel.cs index aecd4b7c98..f1989de570 100644 --- a/src/Avalonia.Controls/RelativePanel.cs +++ b/src/Avalonia.Controls/RelativePanel.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; -using System.Text; +using System.Linq; +using Avalonia.Layout; namespace Avalonia.Controls { @@ -37,8 +38,8 @@ namespace Avalonia.Controls public partial class RelativePanel : Panel { // Dependency property for storing intermediate arrange state on the children - private static readonly DependencyProperty ArrangeStateProperty = - DependencyProperty.Register("ArrangeState", typeof(double[]), typeof(RelativePanel), new PropertyMetadata(null)); + private static readonly StyledProperty ArrangeStateProperty = + AvaloniaProperty.Register("ArrangeState"); /// /// When overridden in a derived class, measures the size in layout required for @@ -55,7 +56,7 @@ namespace Avalonia.Controls /// protected override Size MeasureOverride(Size availableSize) { - foreach (var child in Children.OfType()) + foreach (var child in Children.OfType()) { child.Measure(availableSize); } @@ -83,13 +84,13 @@ namespace Avalonia.Controls return base.ArrangeOverride(finalSize); } - private IEnumerable> CalculateLocations(Size finalSize) + private IEnumerable> CalculateLocations(Size finalSize) { //List of margins for each element between the element and panel (left, top, right, bottom) List arranges = new List(Children.Count); //First pass aligns all sides that aren't constrained by other elements int arrangedCount = 0; - foreach (var child in Children.OfType()) + foreach (var child in Children.OfType()) { //NaN means the arrange value is not constrained yet for that side double[] rect = new[] { double.NaN, double.NaN, double.NaN, double.NaN }; @@ -168,7 +169,7 @@ namespace Avalonia.Controls { bool valueChanged = false; i = 0; - foreach (var child in Children.OfType()) + foreach (var child in Children.OfType()) { double[] rect = arranges[i++]; @@ -386,7 +387,7 @@ namespace Avalonia.Controls i = 0; //Arrange iterations complete - Apply the results to the child elements - foreach (var child in Children.OfType()) + foreach (var child in Children.OfType()) { double[] rect = arranges[i++]; //Measure child again with the new calculated available size @@ -401,24 +402,24 @@ namespace Avalonia.Controls // tb.ArrangeOverride(new Rect(rect[0], rect[1], Math.Max(0, finalSize.Width - rect[2] - rect[0]), Math.Max(0, finalSize.Height - rect[3] - rect[1]))); //} //else - yield return new Tuple(child, new Rect(rect[0], rect[1], Math.Max(0, finalSize.Width - rect[2] - rect[0]), Math.Max(0, finalSize.Height - rect[3] - rect[1]))); + yield return new Tuple(child, new Rect(rect[0], rect[1], Math.Max(0, finalSize.Width - rect[2] - rect[0]), Math.Max(0, finalSize.Height - rect[3] - rect[1]))); } } //Gets the element that's referred to in the alignment attached properties - private UIElement GetDependencyElement(DependencyProperty property, DependencyObject child) + private Layoutable GetDependencyElement(AvaloniaProperty property, AvaloniaObject child) { var dependency = child.GetValue(property); if (dependency == null) return null; - if (dependency is UIElement) + if (dependency is Layoutable) { - if (Children.Contains((UIElement)dependency)) - return (UIElement)dependency; + if (Children.Contains((ILayoutable)dependency)) + return (Layoutable)dependency; throw new ArgumentException(string.Format("RelativePanel error: Element does not exist in the current context", property.Name)); } - throw new ArgumentException("RelativePanel error: Value must be of type UIElement"); + throw new ArgumentException("RelativePanel error: Value must be of type ILayoutable"); } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index 06c5375520..724d910b62 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -12,6 +12,7 @@ + diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/NameReferenceConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/NameReferenceConverter.cs new file mode 100644 index 0000000000..885648ffde --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/NameReferenceConverter.cs @@ -0,0 +1,82 @@ +using Avalonia.Controls; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using System; +using System.ComponentModel; +using System.Globalization; + +namespace Avalonia.Markup.Xaml.Converters +{ + public class NameReferenceConverter : TypeConverter + { + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + { + if (sourceType == typeof(string)) + { + return true; + } + + return base.CanConvertFrom(context, sourceType); + } + + public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + //var nameResolver = (IXamlNameResolver)context.GetService(typeof(IXamlNameResolver)); + //if (nameResolver == null) + //{ + // throw new InvalidOperationException(SR.Get(SRID.MissingNameResolver)); + //} + + //string name = value as string; + //if (String.IsNullOrEmpty(name)) + //{ + // throw new InvalidOperationException(SR.Get(SRID.MustHaveName)); + //} + //object obj = nameResolver.Resolve(name); + //if (obj == null) + //{ + // string[] names = new string[] { name }; + // obj = nameResolver.GetFixupToken(names, true); + //} + return null; //obj; + } + + public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) + { + //if (context == null || (context.GetService(typeof(IXamlNameProvider)) as IXamlNameProvider) == null) + //{ + // return false; + //} + + //if (destinationType == typeof(string)) + //{ + // return true; + //} + + return base.CanConvertTo(context, destinationType); + + } + + public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) + { + //if (context == null) + //{ + // throw new ArgumentNullException(nameof(context)); + //} + + //var nameProvider = (IXamlNameProvider)context.GetService(typeof(IXamlNameProvider)); + //if (nameProvider == null) + //{ + // throw new InvalidOperationException(SR.Get(SRID.MissingNameProvider)); + //} + + //return nameProvider.GetName(value); + return null; + } + } +} From e4fd1ad4f3dbfca9f713e0faf0d07734dac4275a Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 30 May 2020 14:41:12 -0300 Subject: [PATCH 053/693] experimental acrylic brush implementation. --- .../Media/AcrylicBackgroundSource.cs | 8 ++ .../Media/ExperimentalAcrylicBrush.cs | 17 +++++ src/Avalonia.Visuals/Media/IBrush.cs | 2 +- .../Media/IExperimentalAcrylicBrush.cs | 15 ++++ src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 74 +++++++++++++++++++ 5 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 src/Avalonia.Visuals/Media/AcrylicBackgroundSource.cs create mode 100644 src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs create mode 100644 src/Avalonia.Visuals/Media/IExperimentalAcrylicBrush.cs diff --git a/src/Avalonia.Visuals/Media/AcrylicBackgroundSource.cs b/src/Avalonia.Visuals/Media/AcrylicBackgroundSource.cs new file mode 100644 index 0000000000..d902752f85 --- /dev/null +++ b/src/Avalonia.Visuals/Media/AcrylicBackgroundSource.cs @@ -0,0 +1,8 @@ +namespace Avalonia.Media +{ + public enum AcrylicBackgroundSource + { + None, + Digger + } +} diff --git a/src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs b/src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs new file mode 100644 index 0000000000..2ab2701c9d --- /dev/null +++ b/src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs @@ -0,0 +1,17 @@ +namespace Avalonia.Media +{ + public class ExperimentalAcrylicBrush : IExperimentalAcrylicBrush + { + public AcrylicBackgroundSource BackgroundSource { get; set; } + + public Color TintColor { get; set; } + + public double TintOpacity { get; set; } + + public Color FallbackColor { get; set; } + + public double TintLuminosityOpacity { get; set; } + + public double Opacity { get; set; } + } +} diff --git a/src/Avalonia.Visuals/Media/IBrush.cs b/src/Avalonia.Visuals/Media/IBrush.cs index 7756e94598..15b7681be4 100644 --- a/src/Avalonia.Visuals/Media/IBrush.cs +++ b/src/Avalonia.Visuals/Media/IBrush.cs @@ -13,4 +13,4 @@ namespace Avalonia.Media /// double Opacity { get; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Visuals/Media/IExperimentalAcrylicBrush.cs b/src/Avalonia.Visuals/Media/IExperimentalAcrylicBrush.cs new file mode 100644 index 0000000000..ae202b58ae --- /dev/null +++ b/src/Avalonia.Visuals/Media/IExperimentalAcrylicBrush.cs @@ -0,0 +1,15 @@ +namespace Avalonia.Media +{ + public interface IExperimentalAcrylicBrush : IBrush + { + AcrylicBackgroundSource BackgroundSource { get; set; } + + Color TintColor { get; set; } + + double TintOpacity { get; set; } + + double TintLuminosityOpacity { get; set; } + + Color FallbackColor { get; set; } + } +} diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 26fdb08a4b..0d628d6b21 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -227,6 +227,13 @@ namespace Avalonia.Skia return bounds; } + private static bool IsLight (Color c) + { + double lightness = (0.299 * c.R + 0.587 * c.G + 0.114 * c.B) / 255; + + return lightness > 0.5; + } + /// public void DrawRectangle(IBrush brush, IPen pen, RoundedRect rect, BoxShadows boxShadows = default) @@ -238,6 +245,17 @@ namespace Avalonia.Skia if (rect.Rect.Height > 8192 || rect.Rect.Width > 8192) boxShadows = default; + if (brush is IExperimentalAcrylicBrush acrylic && IsLight(acrylic.TintColor)) + { + boxShadows = new BoxShadows(new BoxShadow + { + Blur = 2, + OffsetX = 1, + Color = Colors.White, + IsInset = true + }); + } + var rc = rect.Rect.ToSKRect(); var isRounded = rect.IsRounded; var needRoundRect = rect.IsRounded || (boxShadows.HasInsetShadows); @@ -653,6 +671,36 @@ namespace Avalonia.Skia } } + static SKColor SimpleColorBurn(SKColor bg, SKColor fg) + { + using (var bmp = new SKBitmap(1, 1)) + { + bmp.SetPixel(0, 0, bg); + using (var canvas = new SKCanvas(bmp)) + using (var paint = new SKPaint + { + Color = fg + }) + canvas.DrawRect(-1, -1, 3, 3, paint); + return bmp.GetPixel(0, 0); + } + } + + static SKColorFilter CreateAlphaColorFilter(double opacity) + { + if (opacity > 1) + opacity = 1; + var c = new byte[256]; + var a = new byte[256]; + for (var i = 0; i < 256; i++) + { + c[i] = 255; + a[i] = (byte)(i * opacity); + } + + return SKColorFilter.CreateTable(a, c, c, c); + } + /// /// Creates paint wrapper for given brush. /// @@ -676,6 +724,32 @@ namespace Avalonia.Skia return paintWrapper; } + if(brush is IExperimentalAcrylicBrush acrylicBrush) + { + var tintOpacity = acrylicBrush.TintOpacity; + var noiseOpcity = 0.12; + + var excl = new SKColor(255, 255, 255, (byte)(255 * acrylicBrush.TintLuminosityOpacity)); + var tint = new SKColor(acrylicBrush.TintColor.R, acrylicBrush.TintColor.G, acrylicBrush.TintColor.B, (byte)(255 * tintOpacity)); + + tint = SimpleColorBurn(excl, tint); + + var tintShader = SKShader.CreateColor(tint); + var noiseShader = + SKShader.CreatePerlinNoiseTurbulence(12.876f, 12.876f, 2, 0.76829314f) + .WithColorFilter(CreateAlphaColorFilter(noiseOpcity)); + + var compose = SKShader.CreateCompose(tintShader, noiseShader); + paint.Shader = compose; + + if (acrylicBrush.BackgroundSource == AcrylicBackgroundSource.Digger) + { + paint.BlendMode = SKBlendMode.Src; + } + + return paintWrapper; + } + paint.Color = new SKColor(255, 255, 255, (byte) (255 * opacity)); if (brush is IGradientBrush gradient) From 29984b1d45684403723e790dce235fa0bb73c97a Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 30 May 2020 15:12:23 -0300 Subject: [PATCH 054/693] drop light mode inset shadow. --- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 24 ++++---------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 0d628d6b21..307f01ad1a 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -227,14 +227,6 @@ namespace Avalonia.Skia return bounds; } - private static bool IsLight (Color c) - { - double lightness = (0.299 * c.R + 0.587 * c.G + 0.114 * c.B) / 255; - - return lightness > 0.5; - } - - /// public void DrawRectangle(IBrush brush, IPen pen, RoundedRect rect, BoxShadows boxShadows = default) { @@ -245,17 +237,6 @@ namespace Avalonia.Skia if (rect.Rect.Height > 8192 || rect.Rect.Width > 8192) boxShadows = default; - if (brush is IExperimentalAcrylicBrush acrylic && IsLight(acrylic.TintColor)) - { - boxShadows = new BoxShadows(new BoxShadow - { - Blur = 2, - OffsetX = 1, - Color = Colors.White, - IsInset = true - }); - } - var rc = rect.Rect.ToSKRect(); var isRounded = rect.IsRounded; var needRoundRect = rect.IsRounded || (boxShadows.HasInsetShadows); @@ -726,7 +707,10 @@ namespace Avalonia.Skia if(brush is IExperimentalAcrylicBrush acrylicBrush) { - var tintOpacity = acrylicBrush.TintOpacity; + var tintOpacity = + acrylicBrush.BackgroundSource == AcrylicBackgroundSource.Digger ? + acrylicBrush.TintOpacity : 1; + var noiseOpcity = 0.12; var excl = new SKColor(255, 255, 255, (byte)(255 * acrylicBrush.TintLuminosityOpacity)); From bdfc4deb6a7ed262eaa83f023dd34a3bbf71d885 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 30 May 2020 15:41:53 -0300 Subject: [PATCH 055/693] implement immutable acrylic and avalonia properties. --- .../Media/ExperimentalAcrylicBrush.cs | 64 ++++++++++++++-- .../Media/IExperimentalAcrylicBrush.cs | 10 +-- .../ImmutableExperimentalAcrylicBrush.cs | 73 +++++++++++++++++++ 3 files changed, 135 insertions(+), 12 deletions(-) create mode 100644 src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicBrush.cs diff --git a/src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs b/src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs index 2ab2701c9d..3c093b1919 100644 --- a/src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs +++ b/src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs @@ -1,17 +1,67 @@ namespace Avalonia.Media { - public class ExperimentalAcrylicBrush : IExperimentalAcrylicBrush + public class ExperimentalAcrylicBrush : Brush, IExperimentalAcrylicBrush { - public AcrylicBackgroundSource BackgroundSource { get; set; } + static ExperimentalAcrylicBrush() + { + AffectsRender( + TintColorProperty, + BackgroundSourceProperty, + TintOpacityProperty, + TintLuminosityOpacityProperty); + } - public Color TintColor { get; set; } + /// + /// Defines the property. + /// + public static readonly StyledProperty TintColorProperty = + AvaloniaProperty.Register(nameof(TintColor)); - public double TintOpacity { get; set; } + public static readonly StyledProperty BackgroundSourceProperty = + AvaloniaProperty.Register(nameof(BackgroundSource)); - public Color FallbackColor { get; set; } + public static readonly StyledProperty TintOpacityProperty = + AvaloniaProperty.Register(nameof(TintOpacity)); - public double TintLuminosityOpacity { get; set; } + public static readonly StyledProperty TintLuminosityOpacityProperty = + AvaloniaProperty.Register(nameof(TintLuminosityOpacity)); - public double Opacity { get; set; } + public static readonly StyledProperty FallbackColorProperty = + AvaloniaProperty.Register(nameof(FallbackColor)); + + public AcrylicBackgroundSource BackgroundSource + { + get => GetValue(BackgroundSourceProperty); + set => SetValue(BackgroundSourceProperty, value); + } + + public Color TintColor + { + get => GetValue(TintColorProperty); + set => SetValue(TintColorProperty, value); + } + + public double TintOpacity + { + get => GetValue(TintOpacityProperty); + set => SetValue(TintOpacityProperty, value); + } + + public Color FallbackColor + { + get => GetValue(FallbackColorProperty); + set => SetValue(FallbackColorProperty, value); + } + + public double TintLuminosityOpacity + { + get => GetValue(TintLuminosityOpacityProperty); + set => SetValue(TintLuminosityOpacityProperty, value); + } + + public override IBrush ToImmutable() + { + return new ImmutableExperimentalAcrylicBrush(this); + } } } diff --git a/src/Avalonia.Visuals/Media/IExperimentalAcrylicBrush.cs b/src/Avalonia.Visuals/Media/IExperimentalAcrylicBrush.cs index ae202b58ae..adc9335eab 100644 --- a/src/Avalonia.Visuals/Media/IExperimentalAcrylicBrush.cs +++ b/src/Avalonia.Visuals/Media/IExperimentalAcrylicBrush.cs @@ -2,14 +2,14 @@ { public interface IExperimentalAcrylicBrush : IBrush { - AcrylicBackgroundSource BackgroundSource { get; set; } + AcrylicBackgroundSource BackgroundSource { get; } - Color TintColor { get; set; } + Color TintColor { get; } - double TintOpacity { get; set; } + double TintOpacity { get; } - double TintLuminosityOpacity { get; set; } + double TintLuminosityOpacity { get; } - Color FallbackColor { get; set; } + Color FallbackColor { get; } } } diff --git a/src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicBrush.cs b/src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicBrush.cs new file mode 100644 index 0000000000..ad91b5fa42 --- /dev/null +++ b/src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicBrush.cs @@ -0,0 +1,73 @@ +using System; + +namespace Avalonia.Media +{ + public readonly struct ImmutableExperimentalAcrylicBrush : IExperimentalAcrylicBrush, IEquatable + { + public ImmutableExperimentalAcrylicBrush(IExperimentalAcrylicBrush brush) + { + BackgroundSource = brush.BackgroundSource; + TintColor = brush.TintColor; + TintOpacity = brush.TintOpacity; + TintLuminosityOpacity = brush.TintLuminosityOpacity; + FallbackColor = brush.FallbackColor; + Opacity = brush.Opacity; + } + + public AcrylicBackgroundSource BackgroundSource { get; } + + public Color TintColor { get; } + + public double TintOpacity { get; } + + public double TintLuminosityOpacity { get; } + + public Color FallbackColor { get; } + + public double Opacity { get; } + + public bool Equals(ImmutableExperimentalAcrylicBrush other) + { + // ReSharper disable once CompareOfFloatsByEqualityOperator + return + TintColor == other.TintColor && + Opacity == other.Opacity && + TintOpacity == other.TintOpacity && + BackgroundSource == other.BackgroundSource && + TintLuminosityOpacity == other.TintLuminosityOpacity && + FallbackColor == other.FallbackColor; + } + + public override bool Equals(object obj) + { + return obj is ImmutableExperimentalAcrylicBrush other && Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + int hash = 17; + + hash = (hash * 23) + TintColor.GetHashCode(); + hash = (hash * 23) + TintLuminosityOpacity.GetHashCode(); + hash = (hash * 23) + Opacity.GetHashCode(); + hash = (hash * 23) + TintOpacity.GetHashCode(); + hash = (hash * 23) + BackgroundSource.GetHashCode(); + hash = (hash * 23) + FallbackColor.GetHashCode(); + + return hash; + } + } + + public static bool operator ==(ImmutableExperimentalAcrylicBrush left, ImmutableExperimentalAcrylicBrush right) + { + return left.Equals(right); + } + + public static bool operator !=(ImmutableExperimentalAcrylicBrush left, ImmutableExperimentalAcrylicBrush right) + { + return !left.Equals(right); + } + } +} From f329918116cde7c191e22386b0577e95072dd1ff Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 30 May 2020 16:14:09 -0300 Subject: [PATCH 056/693] AcrylicPage. --- samples/ControlCatalog/MainView.xaml | 1 + samples/ControlCatalog/MainWindow.xaml | 1 + samples/ControlCatalog/Pages/AcrylicPage.xaml | 64 +++++++++++++++++++ .../ControlCatalog/Pages/AcrylicPage.xaml.cs | 19 ++++++ 4 files changed, 85 insertions(+) create mode 100644 samples/ControlCatalog/Pages/AcrylicPage.xaml create mode 100644 samples/ControlCatalog/Pages/AcrylicPage.xaml.cs diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index add5dbde84..30c1586ce3 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -14,6 +14,7 @@ + diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index a0bb956425..1ff23d1233 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -7,6 +7,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="clr-namespace:ControlCatalog.ViewModels" xmlns:v="clr-namespace:ControlCatalog.Views" + TransparencyLevelHint="AcrylicBlur" x:Class="ControlCatalog.MainWindow" WindowState="{Binding WindowState, Mode=TwoWay}" Background="Transparent"> diff --git a/samples/ControlCatalog/Pages/AcrylicPage.xaml b/samples/ControlCatalog/Pages/AcrylicPage.xaml new file mode 100644 index 0000000000..6864551ef7 --- /dev/null +++ b/samples/ControlCatalog/Pages/AcrylicPage.xaml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/AcrylicPage.xaml.cs b/samples/ControlCatalog/Pages/AcrylicPage.xaml.cs new file mode 100644 index 0000000000..44e7c4b92b --- /dev/null +++ b/samples/ControlCatalog/Pages/AcrylicPage.xaml.cs @@ -0,0 +1,19 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace ControlCatalog.Pages +{ + public class AcrylicPage : UserControl + { + public AcrylicPage() + { + this.InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} From 831164b865d24d16858863cdcec8362814e2e531 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 30 May 2020 16:18:52 -0300 Subject: [PATCH 057/693] control catalog solid background. --- samples/ControlCatalog/MainView.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 30c1586ce3..923f5ea456 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -61,7 +61,7 @@ - + No Decorations Border Only From f33daa20bf38ab717763bd438ace09888be179c8 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 30 May 2020 16:40:58 -0300 Subject: [PATCH 058/693] acrylic painting takes into account alpha channel and opacity. --- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 307f01ad1a..5003e40fc6 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -711,16 +711,16 @@ namespace Avalonia.Skia acrylicBrush.BackgroundSource == AcrylicBackgroundSource.Digger ? acrylicBrush.TintOpacity : 1; - var noiseOpcity = 0.12; + var noiseOpcity = 0.9; var excl = new SKColor(255, 255, 255, (byte)(255 * acrylicBrush.TintLuminosityOpacity)); - var tint = new SKColor(acrylicBrush.TintColor.R, acrylicBrush.TintColor.G, acrylicBrush.TintColor.B, (byte)(255 * tintOpacity)); + var tint = new SKColor(acrylicBrush.TintColor.R, acrylicBrush.TintColor.G, acrylicBrush.TintColor.B, (byte)(255 * (tintOpacity * acrylicBrush.Opacity * acrylicBrush.TintColor.A))); tint = SimpleColorBurn(excl, tint); var tintShader = SKShader.CreateColor(tint); var noiseShader = - SKShader.CreatePerlinNoiseTurbulence(12.876f, 12.876f, 2, 0.76829314f) + SKShader.CreatePerlinNoiseTurbulence(15.876f, 15.876f, 2, 0.76829314f) .WithColorFilter(CreateAlphaColorFilter(noiseOpcity)); var compose = SKShader.CreateCompose(tintShader, noiseShader); From 1021b0cb017d728006b26e43fd50085c2eb3deb8 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 30 May 2020 16:52:49 -0300 Subject: [PATCH 059/693] correct opacity calculations. --- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 5003e40fc6..b43e20e390 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -711,10 +711,10 @@ namespace Avalonia.Skia acrylicBrush.BackgroundSource == AcrylicBackgroundSource.Digger ? acrylicBrush.TintOpacity : 1; - var noiseOpcity = 0.9; + var noiseOpcity = 0.09 * brush.Opacity; var excl = new SKColor(255, 255, 255, (byte)(255 * acrylicBrush.TintLuminosityOpacity)); - var tint = new SKColor(acrylicBrush.TintColor.R, acrylicBrush.TintColor.G, acrylicBrush.TintColor.B, (byte)(255 * (tintOpacity * acrylicBrush.Opacity * acrylicBrush.TintColor.A))); + var tint = new SKColor(acrylicBrush.TintColor.R, acrylicBrush.TintColor.G, acrylicBrush.TintColor.B, (byte)(255 * (tintOpacity * acrylicBrush.Opacity * (acrylicBrush.TintColor.A / 255.0)))); tint = SimpleColorBurn(excl, tint); From 96f5ccb7f5b1344492e20ccd5a8e5560ffccf76e Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 30 May 2020 18:09:16 -0300 Subject: [PATCH 060/693] implementation of limiting opacity. --- samples/ControlCatalog/Pages/AcrylicPage.xaml | 34 ++- .../Media/ExperimentalAcrylicBrush.cs | 207 +++++++++++++++++- .../Media/IExperimentalAcrylicBrush.cs | 12 +- .../ImmutableExperimentalAcrylicBrush.cs | 17 +- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 6 +- 5 files changed, 259 insertions(+), 17 deletions(-) diff --git a/samples/ControlCatalog/Pages/AcrylicPage.xaml b/samples/ControlCatalog/Pages/AcrylicPage.xaml index 6864551ef7..ce10956fca 100644 --- a/samples/ControlCatalog/Pages/AcrylicPage.xaml +++ b/samples/ControlCatalog/Pages/AcrylicPage.xaml @@ -6,21 +6,21 @@ x:Class="ControlCatalog.Pages.AcrylicPage"> - - + + @@ -30,7 +30,17 @@ + + + + + + @@ -42,7 +52,17 @@ + + + + + + @@ -52,7 +72,7 @@ diff --git a/src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs b/src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs index 3c093b1919..2250e175b1 100644 --- a/src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs +++ b/src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs @@ -1,4 +1,6 @@ -namespace Avalonia.Media +using System; + +namespace Avalonia.Media { public class ExperimentalAcrylicBrush : Brush, IExperimentalAcrylicBrush { @@ -10,7 +12,7 @@ TintOpacityProperty, TintLuminosityOpacityProperty); } - + /// /// Defines the property. /// @@ -53,7 +55,7 @@ set => SetValue(FallbackColorProperty, value); } - public double TintLuminosityOpacity + public double? TintLuminosityOpacity { get => GetValue(TintLuminosityOpacityProperty); set => SetValue(TintLuminosityOpacityProperty, value); @@ -63,5 +65,204 @@ { return new ImmutableExperimentalAcrylicBrush(this); } + + public struct HsvColor + { + public float Hue { get; set; } + public float Saturation { get; set; } + public float Value { get; set; } + } + + public static Color FromHsv(HsvColor color) + { + var i = (float)Math.Floor(color.Hue * 6f); + var f = color.Hue * 6f - i; + var p = color.Value * (1f - color.Saturation); + var q = color.Value * (1f - f * color.Saturation); + var t = color.Value * (1f - (1f - f) * color.Saturation); + + switch (i % 6) + { + case 0: + return new Color(255, (byte)(255.0 * color.Value), (byte)(255.0 * t), (byte)(255.0 * p)); + case 1: + return new Color(255, (byte)(255.0 * q), (byte)(255.0 * color.Value), (byte)(255.0 * p)); + case 2: + return new Color(255, (byte)(255.0 * p), (byte)(255.0 * color.Value), (byte)(255.0 * t)); + case 3: + return new Color(255, (byte)(255.0 * p), (byte)(255.0 * q), (byte)(255.0 * color.Value)); + case 4: + return new Color(255, (byte)(255.0 * t), (byte)(255.0 * p), (byte)(255.0 * color.Value)); + default: + case 5: + return new Color(255, (byte)(255.0 * color.Value), (byte)(255.0 * p), (byte)(255.0 * q)); + } + } + + public static HsvColor RgbToHsv(Color color) + { + var r = color.R /255.0f; + var g = color.G / 255.0f; + var b = color.B / 255.0f; + var max = Math.Max(r, Math.Max(g, b)); + var min = Math.Min(r, Math.Min(g, b)); + + float h, s, v; + h = s = v = max; + + //v = (0.299f * r + 0.587f * g + 0.114f * b); + + var d = max - min; + s = max == 0 ? 0 : d / max; + + if (max == min) + { + h = 0; // achromatic + } + else + { + if (max == r) + { + h = (g - b) / d + (g < b ? 6 : 0); + } + else if (max == g) + { + h = (b - r) / d + 2; + } + else if (max == b) + { + h = (r - g) / d + 4; + } + + h /= 6; + } + + return new HsvColor { Hue = h, Saturation = s, Value = v }; + } + + public Color GetLuminosityColor () + { + return GetLuminosityColor(TintColor, TintLuminosityOpacity); + } + + Color GetLuminosityColor(Color tintColor, double? luminosityOpacity) + { + var rgbTintColor = tintColor; + + // If luminosity opacity is specified, just use the values as is + if (luminosityOpacity.HasValue) + { + return new Color((byte)(255.0 * Math.Min(1.0, Math.Max(0.0, luminosityOpacity.Value))), tintColor.R, tintColor.G, tintColor.B); + } + else + { + // To create the Luminosity blend input color without luminosity opacity, + // we're taking the TintColor input, converting to HSV, and clamping the V between these values + const double minHsvV = 0.125; + const double maxHsvV = 0.965; + + var hsvTintColor = RgbToHsv(rgbTintColor); + + var clampedHsvV = Math.Max(Math.Min(hsvTintColor.Value, minHsvV), maxHsvV); + var hsvLuminosityColor = hsvTintColor; + hsvLuminosityColor.Value = (float)clampedHsvV; + + var rgbLuminosityColor = FromHsv(hsvLuminosityColor); + + // Now figure out luminosity opacity + // Map original *tint* opacity to this range + const double minLuminosityOpacity = 0.15; + const double maxLuminosityOpacity = 1.03; + + double luminosityOpacityRangeMax = maxLuminosityOpacity - minLuminosityOpacity; + double mappedTintOpacity = ((tintColor.A / 255.0) * luminosityOpacityRangeMax) + minLuminosityOpacity; + + // Finally, combine the luminosity opacity and the HsvV-clamped tint color + return new Color((byte)(255.0 * Math.Min(mappedTintOpacity, 1.0)), rgbLuminosityColor.R, rgbLuminosityColor.G, rgbLuminosityColor.B); + } + + } + + + public Color GetEffectiveTintColor() + { + var tintColor = TintColor; + double tintOpacity = TintOpacity; + + // Update tintColor's alpha with the combined opacity value + // If LuminosityOpacity was specified, we don't intervene into users parameters + if (false)//TintLuminosityOpacity() != nullptr) + { + //tintColor.A = static_cast(round(tintColor.A * tintOpacity)); + } + else + { + double tintOpacityModifier = GetTintOpacityModifier(tintColor); + + tintColor = new Color((byte)(Math.Round(tintColor.A * tintOpacity * tintOpacityModifier)), tintColor.R, tintColor.G, tintColor.B); + } + + return tintColor; + } + + double GetTintOpacityModifier(Color tintColor) + { + // This method supresses the maximum allowable tint opacity depending on the luminosity and saturation of a color by + // compressing the range of allowable values - for example, a user-defined value of 100% will be mapped to 45% for pure + // white (100% luminosity), 85% for pure black (0% luminosity), and 90% for pure gray (50% luminosity). The intensity of + // the effect increases linearly as luminosity deviates from 50%. After this effect is calculated, we cancel it out + // linearly as saturation increases from zero. + + const double midPoint = 0.50; // Mid point of HsvV range that these calculations are based on. This is here for easy tuning. + + const double whiteMaxOpacity = 0.45; // 100% luminosity + const double midPointMaxOpacity = 0.90; // 50% luminosity + const double blackMaxOpacity = 0.85; // 0% luminosity + + var hsv = RgbToHsv(tintColor); + + if(tintColor == Colors.Red) + { + + } + + double opacityModifier = midPointMaxOpacity; + + if (hsv.Value != midPoint) + { + // Determine maximum suppression amount + double lowestMaxOpacity = midPointMaxOpacity; + double maxDeviation = midPoint; + + if (hsv.Value > midPoint) + { + lowestMaxOpacity = whiteMaxOpacity; // At white (100% hsvV) + maxDeviation = 1 - maxDeviation; + } + else if (hsv.Value < midPoint) + { + lowestMaxOpacity = blackMaxOpacity; // At black (0% hsvV) + } + + double maxOpacitySuppression = midPointMaxOpacity - lowestMaxOpacity; + + // Determine normalized deviation from the midpoint + double deviation = Math.Abs(hsv.Value - midPoint); + double normalizedDeviation = deviation / maxDeviation; + + // If we have saturation, reduce opacity suppression to allow that color to come through more + if (hsv.Saturation > 0) + { + // Dampen opacity suppression based on how much saturation there is + maxOpacitySuppression *= Math.Max(1 - (hsv.Saturation * 2), 0.0); + } + + double opacitySuppression = maxOpacitySuppression * normalizedDeviation; + + opacityModifier = midPointMaxOpacity - opacitySuppression; + } + + return opacityModifier; + } } } diff --git a/src/Avalonia.Visuals/Media/IExperimentalAcrylicBrush.cs b/src/Avalonia.Visuals/Media/IExperimentalAcrylicBrush.cs index adc9335eab..af7bf02e15 100644 --- a/src/Avalonia.Visuals/Media/IExperimentalAcrylicBrush.cs +++ b/src/Avalonia.Visuals/Media/IExperimentalAcrylicBrush.cs @@ -1,15 +1,21 @@ -namespace Avalonia.Media +using System.Drawing; + +namespace Avalonia.Media { public interface IExperimentalAcrylicBrush : IBrush { AcrylicBackgroundSource BackgroundSource { get; } - Color TintColor { get; } + Color TintColor { get; } double TintOpacity { get; } - double TintLuminosityOpacity { get; } + double? TintLuminosityOpacity { get; } Color FallbackColor { get; } + + Color GetEffectiveTintColor(); + + Color GetLuminosityColor(); } } diff --git a/src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicBrush.cs b/src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicBrush.cs index ad91b5fa42..67a94c4fb3 100644 --- a/src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicBrush.cs +++ b/src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicBrush.cs @@ -4,10 +4,13 @@ namespace Avalonia.Media { public readonly struct ImmutableExperimentalAcrylicBrush : IExperimentalAcrylicBrush, IEquatable { + private readonly Color luminosityColor; + public ImmutableExperimentalAcrylicBrush(IExperimentalAcrylicBrush brush) { + luminosityColor = brush.GetLuminosityColor(); BackgroundSource = brush.BackgroundSource; - TintColor = brush.TintColor; + TintColor = brush.GetEffectiveTintColor(); TintOpacity = brush.TintOpacity; TintLuminosityOpacity = brush.TintLuminosityOpacity; FallbackColor = brush.FallbackColor; @@ -20,7 +23,7 @@ namespace Avalonia.Media public double TintOpacity { get; } - public double TintLuminosityOpacity { get; } + public double? TintLuminosityOpacity { get; } public Color FallbackColor { get; } @@ -43,6 +46,11 @@ namespace Avalonia.Media return obj is ImmutableExperimentalAcrylicBrush other && Equals(other); } + public Color GetEffectiveTintColor() + { + return TintColor; + } + public override int GetHashCode() { unchecked @@ -60,6 +68,11 @@ namespace Avalonia.Media } } + public Color GetLuminosityColor() + { + return luminosityColor; + } + public static bool operator ==(ImmutableExperimentalAcrylicBrush left, ImmutableExperimentalAcrylicBrush right) { return left.Equals(right); diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index b43e20e390..ec3c254a4d 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -713,8 +713,10 @@ namespace Avalonia.Skia var noiseOpcity = 0.09 * brush.Opacity; - var excl = new SKColor(255, 255, 255, (byte)(255 * acrylicBrush.TintLuminosityOpacity)); - var tint = new SKColor(acrylicBrush.TintColor.R, acrylicBrush.TintColor.G, acrylicBrush.TintColor.B, (byte)(255 * (tintOpacity * acrylicBrush.Opacity * (acrylicBrush.TintColor.A / 255.0)))); + var tintColor = acrylicBrush.GetEffectiveTintColor(); + var luminosityColor = acrylicBrush.GetLuminosityColor(); + var excl = new SKColor(luminosityColor.R, luminosityColor.G, luminosityColor.B, (byte)(255* (luminosityColor.A /255.0) * 0.1)); + var tint = new SKColor(tintColor.R, tintColor.G, tintColor.B, (byte)(255 * ((tintColor.A /255.0) * acrylicBrush.Opacity))); tint = SimpleColorBurn(excl, tint); From 1ec1fafe9119205c4976784360661b4ff5ace844 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 30 May 2020 21:56:06 -0300 Subject: [PATCH 061/693] simplyfy acrylic brush based on avalonias capabilities. --- samples/ControlCatalog/Pages/AcrylicPage.xaml | 38 ++++++-- .../Media/ExperimentalAcrylicBrush.cs | 92 ++----------------- .../Media/IExperimentalAcrylicBrush.cs | 4 - .../ImmutableExperimentalAcrylicBrush.cs | 15 +-- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 3 +- 5 files changed, 40 insertions(+), 112 deletions(-) diff --git a/samples/ControlCatalog/Pages/AcrylicPage.xaml b/samples/ControlCatalog/Pages/AcrylicPage.xaml index ce10956fca..6155d6d68d 100644 --- a/samples/ControlCatalog/Pages/AcrylicPage.xaml +++ b/samples/ControlCatalog/Pages/AcrylicPage.xaml @@ -8,13 +8,11 @@ - @@ -32,7 +30,6 @@ @@ -42,7 +39,6 @@ @@ -54,7 +50,6 @@ @@ -64,7 +59,6 @@ @@ -72,9 +66,37 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs b/src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs index 2250e175b1..99c680ce8b 100644 --- a/src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs +++ b/src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs @@ -73,32 +73,6 @@ namespace Avalonia.Media public float Value { get; set; } } - public static Color FromHsv(HsvColor color) - { - var i = (float)Math.Floor(color.Hue * 6f); - var f = color.Hue * 6f - i; - var p = color.Value * (1f - color.Saturation); - var q = color.Value * (1f - f * color.Saturation); - var t = color.Value * (1f - (1f - f) * color.Saturation); - - switch (i % 6) - { - case 0: - return new Color(255, (byte)(255.0 * color.Value), (byte)(255.0 * t), (byte)(255.0 * p)); - case 1: - return new Color(255, (byte)(255.0 * q), (byte)(255.0 * color.Value), (byte)(255.0 * p)); - case 2: - return new Color(255, (byte)(255.0 * p), (byte)(255.0 * color.Value), (byte)(255.0 * t)); - case 3: - return new Color(255, (byte)(255.0 * p), (byte)(255.0 * q), (byte)(255.0 * color.Value)); - case 4: - return new Color(255, (byte)(255.0 * t), (byte)(255.0 * p), (byte)(255.0 * color.Value)); - default: - case 5: - return new Color(255, (byte)(255.0 * color.Value), (byte)(255.0 * p), (byte)(255.0 * q)); - } - } - public static HsvColor RgbToHsv(Color color) { var r = color.R /255.0f; @@ -140,65 +114,20 @@ namespace Avalonia.Media return new HsvColor { Hue = h, Saturation = s, Value = v }; } - public Color GetLuminosityColor () - { - return GetLuminosityColor(TintColor, TintLuminosityOpacity); - } - - Color GetLuminosityColor(Color tintColor, double? luminosityOpacity) - { - var rgbTintColor = tintColor; - - // If luminosity opacity is specified, just use the values as is - if (luminosityOpacity.HasValue) - { - return new Color((byte)(255.0 * Math.Min(1.0, Math.Max(0.0, luminosityOpacity.Value))), tintColor.R, tintColor.G, tintColor.B); - } - else - { - // To create the Luminosity blend input color without luminosity opacity, - // we're taking the TintColor input, converting to HSV, and clamping the V between these values - const double minHsvV = 0.125; - const double maxHsvV = 0.965; - - var hsvTintColor = RgbToHsv(rgbTintColor); - - var clampedHsvV = Math.Max(Math.Min(hsvTintColor.Value, minHsvV), maxHsvV); - var hsvLuminosityColor = hsvTintColor; - hsvLuminosityColor.Value = (float)clampedHsvV; - - var rgbLuminosityColor = FromHsv(hsvLuminosityColor); - - // Now figure out luminosity opacity - // Map original *tint* opacity to this range - const double minLuminosityOpacity = 0.15; - const double maxLuminosityOpacity = 1.03; - - double luminosityOpacityRangeMax = maxLuminosityOpacity - minLuminosityOpacity; - double mappedTintOpacity = ((tintColor.A / 255.0) * luminosityOpacityRangeMax) + minLuminosityOpacity; - - // Finally, combine the luminosity opacity and the HsvV-clamped tint color - return new Color((byte)(255.0 * Math.Min(mappedTintOpacity, 1.0)), rgbLuminosityColor.R, rgbLuminosityColor.G, rgbLuminosityColor.B); - } - - } - - public Color GetEffectiveTintColor() { var tintColor = TintColor; double tintOpacity = TintOpacity; // Update tintColor's alpha with the combined opacity value - // If LuminosityOpacity was specified, we don't intervene into users parameters - if (false)//TintLuminosityOpacity() != nullptr) + double tintOpacityModifier = GetTintOpacityModifier(tintColor); + + if (false) // non-acrylic blue // TODO detect blur level. { - //tintColor.A = static_cast(round(tintColor.A * tintOpacity)); + tintColor = new Color((byte)(Math.Round(tintColor.A * (((tintOpacity * tintOpacityModifier) * 0.25) + 0.75))), tintColor.R, tintColor.G, tintColor.B); } else { - double tintOpacityModifier = GetTintOpacityModifier(tintColor); - tintColor = new Color((byte)(Math.Round(tintColor.A * tintOpacity * tintOpacityModifier)), tintColor.R, tintColor.G, tintColor.B); } @@ -213,19 +142,14 @@ namespace Avalonia.Media // the effect increases linearly as luminosity deviates from 50%. After this effect is calculated, we cancel it out // linearly as saturation increases from zero. - const double midPoint = 0.50; // Mid point of HsvV range that these calculations are based on. This is here for easy tuning. + const double midPoint = 0.5; // Mid point of HsvV range that these calculations are based on. This is here for easy tuning. - const double whiteMaxOpacity = 0.45; // 100% luminosity - const double midPointMaxOpacity = 0.90; // 50% luminosity - const double blackMaxOpacity = 0.85; // 0% luminosity + const double whiteMaxOpacity = 0.40; // 100% luminosity + const double midPointMaxOpacity = 0.50; // 50% luminosity + const double blackMaxOpacity = 0.84; // 0% luminosity var hsv = RgbToHsv(tintColor); - if(tintColor == Colors.Red) - { - - } - double opacityModifier = midPointMaxOpacity; if (hsv.Value != midPoint) diff --git a/src/Avalonia.Visuals/Media/IExperimentalAcrylicBrush.cs b/src/Avalonia.Visuals/Media/IExperimentalAcrylicBrush.cs index af7bf02e15..3e66d5c863 100644 --- a/src/Avalonia.Visuals/Media/IExperimentalAcrylicBrush.cs +++ b/src/Avalonia.Visuals/Media/IExperimentalAcrylicBrush.cs @@ -10,12 +10,8 @@ namespace Avalonia.Media double TintOpacity { get; } - double? TintLuminosityOpacity { get; } - Color FallbackColor { get; } Color GetEffectiveTintColor(); - - Color GetLuminosityColor(); } } diff --git a/src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicBrush.cs b/src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicBrush.cs index 67a94c4fb3..6765ecae79 100644 --- a/src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicBrush.cs +++ b/src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicBrush.cs @@ -4,15 +4,11 @@ namespace Avalonia.Media { public readonly struct ImmutableExperimentalAcrylicBrush : IExperimentalAcrylicBrush, IEquatable { - private readonly Color luminosityColor; - public ImmutableExperimentalAcrylicBrush(IExperimentalAcrylicBrush brush) - { - luminosityColor = brush.GetLuminosityColor(); + { BackgroundSource = brush.BackgroundSource; TintColor = brush.GetEffectiveTintColor(); TintOpacity = brush.TintOpacity; - TintLuminosityOpacity = brush.TintLuminosityOpacity; FallbackColor = brush.FallbackColor; Opacity = brush.Opacity; } @@ -23,8 +19,6 @@ namespace Avalonia.Media public double TintOpacity { get; } - public double? TintLuminosityOpacity { get; } - public Color FallbackColor { get; } public double Opacity { get; } @@ -37,7 +31,6 @@ namespace Avalonia.Media Opacity == other.Opacity && TintOpacity == other.TintOpacity && BackgroundSource == other.BackgroundSource && - TintLuminosityOpacity == other.TintLuminosityOpacity && FallbackColor == other.FallbackColor; } @@ -58,7 +51,6 @@ namespace Avalonia.Media int hash = 17; hash = (hash * 23) + TintColor.GetHashCode(); - hash = (hash * 23) + TintLuminosityOpacity.GetHashCode(); hash = (hash * 23) + Opacity.GetHashCode(); hash = (hash * 23) + TintOpacity.GetHashCode(); hash = (hash * 23) + BackgroundSource.GetHashCode(); @@ -68,11 +60,6 @@ namespace Avalonia.Media } } - public Color GetLuminosityColor() - { - return luminosityColor; - } - public static bool operator ==(ImmutableExperimentalAcrylicBrush left, ImmutableExperimentalAcrylicBrush right) { return left.Equals(right); diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index ec3c254a4d..2272b28273 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -714,8 +714,7 @@ namespace Avalonia.Skia var noiseOpcity = 0.09 * brush.Opacity; var tintColor = acrylicBrush.GetEffectiveTintColor(); - var luminosityColor = acrylicBrush.GetLuminosityColor(); - var excl = new SKColor(luminosityColor.R, luminosityColor.G, luminosityColor.B, (byte)(255* (luminosityColor.A /255.0) * 0.1)); + var excl = new SKColor(255, 255, 255, (byte)(255 * 0.1)); var tint = new SKColor(tintColor.R, tintColor.G, tintColor.B, (byte)(255 * ((tintColor.A /255.0) * acrylicBrush.Opacity))); tint = SimpleColorBurn(excl, tint); From 25093194491423b3bb90d36f512f78311e98c9c7 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 30 May 2020 22:00:06 -0300 Subject: [PATCH 062/693] black background on control catalog --- samples/ControlCatalog/MainView.xaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 923f5ea456..fde8191407 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -2,7 +2,7 @@ xmlns:pages="clr-namespace:ControlCatalog.Pages" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="ControlCatalog.MainView" - Background="Transparent" + Background="Black" Foreground="{DynamicResource ThemeForegroundBrush}" FontSize="{DynamicResource FontSizeNormal}"> @@ -61,7 +61,7 @@ - + No Decorations Border Only From ff0e25c1d0e276f8cd0ccf78b6718273c3aa2c1a Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 30 May 2020 22:35:31 -0300 Subject: [PATCH 063/693] tone noise down. --- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 2272b28273..2c47edbc48 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -711,7 +711,7 @@ namespace Avalonia.Skia acrylicBrush.BackgroundSource == AcrylicBackgroundSource.Digger ? acrylicBrush.TintOpacity : 1; - var noiseOpcity = 0.09 * brush.Opacity; + var noiseOpcity = 0.04 * brush.Opacity; var tintColor = acrylicBrush.GetEffectiveTintColor(); var excl = new SKColor(255, 255, 255, (byte)(255 * 0.1)); From d06e85abe3dee50f06b8a682454b08f3499189eb Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 30 May 2020 23:32:28 -0300 Subject: [PATCH 064/693] dispose and cache stuff. --- .../Media/ExperimentalAcrylicBrush.cs | 21 ++++++++++--- .../Media/IExperimentalAcrylicBrush.cs | 2 -- .../ImmutableExperimentalAcrylicBrush.cs | 2 +- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 31 ++++++++++--------- 4 files changed, 33 insertions(+), 23 deletions(-) diff --git a/src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs b/src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs index 99c680ce8b..cda4c1c5a2 100644 --- a/src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs +++ b/src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs @@ -4,6 +4,8 @@ namespace Avalonia.Media { public class ExperimentalAcrylicBrush : Brush, IExperimentalAcrylicBrush { + private Color _effectiveTintColor; + static ExperimentalAcrylicBrush() { AffectsRender( @@ -11,6 +13,16 @@ namespace Avalonia.Media BackgroundSourceProperty, TintOpacityProperty, TintLuminosityOpacityProperty); + + TintColorProperty.Changed.AddClassHandler((b, e) => + { + b._effectiveTintColor = GetEffectiveTintColor(b.TintColor, b.TintOpacity); + }); + + TintOpacityProperty.Changed.AddClassHandler((b, e) => + { + b._effectiveTintColor = GetEffectiveTintColor(b.TintColor, b.TintOpacity); + }); } /// @@ -43,6 +55,8 @@ namespace Avalonia.Media set => SetValue(TintColorProperty, value); } + Color IExperimentalAcrylicBrush.TintColor => _effectiveTintColor; + public double TintOpacity { get => GetValue(TintOpacityProperty); @@ -114,11 +128,8 @@ namespace Avalonia.Media return new HsvColor { Hue = h, Saturation = s, Value = v }; } - public Color GetEffectiveTintColor() + private static Color GetEffectiveTintColor(Color tintColor, double tintOpacity) { - var tintColor = TintColor; - double tintOpacity = TintOpacity; - // Update tintColor's alpha with the combined opacity value double tintOpacityModifier = GetTintOpacityModifier(tintColor); @@ -134,7 +145,7 @@ namespace Avalonia.Media return tintColor; } - double GetTintOpacityModifier(Color tintColor) + private static double GetTintOpacityModifier(Color tintColor) { // This method supresses the maximum allowable tint opacity depending on the luminosity and saturation of a color by // compressing the range of allowable values - for example, a user-defined value of 100% will be mapped to 45% for pure diff --git a/src/Avalonia.Visuals/Media/IExperimentalAcrylicBrush.cs b/src/Avalonia.Visuals/Media/IExperimentalAcrylicBrush.cs index 3e66d5c863..fdcf6148fe 100644 --- a/src/Avalonia.Visuals/Media/IExperimentalAcrylicBrush.cs +++ b/src/Avalonia.Visuals/Media/IExperimentalAcrylicBrush.cs @@ -11,7 +11,5 @@ namespace Avalonia.Media double TintOpacity { get; } Color FallbackColor { get; } - - Color GetEffectiveTintColor(); } } diff --git a/src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicBrush.cs b/src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicBrush.cs index 6765ecae79..ed3f532035 100644 --- a/src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicBrush.cs +++ b/src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicBrush.cs @@ -7,7 +7,7 @@ namespace Avalonia.Media public ImmutableExperimentalAcrylicBrush(IExperimentalAcrylicBrush brush) { BackgroundSource = brush.BackgroundSource; - TintColor = brush.GetEffectiveTintColor(); + TintColor = brush.TintColor; TintOpacity = brush.TintOpacity; FallbackColor = brush.FallbackColor; Opacity = brush.Opacity; diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 2c47edbc48..039213aecc 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -705,34 +705,35 @@ namespace Avalonia.Skia return paintWrapper; } - if(brush is IExperimentalAcrylicBrush acrylicBrush) + if (brush is IExperimentalAcrylicBrush acrylicBrush) { - var tintOpacity = - acrylicBrush.BackgroundSource == AcrylicBackgroundSource.Digger ? + var tintOpacity = + acrylicBrush.BackgroundSource == AcrylicBackgroundSource.Digger ? acrylicBrush.TintOpacity : 1; var noiseOpcity = 0.04 * brush.Opacity; - var tintColor = acrylicBrush.GetEffectiveTintColor(); + var tintColor = acrylicBrush.TintColor; var excl = new SKColor(255, 255, 255, (byte)(255 * 0.1)); - var tint = new SKColor(tintColor.R, tintColor.G, tintColor.B, (byte)(255 * ((tintColor.A /255.0) * acrylicBrush.Opacity))); + var tint = new SKColor(tintColor.R, tintColor.G, tintColor.B, (byte)(255 * ((tintColor.A / 255.0) * acrylicBrush.Opacity))); tint = SimpleColorBurn(excl, tint); - var tintShader = SKShader.CreateColor(tint); - var noiseShader = + using (var tintShader = SKShader.CreateColor(tint)) + using (var noiseShader = SKShader.CreatePerlinNoiseTurbulence(15.876f, 15.876f, 2, 0.76829314f) - .WithColorFilter(CreateAlphaColorFilter(noiseOpcity)); + .WithColorFilter(CreateAlphaColorFilter(noiseOpcity))) + using (var compose = SKShader.CreateCompose(tintShader, noiseShader)) + { + paint.Shader = compose; - var compose = SKShader.CreateCompose(tintShader, noiseShader); - paint.Shader = compose; + if (acrylicBrush.BackgroundSource == AcrylicBackgroundSource.Digger) + { + paint.BlendMode = SKBlendMode.Src; + } - if (acrylicBrush.BackgroundSource == AcrylicBackgroundSource.Digger) - { - paint.BlendMode = SKBlendMode.Src; + return paintWrapper; } - - return paintWrapper; } paint.Color = new SKColor(255, 255, 255, (byte) (255 * opacity)); From 99c31c506f5507fc290259b47916a63a8ec5ad63 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 30 May 2020 23:44:12 -0300 Subject: [PATCH 065/693] more noise. --- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 039213aecc..01eddd2aad 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -711,7 +711,7 @@ namespace Avalonia.Skia acrylicBrush.BackgroundSource == AcrylicBackgroundSource.Digger ? acrylicBrush.TintOpacity : 1; - var noiseOpcity = 0.04 * brush.Opacity; + var noiseOpcity = 0.06 * brush.Opacity; var tintColor = acrylicBrush.TintColor; var excl = new SKColor(255, 255, 255, (byte)(255 * 0.1)); From 91631271e7568693c9b83ceb47d0fdad83e1a13d Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sun, 31 May 2020 12:09:40 -0300 Subject: [PATCH 066/693] use invisible black for accent gradient on win32 acrylic blur. --- src/Windows/Avalonia.Win32/WindowImpl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 0cf5a73b9f..62c6fc742b 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -267,7 +267,7 @@ namespace Avalonia.Win32 } accent.AccentFlags = 2; - accent.GradientColor = 0x00FFFFFF; + accent.GradientColor = 0x01000000; var accentPtr = Marshal.AllocHGlobal(accentStructSize); Marshal.StructureToPtr(accent, accentPtr, false); From d2fa8e490544603581796ccb42f0f5dd830a8135 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sun, 31 May 2020 14:46:13 -0300 Subject: [PATCH 067/693] use embedded noise asset. --- .../Assets/NoiseAsset_256X256_PNG.png | Bin 0 -> 3241 bytes src/Skia/Avalonia.Skia/Avalonia.Skia.csproj | 3 ++ src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 39 +++++++----------- 3 files changed, 18 insertions(+), 24 deletions(-) create mode 100644 src/Skia/Avalonia.Skia/Assets/NoiseAsset_256X256_PNG.png diff --git a/src/Skia/Avalonia.Skia/Assets/NoiseAsset_256X256_PNG.png b/src/Skia/Avalonia.Skia/Assets/NoiseAsset_256X256_PNG.png new file mode 100644 index 0000000000000000000000000000000000000000..41de173d9014b38f8290acf270c490b37fc79a60 GIT binary patch literal 3241 zcmV;a3|8}rP)QN3MgRZ*%*@Q0nVDv0W-~K00002< zk9uhU01PcjL_t(|+U#A4b|N_pEcy8VUmmMU29f{)!qIKbyj=nf-L`d=I=}Qch#V$^Ji@p%8Ow}aVg{OQfFawvx$7QXHNf;JlH z>=OGpehiMY6qvQ>r+31?Qu`coulkvHr8sf?eJ%Pq`{!L3HPrR)ZPuzD>pymc)ERd) zzk>rC1G3S8Q*}_1#B7CMSB{eOXyX??K=#C&ZSfa;COcp~#v&$=7`2lg#z$%Z{KSqq zKgR=5D%9d=VNhEG;T6Bn*jciRjl(~1ghpC5?^iE1h+v&EM5%$(W^~Kq7Js%C)S$8R z&&Vf{$L|gr4t5WQ47&JVk;>Q~QqTw0G|KqPrW>W$rJ#h6`dG*ziIh6YM-(w-d_ahz ziCLs8i6~hT(>ah5w;KCCXvRidq*{|KIJHcYAbB!zNskc|{4NutczYczbgqZ0iFCnW zsml~G<0~cJm8Lcg-XClfsxBV7RD|?NwHq6y+E5ESTM>I)D+Wc$hvpJtO?p~5Qg6~9 zF8L;*25*`-T_?5T2C1B>DSuAuR1~&MUa-f+18ELrq4EvMW)^~QU_#>sU6Lx3Bulc`iuXz5gSp3Y5ql*T!p+08KROn z54-e)hzb7ZOlVZE<|Csr2oqhdDSf2{^pipyR3pxkltWD5Xrhq_c-ch|MO=ug1cWpm zMxhs>VGW|2LkntvY%7y7lu$m>N(aX-J(NM=KYzXps?fCt@nx8THwa5*vx}3~4o$rY z*BxwSC8jhr15tEmW{a>O_$W=9?m*V!GNcejK^+%5b-~I6@?q_4h-amS(hB(R;QgIi z@7)vt#baDZC-o(z7T`pVIV}lw*6WnMs_#O1^^Y)E;=#H#PJDH9cD9E2mG~$N zOJnZDfpysHK41$2Dg5Q&H8Ok@V z_BIh+f=y1!p>PG0gIdFbEOK8D?rLZAEcmOuk3p?&%Q!F5>Z*^s~XRzn`uvpVHt>ZE@mpO;urk=<&lRTw~623`?ognS}u(o9b+f>U8&Q* zHFOIPUL7H^`w+1nC@$BhkhvD1FZq#*&biTE-Dzw~Bf~QmN{fp=)*-%inhk1JtrPqq z2ft2NUhO*alS*|4IZDh2zt8*dHjg7Lh;v25p(#0-{`m$}Q_aLdBgWYQ*((?!iM%(O zP1or$Q1aSNTyS~r;i)TBr#^)PG;Px{svgv6AycK!aD2E3VMS;#=(W)MbhrmeU=a%B zBxkAvXh$I*KkPeI%D~Is%3iiUOSGmV12c4V@Ca_9h!|Q`4+g;!iY;HoHy;~8S zTP&zCebpfC=*h{G7O58rCu7&qj3>;U@S@u_Ii7NznOW0+@>!;X6mcj>D36U%-?uVC+#}<{>nL%O`i}W9)Cw89T@#oNG=G+Jk zV^b@YAIMNeh9rndV+>dDZ*#W%5OgnEO7U|aE-_lFCRdtJy^a@Shyy?fq( z{aWznz$$i>+W#7GPQPE^jk{AenMj2fq~38(QEgh#w&@%UynFh&wJ1D3F#p)p9t-Sy zUFYvh+So}t-^F&0yG!qWGPoj1`QVEwqgee&&EIm@kUJ=@!pg~<->?1xvTc+5 zo^6Iz10A<&2y(3g{B*yNx_kzG=iI04&?am-o&V*u#7?L2Pn(S*bzyN%0~bttxu=i{ z5?Q>nT*OKhVjsy=!A=f@dYP1AmF;hj+ROj>0VTT#EIqgz{6XYBaaiF; znaq2f3oN}K=7uCABu*5kDr4ot6K-)akRlD_ii?7h{ID@t@^g1ll8XRp@~|-04R+n8 z#mI4_+GPPmma|1KPW94PkjTAftZLWTK4kSZVF@HYZj|v>tWgN#@nu6PCbykcZZj6N6P!OHuA zZdsDXdU;>>#TN1LOvMjxroc*W?IZOsu-|>(&xv6oRa)p7ctGa80!{>MGl%D^IZ*)bj$D|0E`ujBi-XnCoQ6P4EZ2k71eLyR@Ce=u=KIg zH!Ni!$DHqS1}swAzLUjISn}BrRw|YM$eJ`#true true + + + diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 01eddd2aad..63b51dcccb 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -34,6 +34,7 @@ namespace Avalonia.Skia private readonly SKPaint _strokePaint = new SKPaint(); private readonly SKPaint _fillPaint = new SKPaint(); private readonly SKPaint _boxShadowPaint = new SKPaint(); + private static SKShader s_acrylicNoiseShader; /// /// Context create info. @@ -652,21 +653,6 @@ namespace Avalonia.Skia } } - static SKColor SimpleColorBurn(SKColor bg, SKColor fg) - { - using (var bmp = new SKBitmap(1, 1)) - { - bmp.SetPixel(0, 0, bg); - using (var canvas = new SKCanvas(bmp)) - using (var paint = new SKPaint - { - Color = fg - }) - canvas.DrawRect(-1, -1, 3, 3, paint); - return bmp.GetPixel(0, 0); - } - } - static SKColorFilter CreateAlphaColorFilter(double opacity) { if (opacity > 1) @@ -711,19 +697,24 @@ namespace Avalonia.Skia acrylicBrush.BackgroundSource == AcrylicBackgroundSource.Digger ? acrylicBrush.TintOpacity : 1; - var noiseOpcity = 0.06 * brush.Opacity; + const double noiseOpcity = 0.06; - var tintColor = acrylicBrush.TintColor; - var excl = new SKColor(255, 255, 255, (byte)(255 * 0.1)); - var tint = new SKColor(tintColor.R, tintColor.G, tintColor.B, (byte)(255 * ((tintColor.A / 255.0) * acrylicBrush.Opacity))); + var tintColor = acrylicBrush.TintColor; + var tint = new SKColor(tintColor.R, tintColor.G, tintColor.B, (byte)(255 * ((tintColor.A / 255.0) * acrylicBrush.Opacity))); + + if(s_acrylicNoiseShader == null) + { + using(var stream = typeof(DrawingContextImpl).Assembly.GetManifestResourceStream("Avalonia.Skia.Assets.NoiseAsset_256X256_PNG.png")) + using (var bitmap = SKBitmap.Decode(stream)) + { + s_acrylicNoiseShader = SKShader.CreateBitmap(bitmap, SKShaderTileMode.Repeat, SKShaderTileMode.Repeat) + .WithColorFilter(CreateAlphaColorFilter(noiseOpcity)); + } - tint = SimpleColorBurn(excl, tint); + } using (var tintShader = SKShader.CreateColor(tint)) - using (var noiseShader = - SKShader.CreatePerlinNoiseTurbulence(15.876f, 15.876f, 2, 0.76829314f) - .WithColorFilter(CreateAlphaColorFilter(noiseOpcity))) - using (var compose = SKShader.CreateCompose(tintShader, noiseShader)) + using (var compose = SKShader.CreateCompose(tintShader, s_acrylicNoiseShader)) { paint.Shader = compose; From 8eb987c99065cf740dc51b6e71cc5eaf9a096648 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sun, 31 May 2020 14:46:29 -0300 Subject: [PATCH 068/693] tweak dark opacity. --- src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs b/src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs index cda4c1c5a2..7712b99b27 100644 --- a/src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs +++ b/src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs @@ -157,7 +157,7 @@ namespace Avalonia.Media const double whiteMaxOpacity = 0.40; // 100% luminosity const double midPointMaxOpacity = 0.50; // 50% luminosity - const double blackMaxOpacity = 0.84; // 0% luminosity + const double blackMaxOpacity = 0.60; // 0% luminosity var hsv = RgbToHsv(tintColor); From 3b317fc3080b183892a4b0c419e7c32d64fe6af5 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 28 May 2020 15:58:13 -0300 Subject: [PATCH 069/693] initial progressbar port. --- .../Accents/FluentBaseDark.xaml | 5 + .../Accents/FluentBaseLight.xaml | 6 + .../Accents/FluentControlResourcesDark.xaml | 9 ++ .../Accents/FluentControlResourcesLight.xaml | 8 ++ src/Avalonia.Themes.Fluent/ProgressBar.xaml | 108 ++++++++++++++---- 5 files changed, 116 insertions(+), 20 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml index 7a715bbde7..aed5ac1360 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml @@ -162,6 +162,11 @@ + + + + + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml index 5c6286a0bc..50d29f44ab 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml @@ -164,6 +164,12 @@ + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml index 7364c339f1..21181bf19d 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml @@ -169,6 +169,15 @@ + + 0.6 + 4 + 0 + + + + + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml index 15e157f573..8ce75862f1 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml @@ -168,6 +168,14 @@ + + 0.6 + 4 + 0 + + + + diff --git a/src/Avalonia.Themes.Fluent/ProgressBar.xaml b/src/Avalonia.Themes.Fluent/ProgressBar.xaml index d3c2f0c784..39a573303c 100644 --- a/src/Avalonia.Themes.Fluent/ProgressBar.xaml +++ b/src/Avalonia.Themes.Fluent/ProgressBar.xaml @@ -1,24 +1,92 @@ - - - + + + + + + + + + + + + - + + + + + + + + + + @@ -68,388 +55,44 @@ - - - - - - - - - - - - - - - - - - - - - - - - - + - + - + + + + + + + From 5a4a7c80c7861aaf68bd0a9932232ab40bdf0f78 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 9 Jun 2020 17:23:13 +0300 Subject: [PATCH 077/693] .axaml --- packages/Avalonia/Avalonia.csproj | 4 ++++ packages/Avalonia/AvaloniaBuildTasks.props | 11 ++++++++++- packages/Avalonia/AvaloniaBuildTasks.targets | 7 +++++-- packages/Avalonia/AvaloniaItemSchema.xaml | 13 +++++++++++++ .../GenerateAvaloniaResourcesTask.cs | 4 ++-- .../XamlCompilerTaskExecutor.cs | 3 ++- 6 files changed, 36 insertions(+), 6 deletions(-) create mode 100644 packages/Avalonia/AvaloniaItemSchema.xaml diff --git a/packages/Avalonia/Avalonia.csproj b/packages/Avalonia/Avalonia.csproj index 2c5a09bee7..cd3ce9adcd 100644 --- a/packages/Avalonia/Avalonia.csproj +++ b/packages/Avalonia/Avalonia.csproj @@ -41,6 +41,10 @@ true build\ + + true + build\ + diff --git a/packages/Avalonia/AvaloniaBuildTasks.props b/packages/Avalonia/AvaloniaBuildTasks.props index 30bafa37ee..8b452c7890 100644 --- a/packages/Avalonia/AvaloniaBuildTasks.props +++ b/packages/Avalonia/AvaloniaBuildTasks.props @@ -1,3 +1,12 @@ - + + + + + + + %(Filename) + Code + + diff --git a/packages/Avalonia/AvaloniaBuildTasks.targets b/packages/Avalonia/AvaloniaBuildTasks.targets index 537495fcad..6a4db38b28 100644 --- a/packages/Avalonia/AvaloniaBuildTasks.targets +++ b/packages/Avalonia/AvaloniaBuildTasks.targets @@ -31,9 +31,12 @@ + DependsOnTargets="$(BuildAvaloniaResourcesDependsOn)"> + + + + + + + + + + + + diff --git a/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs b/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs index 406abe6f99..ae2bf99d1e 100644 --- a/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs +++ b/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs @@ -107,7 +107,7 @@ namespace Avalonia.Build.Tasks foreach (var s in sources.ToList()) { - if (s.Path.ToLowerInvariant().EndsWith(".xaml") || s.Path.ToLowerInvariant().EndsWith(".paml")) + if (s.Path.ToLowerInvariant().EndsWith(".xaml") || s.Path.ToLowerInvariant().EndsWith(".paml") || s.Path.ToLowerInvariant().EndsWith(".axaml")) { XamlFileInfo info; try @@ -150,7 +150,7 @@ namespace Avalonia.Build.Tasks BuildEngine.LogMessage($"GenerateAvaloniaResourcesTask -> Root: {Root}, {Resources?.Count()} resources, Output:{Output}", _reportImportance < MessageImportance.Low ? MessageImportance.High : _reportImportance); - foreach (var r in EmbeddedResources.Where(r => r.ItemSpec.EndsWith(".xaml") || r.ItemSpec.EndsWith(".paml"))) + foreach (var r in EmbeddedResources.Where(r => r.ItemSpec.EndsWith(".xaml") || r.ItemSpec.EndsWith(".paml") || r.ItemSpec.EndsWith(".axaml"))) BuildEngine.LogWarning(BuildEngineErrorCode.LegacyResmScheme, r.ItemSpec, "XAML file is packed using legacy EmbeddedResource/resm scheme, relative URIs won't work"); var resources = BuildResourceSources(); diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs index 3b69109e68..30e8f120d7 100644 --- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs +++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs @@ -25,7 +25,8 @@ namespace Avalonia.Build.Tasks public static partial class XamlCompilerTaskExecutor { static bool CheckXamlName(IResource r) => r.Name.ToLowerInvariant().EndsWith(".xaml") - || r.Name.ToLowerInvariant().EndsWith(".paml"); + || r.Name.ToLowerInvariant().EndsWith(".paml") + || r.Name.ToLowerInvariant().EndsWith(".axaml"); public class CompileResult { From 927692763ebcf5abca2cae4607ed34edb2f12e57 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 9 Jun 2020 18:11:33 +0300 Subject: [PATCH 078/693] More msbuild metadata --- packages/Avalonia/AvaloniaBuildTasks.props | 1 + packages/Avalonia/AvaloniaBuildTasks.targets | 1 + packages/Avalonia/AvaloniaItemSchema.xaml | 5 +++++ 3 files changed, 7 insertions(+) diff --git a/packages/Avalonia/AvaloniaBuildTasks.props b/packages/Avalonia/AvaloniaBuildTasks.props index 8b452c7890..21cc77a1ff 100644 --- a/packages/Avalonia/AvaloniaBuildTasks.props +++ b/packages/Avalonia/AvaloniaBuildTasks.props @@ -1,6 +1,7 @@ + diff --git a/packages/Avalonia/AvaloniaBuildTasks.targets b/packages/Avalonia/AvaloniaBuildTasks.targets index 6a4db38b28..68b54c0916 100644 --- a/packages/Avalonia/AvaloniaBuildTasks.targets +++ b/packages/Avalonia/AvaloniaBuildTasks.targets @@ -82,5 +82,6 @@ + diff --git a/packages/Avalonia/AvaloniaItemSchema.xaml b/packages/Avalonia/AvaloniaItemSchema.xaml index b103e69c4f..a51ea3c0be 100644 --- a/packages/Avalonia/AvaloniaItemSchema.xaml +++ b/packages/Avalonia/AvaloniaItemSchema.xaml @@ -10,4 +10,9 @@ + From 5fd0dfe0b390d2afe2fe3e653880d2f8be60fb93 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 9 Jun 2020 18:28:49 +0300 Subject: [PATCH 079/693] More metadata --- packages/Avalonia/AvaloniaBuildTasks.props | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/Avalonia/AvaloniaBuildTasks.props b/packages/Avalonia/AvaloniaBuildTasks.props index 21cc77a1ff..8f98f5424a 100644 --- a/packages/Avalonia/AvaloniaBuildTasks.props +++ b/packages/Avalonia/AvaloniaBuildTasks.props @@ -9,5 +9,9 @@ %(Filename) Code + + %(Filename) + Code + From 175eff6429f4fb51b2b512fd567f43b1a5cb6535 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 9 Jun 2020 17:00:37 -0300 Subject: [PATCH 080/693] Fix acrylic. --- samples/ControlCatalog/Pages/AcrylicPage.xaml | 10 ++-------- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 4 ++-- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/samples/ControlCatalog/Pages/AcrylicPage.xaml b/samples/ControlCatalog/Pages/AcrylicPage.xaml index 6155d6d68d..6dc23c4841 100644 --- a/samples/ControlCatalog/Pages/AcrylicPage.xaml +++ b/samples/ControlCatalog/Pages/AcrylicPage.xaml @@ -3,13 +3,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="ControlCatalog.Pages.AcrylicPage"> - - - + x:Class="ControlCatalog.Pages.AcrylicPage"> @@ -86,7 +80,7 @@ diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 63b51dcccb..ad2a896508 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -661,7 +661,7 @@ namespace Avalonia.Skia var a = new byte[256]; for (var i = 0; i < 256; i++) { - c[i] = 255; + c[i] = (byte)i; a[i] = (byte)(i * opacity); } @@ -697,7 +697,7 @@ namespace Avalonia.Skia acrylicBrush.BackgroundSource == AcrylicBackgroundSource.Digger ? acrylicBrush.TintOpacity : 1; - const double noiseOpcity = 0.06; + const double noiseOpcity = 0.02; var tintColor = acrylicBrush.TintColor; var tint = new SKColor(tintColor.R, tintColor.G, tintColor.B, (byte)(255 * ((tintColor.A / 255.0) * acrylicBrush.Opacity))); From 4dc8a2a3ee18b92d1551ae1935f30886a9d4b6cf Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 9 Jun 2020 18:28:50 -0300 Subject: [PATCH 081/693] tweaks and fixes to acrylic. --- samples/ControlCatalog/Pages/AcrylicPage.xaml | 6 +++--- src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs | 4 ++-- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/samples/ControlCatalog/Pages/AcrylicPage.xaml b/samples/ControlCatalog/Pages/AcrylicPage.xaml index 6dc23c4841..88bbdb0346 100644 --- a/samples/ControlCatalog/Pages/AcrylicPage.xaml +++ b/samples/ControlCatalog/Pages/AcrylicPage.xaml @@ -71,7 +71,7 @@ @@ -89,8 +89,8 @@ diff --git a/src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs b/src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs index 7712b99b27..acc660175f 100644 --- a/src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs +++ b/src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs @@ -135,7 +135,7 @@ namespace Avalonia.Media if (false) // non-acrylic blue // TODO detect blur level. { - tintColor = new Color((byte)(Math.Round(tintColor.A * (((tintOpacity * tintOpacityModifier) * 0.25) + 0.75))), tintColor.R, tintColor.G, tintColor.B); + tintColor = new Color((byte)(Math.Round(tintColor.A * (((tintOpacity * tintOpacityModifier) * 0.15) + 0.85))), tintColor.R, tintColor.G, tintColor.B); } else { @@ -157,7 +157,7 @@ namespace Avalonia.Media const double whiteMaxOpacity = 0.40; // 100% luminosity const double midPointMaxOpacity = 0.50; // 50% luminosity - const double blackMaxOpacity = 0.60; // 0% luminosity + const double blackMaxOpacity = 0.80; // 0% luminosity var hsv = RgbToHsv(tintColor); diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index ad2a896508..d2c7bdaedc 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -697,10 +697,10 @@ namespace Avalonia.Skia acrylicBrush.BackgroundSource == AcrylicBackgroundSource.Digger ? acrylicBrush.TintOpacity : 1; - const double noiseOpcity = 0.02; + const double noiseOpcity = 0.0225; var tintColor = acrylicBrush.TintColor; - var tint = new SKColor(tintColor.R, tintColor.G, tintColor.B, (byte)(255 * ((tintColor.A / 255.0) * acrylicBrush.Opacity))); + var tint = new SKColor(tintColor.R, tintColor.G, tintColor.B, (byte)(255 * ((tintColor.A / 255.0) * acrylicBrush.Opacity * tintOpacity))); if(s_acrylicNoiseShader == null) { From 5ba822f94f95054ebcd655cf0c193151b44480a3 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 9 Jun 2020 20:24:48 -0300 Subject: [PATCH 082/693] adjust opacity for normal blur... --- .../Media/ExperimentalAcrylicBrush.cs | 16 ++++++---- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 30 +++++++++++++++++-- 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs b/src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs index acc660175f..e52774ea18 100644 --- a/src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs +++ b/src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs @@ -139,12 +139,18 @@ namespace Avalonia.Media } else { - tintColor = new Color((byte)(Math.Round(tintColor.A * tintOpacity * tintOpacityModifier)), tintColor.R, tintColor.G, tintColor.B); + tintColor = new Color((byte)(255 * ((255.0 / tintColor.A) * tintOpacity) * tintOpacityModifier), tintColor.R, tintColor.G, tintColor.B); } return tintColor; } + private static double AdjustOpacity(double opacity) + { + var result = Math.Max((1.0 - Math.Pow((1.0 - opacity), 3.85)), 0.92); + return result; + } + private static double GetTintOpacityModifier(Color tintColor) { // This method supresses the maximum allowable tint opacity depending on the luminosity and saturation of a color by @@ -155,9 +161,9 @@ namespace Avalonia.Media const double midPoint = 0.5; // Mid point of HsvV range that these calculations are based on. This is here for easy tuning. - const double whiteMaxOpacity = 0.40; // 100% luminosity - const double midPointMaxOpacity = 0.50; // 50% luminosity - const double blackMaxOpacity = 0.80; // 0% luminosity + double whiteMaxOpacity = AdjustOpacity(0.45); // 100% luminosity + double midPointMaxOpacity = AdjustOpacity(0.40); // 50% luminosity + double blackMaxOpacity = AdjustOpacity(0.60); // 0% luminosity var hsv = RgbToHsv(tintColor); @@ -189,7 +195,7 @@ namespace Avalonia.Media if (hsv.Saturation > 0) { // Dampen opacity suppression based on how much saturation there is - maxOpacitySuppression *= Math.Max(1 - (hsv.Saturation * 2), 0.0); + //maxOpacitySuppression *= Math.Max(1 - (hsv.Saturation * 2), 0.0); } double opacitySuppression = maxOpacitySuppression * normalizedDeviation; diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index d2c7bdaedc..7fcc13ebd3 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -668,6 +668,27 @@ namespace Avalonia.Skia return SKColorFilter.CreateTable(a, c, c, c); } + static byte Blend(byte leftColor, byte leftAlpha, byte rightColor, byte rightAlpha) + { + var ca = leftColor / 255d; + var aa = leftAlpha / 255d; + var cb = rightColor / 255d; + var ab = rightAlpha / 255d; + var r = (ca * aa + cb * ab * (1 - aa)) / (aa + ab * (1 - aa)); + return (byte)(r * 255); + } + static SKColor Blend(SKColor left, SKColor right) + { + var aa = left.Alpha / 255d; + var ab = right.Alpha / 255d; + return new SKColor( + Blend(left.Red, left.Alpha, right.Red, right.Alpha), + Blend(left.Green, left.Alpha, right.Green, right.Alpha), + Blend(left.Blue, left.Alpha, right.Blue, right.Alpha), + (byte)((aa + ab * (1 - aa)) * 255) + ); + } + /// /// Creates paint wrapper for given brush. /// @@ -700,9 +721,14 @@ namespace Avalonia.Skia const double noiseOpcity = 0.0225; var tintColor = acrylicBrush.TintColor; - var tint = new SKColor(tintColor.R, tintColor.G, tintColor.B, (byte)(255 * ((tintColor.A / 255.0) * acrylicBrush.Opacity * tintOpacity))); + var tint = new SKColor(tintColor.R, tintColor.G, tintColor.B, tintColor.A); + var tracingPaper = new SKColor(0xaf, 0x7f, 0x7f, 0x7f); + + //tint = Blend(tracingPaper, tint); + - if(s_acrylicNoiseShader == null) + + if (s_acrylicNoiseShader == null) { using(var stream = typeof(DrawingContextImpl).Assembly.GetManifestResourceStream("Avalonia.Skia.Assets.NoiseAsset_256X256_PNG.png")) using (var bitmap = SKBitmap.Decode(stream)) From 98e7b140232f81330405444ace1d018e08f42229 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 10 Jun 2020 16:35:32 -0300 Subject: [PATCH 083/693] use proper corrections. and blend with mid range gray to allow for readability. --- samples/ControlCatalog/Pages/AcrylicPage.xaml | 22 ++- .../Media/ExperimentalAcrylicBrush.cs | 131 +++++++++++++++++- .../Media/IExperimentalAcrylicBrush.cs | 4 +- .../ImmutableExperimentalAcrylicBrush.cs | 3 + src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 10 +- 5 files changed, 157 insertions(+), 13 deletions(-) diff --git a/samples/ControlCatalog/Pages/AcrylicPage.xaml b/samples/ControlCatalog/Pages/AcrylicPage.xaml index 88bbdb0346..29f5fb8409 100644 --- a/samples/ControlCatalog/Pages/AcrylicPage.xaml +++ b/samples/ControlCatalog/Pages/AcrylicPage.xaml @@ -3,10 +3,21 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="ControlCatalog.Pages.AcrylicPage"> + x:Class="ControlCatalog.Pages.AcrylicPage"> + + + + + + + @@ -24,6 +35,7 @@ @@ -33,6 +45,7 @@ @@ -44,6 +57,7 @@ @@ -53,6 +67,7 @@ @@ -62,6 +77,7 @@ @@ -73,6 +89,7 @@ @@ -82,6 +99,7 @@ @@ -90,7 +108,7 @@ diff --git a/src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs b/src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs index e52774ea18..49b0e04cf3 100644 --- a/src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs +++ b/src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs @@ -5,6 +5,7 @@ namespace Avalonia.Media public class ExperimentalAcrylicBrush : Brush, IExperimentalAcrylicBrush { private Color _effectiveTintColor; + private Color _effectiveLuminosityColor; static ExperimentalAcrylicBrush() { @@ -17,11 +18,19 @@ namespace Avalonia.Media TintColorProperty.Changed.AddClassHandler((b, e) => { b._effectiveTintColor = GetEffectiveTintColor(b.TintColor, b.TintOpacity); + b._effectiveLuminosityColor = b.GetEffectiveLuminosityColor(); }); TintOpacityProperty.Changed.AddClassHandler((b, e) => { b._effectiveTintColor = GetEffectiveTintColor(b.TintColor, b.TintOpacity); + b._effectiveLuminosityColor = b.GetEffectiveLuminosityColor(); + }); + + TintLuminosityOpacityProperty.Changed.AddClassHandler((b, e) => + { + b._effectiveTintColor = GetEffectiveTintColor(b.TintColor, b.TintOpacity); + b._effectiveLuminosityColor = b.GetEffectiveLuminosityColor(); }); } @@ -37,8 +46,8 @@ namespace Avalonia.Media public static readonly StyledProperty TintOpacityProperty = AvaloniaProperty.Register(nameof(TintOpacity)); - public static readonly StyledProperty TintLuminosityOpacityProperty = - AvaloniaProperty.Register(nameof(TintLuminosityOpacity)); + public static readonly StyledProperty TintLuminosityOpacityProperty = + AvaloniaProperty.Register(nameof(TintLuminosityOpacity)); public static readonly StyledProperty FallbackColorProperty = AvaloniaProperty.Register(nameof(FallbackColor)); @@ -57,6 +66,8 @@ namespace Avalonia.Media Color IExperimentalAcrylicBrush.TintColor => _effectiveTintColor; + Color IExperimentalAcrylicBrush.LuminosityColor => _effectiveLuminosityColor; + public double TintOpacity { get => GetValue(TintOpacityProperty); @@ -161,9 +172,9 @@ namespace Avalonia.Media const double midPoint = 0.5; // Mid point of HsvV range that these calculations are based on. This is here for easy tuning. - double whiteMaxOpacity = AdjustOpacity(0.45); // 100% luminosity - double midPointMaxOpacity = AdjustOpacity(0.40); // 50% luminosity - double blackMaxOpacity = AdjustOpacity(0.60); // 0% luminosity + double whiteMaxOpacity = 0.45; // 100% luminosity + double midPointMaxOpacity = 0.90; // 50% luminosity + double blackMaxOpacity = 0.85; // 0% luminosity var hsv = RgbToHsv(tintColor); @@ -205,5 +216,115 @@ namespace Avalonia.Media return opacityModifier; } + + Color GetEffectiveLuminosityColor() + { + double tintOpacity = TintOpacity; + + // Purposely leaving out tint opacity modifier here because GetLuminosityColor needs the *original* tint opacity set by the user. + var tintColor = new Color((byte)(Math.Round(TintColor.A * tintOpacity)), TintColor.R, TintColor.G, TintColor.B); + + double? luminosityOpacity = TintLuminosityOpacity; + + return GetLuminosityColor(tintColor, luminosityOpacity); + } + + public static Color FromHsv(HsvColor color) + { + float r = 0; + float g = 0; + float b = 0; + + var i = (float)Math.Floor(color.Hue * 6f); + var f = color.Hue * 6f - i; + var p = color.Value * (1f - color.Saturation); + var q = color.Value * (1f - f * color.Saturation); + var t = color.Value * (1f - (1f - f) * color.Saturation); + + switch (i % 6) + { + case 0: + r = color.Value; + g = t; + b = p; + break; + case 1: + r = q; + g = color.Value; + b = p; + break; + case 2: + r = p; + g = color.Value; + b = t; + break; + case 3: + r = p; + g = q; + b = color.Value; + break; + case 4: + r = t; + g = p; + b = color.Value; + break; + case 5: + r = color.Value; + g = p; + b = q; + break; + } + + return new Color(Trim(r), Trim(g), Trim(b), 255); + } + + private static byte Trim(double value) + { + value = Math.Min(Math.Floor(value * 256), 255); + + if (value < 0) + { + return 0; + } + else if (value > 255) + { + return 255; + } + + return (byte)value; + } + + double Luminosity (Color color) + { + return 0.299 * color.R + 0.587 * color.G + 0.114 * color.B; + } + + // The tintColor passed into this method should be the original, unmodified color created using user values for TintColor + TintOpacity + Color GetLuminosityColor(Color tintColor, double? luminosityOpacity) + { + var luminosityColor = new Color(255, 127, 127, 127); + + var modifier = GetTintOpacityModifier(luminosityColor); + + // If luminosity opacity is specified, just use the values as is + if (luminosityOpacity.HasValue) + { + return new Color((byte)(255 * Math.Max(Math.Min(luminosityOpacity.Value * modifier, 1.0), 0.0)), luminosityColor.R, luminosityColor.G, luminosityColor.B); + } + else + { + // Now figure out luminosity opacity + // Map original *tint* opacity to this range + const double minLuminosityOpacity = 0.15; + const double maxLuminosityOpacity = 1.03; + + double luminosityOpacityRangeMax = maxLuminosityOpacity - minLuminosityOpacity; + double mappedTintOpacity = ((tintColor.A / 255.0) * luminosityOpacityRangeMax) + minLuminosityOpacity; + + // Finally, combine the luminosity opacity and the HsvV-clamped tint color + return new Color(Trim(Math.Min(mappedTintOpacity * modifier, 1.0)), luminosityColor.R, luminosityColor.G, luminosityColor.B); + } + + } } } diff --git a/src/Avalonia.Visuals/Media/IExperimentalAcrylicBrush.cs b/src/Avalonia.Visuals/Media/IExperimentalAcrylicBrush.cs index fdcf6148fe..30cbeb331c 100644 --- a/src/Avalonia.Visuals/Media/IExperimentalAcrylicBrush.cs +++ b/src/Avalonia.Visuals/Media/IExperimentalAcrylicBrush.cs @@ -6,7 +6,9 @@ namespace Avalonia.Media { AcrylicBackgroundSource BackgroundSource { get; } - Color TintColor { get; } + Color TintColor { get; } + + Color LuminosityColor { get; } double TintOpacity { get; } diff --git a/src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicBrush.cs b/src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicBrush.cs index ed3f532035..e05f851a73 100644 --- a/src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicBrush.cs +++ b/src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicBrush.cs @@ -11,12 +11,15 @@ namespace Avalonia.Media TintOpacity = brush.TintOpacity; FallbackColor = brush.FallbackColor; Opacity = brush.Opacity; + LuminosityColor = brush.LuminosityColor; } public AcrylicBackgroundSource BackgroundSource { get; } public Color TintColor { get; } + public Color LuminosityColor { get; } + public double TintOpacity { get; } public Color FallbackColor { get; } diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 7fcc13ebd3..aefbbab7cc 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -726,8 +726,6 @@ namespace Avalonia.Skia //tint = Blend(tracingPaper, tint); - - if (s_acrylicNoiseShader == null) { using(var stream = typeof(DrawingContextImpl).Assembly.GetManifestResourceStream("Avalonia.Skia.Assets.NoiseAsset_256X256_PNG.png")) @@ -736,11 +734,13 @@ namespace Avalonia.Skia s_acrylicNoiseShader = SKShader.CreateBitmap(bitmap, SKShaderTileMode.Repeat, SKShaderTileMode.Repeat) .WithColorFilter(CreateAlphaColorFilter(noiseOpcity)); } - } - + + using (var backdrop = SKShader.CreateColor(new SKColor(acrylicBrush.LuminosityColor.R, acrylicBrush.LuminosityColor.G, acrylicBrush.LuminosityColor.B, (byte)Math.Round((acrylicBrush.LuminosityColor.A * 0.75))))) + using (var backdropResult = SKShader.CreateCompose(backdrop, backdrop, SKBlendMode.Luminosity)) using (var tintShader = SKShader.CreateColor(tint)) - using (var compose = SKShader.CreateCompose(tintShader, s_acrylicNoiseShader)) + using (var effectiveTint = SKShader.CreateCompose (backdropResult, tintShader)) + using (var compose = SKShader.CreateCompose(effectiveTint, s_acrylicNoiseShader)) { paint.Shader = compose; From e957f75fdb17d588b0a94fa5fa414275489c4afd Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 10 Jun 2020 18:37:57 -0300 Subject: [PATCH 084/693] remove unused code. --- samples/ControlCatalog/Pages/AcrylicPage.xaml | 27 ++++++------ .../Media/ExperimentalAcrylicBrush.cs | 42 ++++++------------- .../ImmutableExperimentalAcrylicBrush.cs | 4 +- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 29 ++++++------- 4 files changed, 42 insertions(+), 60 deletions(-) diff --git a/samples/ControlCatalog/Pages/AcrylicPage.xaml b/samples/ControlCatalog/Pages/AcrylicPage.xaml index 29f5fb8409..c0b3d164bf 100644 --- a/samples/ControlCatalog/Pages/AcrylicPage.xaml +++ b/samples/ControlCatalog/Pages/AcrylicPage.xaml @@ -7,17 +7,7 @@ - - - - - - - + @@ -107,12 +97,23 @@ + + + + + + diff --git a/src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs b/src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs index 49b0e04cf3..194c32a090 100644 --- a/src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs +++ b/src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs @@ -33,7 +33,7 @@ namespace Avalonia.Media b._effectiveLuminosityColor = b.GetEffectiveLuminosityColor(); }); } - + /// /// Defines the property. /// @@ -46,8 +46,8 @@ namespace Avalonia.Media public static readonly StyledProperty TintOpacityProperty = AvaloniaProperty.Register(nameof(TintOpacity)); - public static readonly StyledProperty TintLuminosityOpacityProperty = - AvaloniaProperty.Register(nameof(TintLuminosityOpacity)); + public static readonly StyledProperty TintLuminosityOpacityProperty = + AvaloniaProperty.Register(nameof(TintLuminosityOpacity), 0.9); public static readonly StyledProperty FallbackColorProperty = AvaloniaProperty.Register(nameof(FallbackColor)); @@ -80,7 +80,7 @@ namespace Avalonia.Media set => SetValue(FallbackColorProperty, value); } - public double? TintLuminosityOpacity + public double TintLuminosityOpacity { get => GetValue(TintLuminosityOpacityProperty); set => SetValue(TintLuminosityOpacityProperty, value); @@ -100,7 +100,7 @@ namespace Avalonia.Media public static HsvColor RgbToHsv(Color color) { - var r = color.R /255.0f; + var r = color.R / 255.0f; var g = color.G / 255.0f; var b = color.B / 255.0f; var max = Math.Max(r, Math.Max(g, b)); @@ -172,10 +172,10 @@ namespace Avalonia.Media const double midPoint = 0.5; // Mid point of HsvV range that these calculations are based on. This is here for easy tuning. - double whiteMaxOpacity = 0.45; // 100% luminosity + double whiteMaxOpacity = 0.65; // 100% luminosity double midPointMaxOpacity = 0.90; // 50% luminosity double blackMaxOpacity = 0.85; // 0% luminosity - + var hsv = RgbToHsv(tintColor); double opacityModifier = midPointMaxOpacity; @@ -218,7 +218,7 @@ namespace Avalonia.Media } Color GetEffectiveLuminosityColor() - { + { double tintOpacity = TintOpacity; // Purposely leaving out tint opacity modifier here because GetLuminosityColor needs the *original* tint opacity set by the user. @@ -226,7 +226,7 @@ namespace Avalonia.Media double? luminosityOpacity = TintLuminosityOpacity; - return GetLuminosityColor(tintColor, luminosityOpacity); + return GetLuminosityColor(luminosityOpacity); } public static Color FromHsv(HsvColor color) @@ -294,37 +294,19 @@ namespace Avalonia.Media return (byte)value; } - double Luminosity (Color color) + double Luminosity(Color color) { return 0.299 * color.R + 0.587 * color.G + 0.114 * color.B; } // The tintColor passed into this method should be the original, unmodified color created using user values for TintColor + TintOpacity - Color GetLuminosityColor(Color tintColor, double? luminosityOpacity) + Color GetLuminosityColor(double? luminosityOpacity) { var luminosityColor = new Color(255, 127, 127, 127); var modifier = GetTintOpacityModifier(luminosityColor); - // If luminosity opacity is specified, just use the values as is - if (luminosityOpacity.HasValue) - { - return new Color((byte)(255 * Math.Max(Math.Min(luminosityOpacity.Value * modifier, 1.0), 0.0)), luminosityColor.R, luminosityColor.G, luminosityColor.B); - } - else - { - // Now figure out luminosity opacity - // Map original *tint* opacity to this range - const double minLuminosityOpacity = 0.15; - const double maxLuminosityOpacity = 1.03; - - double luminosityOpacityRangeMax = maxLuminosityOpacity - minLuminosityOpacity; - double mappedTintOpacity = ((tintColor.A / 255.0) * luminosityOpacityRangeMax) + minLuminosityOpacity; - - // Finally, combine the luminosity opacity and the HsvV-clamped tint color - return new Color(Trim(Math.Min(mappedTintOpacity * modifier, 1.0)), luminosityColor.R, luminosityColor.G, luminosityColor.B); - } - + return new Color((byte)(255 * Math.Max(Math.Min(luminosityOpacity.Value * modifier, 1.0), 0.0)), luminosityColor.R, luminosityColor.G, luminosityColor.B); } } } diff --git a/src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicBrush.cs b/src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicBrush.cs index e05f851a73..8cb61dbaf4 100644 --- a/src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicBrush.cs +++ b/src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicBrush.cs @@ -34,7 +34,8 @@ namespace Avalonia.Media Opacity == other.Opacity && TintOpacity == other.TintOpacity && BackgroundSource == other.BackgroundSource && - FallbackColor == other.FallbackColor; + FallbackColor == other.FallbackColor && LuminosityColor == other.LuminosityColor; + } public override bool Equals(object obj) @@ -58,6 +59,7 @@ namespace Avalonia.Media hash = (hash * 23) + TintOpacity.GetHashCode(); hash = (hash * 23) + BackgroundSource.GetHashCode(); hash = (hash * 23) + FallbackColor.GetHashCode(); + hash = (hash * 23) + LuminosityColor.GetHashCode(); return hash; } diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index aefbbab7cc..1ddc0980b5 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -677,17 +677,18 @@ namespace Avalonia.Skia var r = (ca * aa + cb * ab * (1 - aa)) / (aa + ab * (1 - aa)); return (byte)(r * 255); } - static SKColor Blend(SKColor left, SKColor right) - { - var aa = left.Alpha / 255d; - var ab = right.Alpha / 255d; - return new SKColor( - Blend(left.Red, left.Alpha, right.Red, right.Alpha), - Blend(left.Green, left.Alpha, right.Green, right.Alpha), - Blend(left.Blue, left.Alpha, right.Blue, right.Alpha), - (byte)((aa + ab * (1 - aa)) * 255) + + static Color Blend(Color left, Color right) + { + var aa = left.A / 255d; + var ab = right.A / 255d; + return new Color( + (byte)((aa + ab * (1 - aa)) * 255), + Blend(left.R, left.A, right.R, right.A), + Blend(left.G, left.A, right.G, right.A), + Blend(left.B, left.A, right.B, right.A) ); - } + } /// /// Creates paint wrapper for given brush. @@ -722,9 +723,6 @@ namespace Avalonia.Skia var tintColor = acrylicBrush.TintColor; var tint = new SKColor(tintColor.R, tintColor.G, tintColor.B, tintColor.A); - var tracingPaper = new SKColor(0xaf, 0x7f, 0x7f, 0x7f); - - //tint = Blend(tracingPaper, tint); if (s_acrylicNoiseShader == null) { @@ -736,10 +734,9 @@ namespace Avalonia.Skia } } - using (var backdrop = SKShader.CreateColor(new SKColor(acrylicBrush.LuminosityColor.R, acrylicBrush.LuminosityColor.G, acrylicBrush.LuminosityColor.B, (byte)Math.Round((acrylicBrush.LuminosityColor.A * 0.75))))) - using (var backdropResult = SKShader.CreateCompose(backdrop, backdrop, SKBlendMode.Luminosity)) + using (var backdrop = SKShader.CreateColor(new SKColor(acrylicBrush.LuminosityColor.R, acrylicBrush.LuminosityColor.G, acrylicBrush.LuminosityColor.B, acrylicBrush.LuminosityColor.A))) using (var tintShader = SKShader.CreateColor(tint)) - using (var effectiveTint = SKShader.CreateCompose (backdropResult, tintShader)) + using (var effectiveTint = SKShader.CreateCompose (backdrop, tintShader)) using (var compose = SKShader.CreateCompose(effectiveTint, s_acrylicNoiseShader)) { paint.Shader = compose; From ecdd8b9c7bd02c683af82384a146db481ba052d7 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 10 Jun 2020 18:50:56 -0300 Subject: [PATCH 085/693] sensible defaults. tidy acrylic demo. --- samples/ControlCatalog/Pages/AcrylicPage.xaml | 20 ++++++++++++++++--- .../Media/ExperimentalAcrylicBrush.cs | 4 ++-- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/samples/ControlCatalog/Pages/AcrylicPage.xaml b/samples/ControlCatalog/Pages/AcrylicPage.xaml index c0b3d164bf..f2785dfcfd 100644 --- a/samples/ControlCatalog/Pages/AcrylicPage.xaml +++ b/samples/ControlCatalog/Pages/AcrylicPage.xaml @@ -6,8 +6,22 @@ x:Class="ControlCatalog.Pages.AcrylicPage"> - - + + + + + + + + + + + + + + @@ -112,7 +126,7 @@ TintOpacity="{Binding #TintOpacitySlider.Value}" TintLuminosityOpacity="{Binding #TintLuminositySlider.Value}" BackgroundSource="Digger" /> - + diff --git a/src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs b/src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs index 194c32a090..475afb8567 100644 --- a/src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs +++ b/src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs @@ -44,10 +44,10 @@ namespace Avalonia.Media AvaloniaProperty.Register(nameof(BackgroundSource)); public static readonly StyledProperty TintOpacityProperty = - AvaloniaProperty.Register(nameof(TintOpacity)); + AvaloniaProperty.Register(nameof(TintOpacity), 0.9); public static readonly StyledProperty TintLuminosityOpacityProperty = - AvaloniaProperty.Register(nameof(TintLuminosityOpacity), 0.9); + AvaloniaProperty.Register(nameof(TintLuminosityOpacity), 0.8); public static readonly StyledProperty FallbackColorProperty = AvaloniaProperty.Register(nameof(FallbackColor)); From 9eaaceff60495c3ccf8f844b28490e3b301b448c Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 12 Jun 2020 16:11:42 -0300 Subject: [PATCH 086/693] Implement Acrylic as a "Material" and a special Decorator to display Acrylic. --- .../ExperimentalAcrylicBorder.cs | 81 ++++++++ ...rush.cs => ExperimentalAcrylicMaterial.cs} | 183 ++++++++++++++++-- .../Media/IExperimentalAcrylicBrush.cs | 17 -- .../ImmutableExperimentalAcrylicBrush.cs | 78 -------- .../IDrawingContextWithAcrylicLikeSupport.cs | 9 + .../SceneGraph/DeferredDrawingContextImpl.cs | 19 +- .../SceneGraph/ExperimentalAcrylicNode.cs | 93 +++++++++ .../Rendering/SceneGraph/RectangleNode.cs | 2 +- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 120 ++++++++---- 9 files changed, 445 insertions(+), 157 deletions(-) create mode 100644 src/Avalonia.Controls/ExperimentalAcrylicBorder.cs rename src/Avalonia.Visuals/Media/{ExperimentalAcrylicBrush.cs => ExperimentalAcrylicMaterial.cs} (62%) delete mode 100644 src/Avalonia.Visuals/Media/IExperimentalAcrylicBrush.cs delete mode 100644 src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicBrush.cs create mode 100644 src/Avalonia.Visuals/Platform/IDrawingContextWithAcrylicLikeSupport.cs create mode 100644 src/Avalonia.Visuals/Rendering/SceneGraph/ExperimentalAcrylicNode.cs diff --git a/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs b/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs new file mode 100644 index 0000000000..4a855b9838 --- /dev/null +++ b/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs @@ -0,0 +1,81 @@ +using Avalonia.Controls.Utils; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.Platform; + +namespace Avalonia.Controls +{ + public class ExperimentalAcrylicBorder : Decorator + { + public static readonly StyledProperty CornerRadiusProperty = + Border.CornerRadiusProperty.AddOwner(); + + public static readonly StyledProperty MaterialProperty = + AvaloniaProperty.Register(nameof(Material)); + + private readonly BorderRenderHelper _borderRenderHelper = new BorderRenderHelper(); + + static ExperimentalAcrylicBorder() + { + AffectsRender( + MaterialProperty, + CornerRadiusProperty); + } + + + /// + /// Gets or sets the radius of the border rounded corners. + /// + public CornerRadius CornerRadius + { + get { return GetValue(CornerRadiusProperty); } + set { SetValue(CornerRadiusProperty, value); } + } + + public ExperimentalAcrylicMaterial Material + { + get => GetValue(MaterialProperty); + set => SetValue(MaterialProperty, value); + } + + + public override void Render(DrawingContext context) + { + if(context.PlatformImpl is IDrawingContextWithAcrylicLikeSupport idc) + { + var cornerRadius = CornerRadius; + + idc.DrawRectangle( + Material, + new RoundedRect( + new Rect(Bounds.Size), + cornerRadius.TopLeft, cornerRadius.TopRight, + cornerRadius.BottomRight, cornerRadius.BottomLeft)); + } + else + { + _borderRenderHelper.Render(context, Bounds.Size, new Thickness(), CornerRadius, new SolidColorBrush(Material.FallbackColor), null, default); + } + } + + /// + /// Measures the control. + /// + /// The available size. + /// The desired size of the control. + protected override Size MeasureOverride(Size availableSize) + { + return LayoutHelper.MeasureChild(Child, availableSize, Padding); + } + + /// + /// Arranges the control's child. + /// + /// The size allocated to the control. + /// The space taken. + protected override Size ArrangeOverride(Size finalSize) + { + return LayoutHelper.ArrangeChild(Child, finalSize, Padding); + } + } +} diff --git a/src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs b/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs similarity index 62% rename from src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs rename to src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs index 475afb8567..507931da5a 100644 --- a/src/Avalonia.Visuals/Media/ExperimentalAcrylicBrush.cs +++ b/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs @@ -2,32 +2,144 @@ namespace Avalonia.Media { - public class ExperimentalAcrylicBrush : Brush, IExperimentalAcrylicBrush + /// + /// Represents a mutable brush which can return an immutable clone of itself. + /// + public interface IMutableExperimentalAcrylicMaterial : IExperimentalAcrylicMaterial, IAffectsRender + { + /// + /// Creates an immutable clone of the brush. + /// + /// The immutable clone. + IExperimentalAcrylicMaterial ToImmutable(); + } + + public interface IExperimentalAcrylicMaterial + { + AcrylicBackgroundSource BackgroundSource { get; } + + Color TintColor { get; } + + Color LuminosityColor { get; } + + double TintOpacity { get; } + + Color FallbackColor { get; } + } + + public static class MaterialExtensions + { + /// + /// Converts a brush to an immutable brush. + /// + /// The brush. + /// + /// The result of calling if the brush is mutable, + /// otherwise . + /// + public static IExperimentalAcrylicMaterial ToImmutable(this IExperimentalAcrylicMaterial material) + { + Contract.Requires(material != null); + + return (material as IMutableExperimentalAcrylicMaterial)?.ToImmutable() ?? material; + } + } + + public readonly struct ImmutableExperimentalAcrylicMaterial : IExperimentalAcrylicMaterial, IEquatable + { + public ImmutableExperimentalAcrylicMaterial(IExperimentalAcrylicMaterial brush) + { + BackgroundSource = brush.BackgroundSource; + TintColor = brush.TintColor; + TintOpacity = brush.TintOpacity; + FallbackColor = brush.FallbackColor; + LuminosityColor = brush.LuminosityColor; + } + + public AcrylicBackgroundSource BackgroundSource { get; } + + public Color TintColor { get; } + + public Color LuminosityColor { get; } + + public double TintOpacity { get; } + + public Color FallbackColor { get; } + + public bool Equals(ImmutableExperimentalAcrylicMaterial other) + { + // ReSharper disable once CompareOfFloatsByEqualityOperator + return + TintColor == other.TintColor && + TintOpacity == other.TintOpacity && + BackgroundSource == other.BackgroundSource && + FallbackColor == other.FallbackColor && LuminosityColor == other.LuminosityColor; + + } + + public override bool Equals(object obj) + { + return obj is ImmutableExperimentalAcrylicMaterial other && Equals(other); + } + + public Color GetEffectiveTintColor() + { + return TintColor; + } + + public override int GetHashCode() + { + unchecked + { + int hash = 17; + + hash = (hash * 23) + TintColor.GetHashCode(); + hash = (hash * 23) + TintOpacity.GetHashCode(); + hash = (hash * 23) + BackgroundSource.GetHashCode(); + hash = (hash * 23) + FallbackColor.GetHashCode(); + hash = (hash * 23) + LuminosityColor.GetHashCode(); + + return hash; + } + } + + public static bool operator ==(ImmutableExperimentalAcrylicMaterial left, ImmutableExperimentalAcrylicMaterial right) + { + return left.Equals(right); + } + + public static bool operator !=(ImmutableExperimentalAcrylicMaterial left, ImmutableExperimentalAcrylicMaterial right) + { + return !left.Equals(right); + } + } + + public class ExperimentalAcrylicMaterial : AvaloniaObject, IMutableExperimentalAcrylicMaterial { private Color _effectiveTintColor; private Color _effectiveLuminosityColor; - static ExperimentalAcrylicBrush() + static ExperimentalAcrylicMaterial() { - AffectsRender( + AffectsRender( TintColorProperty, BackgroundSourceProperty, TintOpacityProperty, TintLuminosityOpacityProperty); - TintColorProperty.Changed.AddClassHandler((b, e) => + TintColorProperty.Changed.AddClassHandler((b, e) => { b._effectiveTintColor = GetEffectiveTintColor(b.TintColor, b.TintOpacity); b._effectiveLuminosityColor = b.GetEffectiveLuminosityColor(); }); - TintOpacityProperty.Changed.AddClassHandler((b, e) => + TintOpacityProperty.Changed.AddClassHandler((b, e) => { b._effectiveTintColor = GetEffectiveTintColor(b.TintColor, b.TintOpacity); b._effectiveLuminosityColor = b.GetEffectiveLuminosityColor(); }); - TintLuminosityOpacityProperty.Changed.AddClassHandler((b, e) => + TintLuminosityOpacityProperty.Changed.AddClassHandler((b, e) => { b._effectiveTintColor = GetEffectiveTintColor(b.TintColor, b.TintOpacity); b._effectiveLuminosityColor = b.GetEffectiveLuminosityColor(); @@ -38,19 +150,22 @@ namespace Avalonia.Media /// Defines the property. /// public static readonly StyledProperty TintColorProperty = - AvaloniaProperty.Register(nameof(TintColor)); + AvaloniaProperty.Register(nameof(TintColor)); public static readonly StyledProperty BackgroundSourceProperty = - AvaloniaProperty.Register(nameof(BackgroundSource)); + AvaloniaProperty.Register(nameof(BackgroundSource)); public static readonly StyledProperty TintOpacityProperty = - AvaloniaProperty.Register(nameof(TintOpacity), 0.9); + AvaloniaProperty.Register(nameof(TintOpacity), 0.8); public static readonly StyledProperty TintLuminosityOpacityProperty = - AvaloniaProperty.Register(nameof(TintLuminosityOpacity), 0.8); + AvaloniaProperty.Register(nameof(TintLuminosityOpacity), 0.5); public static readonly StyledProperty FallbackColorProperty = - AvaloniaProperty.Register(nameof(FallbackColor)); + AvaloniaProperty.Register(nameof(FallbackColor)); + + /// + public event EventHandler Invalidated; public AcrylicBackgroundSource BackgroundSource { @@ -64,10 +179,6 @@ namespace Avalonia.Media set => SetValue(TintColorProperty, value); } - Color IExperimentalAcrylicBrush.TintColor => _effectiveTintColor; - - Color IExperimentalAcrylicBrush.LuminosityColor => _effectiveLuminosityColor; - public double TintOpacity { get => GetValue(TintOpacityProperty); @@ -86,10 +197,9 @@ namespace Avalonia.Media set => SetValue(TintLuminosityOpacityProperty, value); } - public override IBrush ToImmutable() - { - return new ImmutableExperimentalAcrylicBrush(this); - } + Color IExperimentalAcrylicMaterial.LuminosityColor => _effectiveLuminosityColor; + + Color IExperimentalAcrylicMaterial.TintColor => _effectiveTintColor; public struct HsvColor { @@ -172,7 +282,7 @@ namespace Avalonia.Media const double midPoint = 0.5; // Mid point of HsvV range that these calculations are based on. This is here for easy tuning. - double whiteMaxOpacity = 0.65; // 100% luminosity + double whiteMaxOpacity = 0.45; // 100% luminosity double midPointMaxOpacity = 0.90; // 50% luminosity double blackMaxOpacity = 0.85; // 0% luminosity @@ -308,5 +418,38 @@ namespace Avalonia.Media return new Color((byte)(255 * Math.Max(Math.Min(luminosityOpacity.Value * modifier, 1.0), 0.0)), luminosityColor.R, luminosityColor.G, luminosityColor.B); } + + /// + /// Marks a property as affecting the brush's visual representation. + /// + /// The properties. + /// + /// After a call to this method in a brush's static constructor, any change to the + /// property will cause the event to be raised on the brush. + /// + protected static void AffectsRender(params AvaloniaProperty[] properties) + where T : ExperimentalAcrylicMaterial + { + static void Invalidate(AvaloniaPropertyChangedEventArgs e) + { + (e.Sender as T)?.RaiseInvalidated(EventArgs.Empty); + } + + foreach (var property in properties) + { + property.Changed.Subscribe(e => Invalidate(e)); + } + } + + /// + /// Raises the event. + /// + /// The event args. + protected void RaiseInvalidated(EventArgs e) => Invalidated?.Invoke(this, e); + + public IExperimentalAcrylicMaterial ToImmutable() + { + return new ImmutableExperimentalAcrylicMaterial(this); + } } } diff --git a/src/Avalonia.Visuals/Media/IExperimentalAcrylicBrush.cs b/src/Avalonia.Visuals/Media/IExperimentalAcrylicBrush.cs deleted file mode 100644 index 30cbeb331c..0000000000 --- a/src/Avalonia.Visuals/Media/IExperimentalAcrylicBrush.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Drawing; - -namespace Avalonia.Media -{ - public interface IExperimentalAcrylicBrush : IBrush - { - AcrylicBackgroundSource BackgroundSource { get; } - - Color TintColor { get; } - - Color LuminosityColor { get; } - - double TintOpacity { get; } - - Color FallbackColor { get; } - } -} diff --git a/src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicBrush.cs b/src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicBrush.cs deleted file mode 100644 index 8cb61dbaf4..0000000000 --- a/src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicBrush.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System; - -namespace Avalonia.Media -{ - public readonly struct ImmutableExperimentalAcrylicBrush : IExperimentalAcrylicBrush, IEquatable - { - public ImmutableExperimentalAcrylicBrush(IExperimentalAcrylicBrush brush) - { - BackgroundSource = brush.BackgroundSource; - TintColor = brush.TintColor; - TintOpacity = brush.TintOpacity; - FallbackColor = brush.FallbackColor; - Opacity = brush.Opacity; - LuminosityColor = brush.LuminosityColor; - } - - public AcrylicBackgroundSource BackgroundSource { get; } - - public Color TintColor { get; } - - public Color LuminosityColor { get; } - - public double TintOpacity { get; } - - public Color FallbackColor { get; } - - public double Opacity { get; } - - public bool Equals(ImmutableExperimentalAcrylicBrush other) - { - // ReSharper disable once CompareOfFloatsByEqualityOperator - return - TintColor == other.TintColor && - Opacity == other.Opacity && - TintOpacity == other.TintOpacity && - BackgroundSource == other.BackgroundSource && - FallbackColor == other.FallbackColor && LuminosityColor == other.LuminosityColor; - - } - - public override bool Equals(object obj) - { - return obj is ImmutableExperimentalAcrylicBrush other && Equals(other); - } - - public Color GetEffectiveTintColor() - { - return TintColor; - } - - public override int GetHashCode() - { - unchecked - { - int hash = 17; - - hash = (hash * 23) + TintColor.GetHashCode(); - hash = (hash * 23) + Opacity.GetHashCode(); - hash = (hash * 23) + TintOpacity.GetHashCode(); - hash = (hash * 23) + BackgroundSource.GetHashCode(); - hash = (hash * 23) + FallbackColor.GetHashCode(); - hash = (hash * 23) + LuminosityColor.GetHashCode(); - - return hash; - } - } - - public static bool operator ==(ImmutableExperimentalAcrylicBrush left, ImmutableExperimentalAcrylicBrush right) - { - return left.Equals(right); - } - - public static bool operator !=(ImmutableExperimentalAcrylicBrush left, ImmutableExperimentalAcrylicBrush right) - { - return !left.Equals(right); - } - } -} diff --git a/src/Avalonia.Visuals/Platform/IDrawingContextWithAcrylicLikeSupport.cs b/src/Avalonia.Visuals/Platform/IDrawingContextWithAcrylicLikeSupport.cs new file mode 100644 index 0000000000..a3cf0298dd --- /dev/null +++ b/src/Avalonia.Visuals/Platform/IDrawingContextWithAcrylicLikeSupport.cs @@ -0,0 +1,9 @@ +using Avalonia.Media; + +namespace Avalonia.Platform +{ + public interface IDrawingContextWithAcrylicLikeSupport + { + void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect); + } +} diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs index 6ad71ac111..0ca7e876cb 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs @@ -11,7 +11,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// A drawing context which builds a scene graph. /// - internal class DeferredDrawingContextImpl : IDrawingContextImpl + internal class DeferredDrawingContextImpl : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport { private readonly ISceneBuilder _sceneBuilder; private VisualNode _node; @@ -163,7 +163,22 @@ namespace Avalonia.Rendering.SceneGraph ++_drawOperationindex; } } - + + /// + public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, material, rect)) + { + Add(new ExperimentalAcrylicNode(Transform, material, rect)); + } + else + { + ++_drawOperationindex; + } + } + public void Custom(ICustomDrawOperation custom) { var next = NextDrawAs(); diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/ExperimentalAcrylicNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/ExperimentalAcrylicNode.cs new file mode 100644 index 0000000000..9969652a8b --- /dev/null +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/ExperimentalAcrylicNode.cs @@ -0,0 +1,93 @@ +using System; +using Avalonia.Media; +using Avalonia.Platform; + +namespace Avalonia.Rendering.SceneGraph +{ + /// + /// A node in the scene graph which represents a rectangle draw. + /// + internal class ExperimentalAcrylicNode : DrawOperation + { + /// + /// Initializes a new instance of the class. + /// + /// The transform. + /// The rectangle to draw. + /// The box shadow parameters + /// Child scenes for drawing visual brushes. + public ExperimentalAcrylicNode( + Matrix transform, + IExperimentalAcrylicMaterial material, + RoundedRect rect) + : base(rect.Rect, transform) + { + Transform = transform; + Material = material?.ToImmutable(); + Rect = rect; + } + + /// + /// Gets the transform with which the node will be drawn. + /// + public Matrix Transform { get; } + + public IExperimentalAcrylicMaterial Material { get; } + + /// + /// Gets the rectangle to draw. + /// + public RoundedRect Rect { get; } + + /// + /// Determines if this draw operation equals another. + /// + /// The transform of the other draw operation. + /// The fill of the other draw operation. + /// The rectangle of the other draw operation. + /// True if the draw operations are the same, otherwise false. + /// + /// The properties of the other draw operation are passed in as arguments to prevent + /// allocation of a not-yet-constructed draw operation object. + /// + public bool Equals(Matrix transform, IExperimentalAcrylicMaterial material, RoundedRect rect) + { + return transform == Transform && + Equals(material, Material) && + rect.Equals(Rect); + } + + /// + public override void Render(IDrawingContextImpl context) + { + context.Transform = Transform; + + if(context is IDrawingContextWithAcrylicLikeSupport idc) + { + idc.DrawRectangle(Material, Rect); + } + else + { + context.DrawRectangle(new SolidColorBrush(Material.FallbackColor), null, Rect); + } + } + + /// + public override bool HitTest(Point p) + { + // TODO: This doesn't respect CornerRadius yet. + if (Transform.HasInverse) + { + p *= Transform.Invert(); + + if (Material != null) + { + var rect = Rect.Rect; + return rect.Contains(p); + } + } + + return false; + } + } +} diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs index 5059a6d042..ec1a7753b1 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs @@ -19,7 +19,7 @@ namespace Avalonia.Rendering.SceneGraph /// The fill brush. /// The stroke pen. /// The rectangle to draw. - /// The box shadow parameters + /// The box shadow parameters /// Child scenes for drawing visual brushes. public RectangleNode( Matrix transform, diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 1ddc0980b5..47139b19d9 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -17,7 +17,7 @@ namespace Avalonia.Skia /// /// Skia based drawing context. /// - internal class DrawingContextImpl : IDrawingContextImpl, ISkiaDrawingContextImpl + internal class DrawingContextImpl : IDrawingContextImpl, ISkiaDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport { private IDisposable[] _disposables; private readonly Vector _dpi; @@ -228,6 +228,42 @@ namespace Avalonia.Skia return bounds; } + /// + public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect) + { + if (rect.Rect.Height <= 0 || rect.Rect.Width <= 0) + return; + + var rc = rect.Rect.ToSKRect(); + var isRounded = rect.IsRounded; + var needRoundRect = rect.IsRounded; + using var skRoundRect = needRoundRect ? new SKRoundRect() : null; + + if (needRoundRect) + skRoundRect.SetRectRadii(rc, + new[] + { + rect.RadiiTopLeft.ToSKPoint(), rect.RadiiTopRight.ToSKPoint(), + rect.RadiiBottomRight.ToSKPoint(), rect.RadiiBottomLeft.ToSKPoint(), + }); + + if (material != null) + { + using (var paint = CreateAcrylicPaint(_fillPaint, material, rect.Rect.Size)) + { + if (isRounded) + { + Canvas.DrawRoundRect(skRoundRect, paint.Paint); + } + else + { + Canvas.DrawRect(rc, paint.Paint); + } + + } + } + } + /// public void DrawRectangle(IBrush brush, IPen pen, RoundedRect rect, BoxShadows boxShadows = default) { @@ -690,6 +726,49 @@ namespace Avalonia.Skia ); } + internal PaintWrapper CreateAcrylicPaint (SKPaint paint, IExperimentalAcrylicMaterial material, Size targetSize, bool disposePaint = false) + { + var paintWrapper = new PaintWrapper(paint, disposePaint); + + paint.IsAntialias = true; + + double opacity = _currentOpacity; + + var tintOpacity = + material.BackgroundSource == AcrylicBackgroundSource.Digger ? + material.TintOpacity : 1; + + const double noiseOpcity = 0.0225; + + var tintColor = material.TintColor; + var tint = new SKColor(tintColor.R, tintColor.G, tintColor.B, tintColor.A); + + if (s_acrylicNoiseShader == null) + { + using (var stream = typeof(DrawingContextImpl).Assembly.GetManifestResourceStream("Avalonia.Skia.Assets.NoiseAsset_256X256_PNG.png")) + using (var bitmap = SKBitmap.Decode(stream)) + { + s_acrylicNoiseShader = SKShader.CreateBitmap(bitmap, SKShaderTileMode.Repeat, SKShaderTileMode.Repeat) + .WithColorFilter(CreateAlphaColorFilter(noiseOpcity)); + } + } + + using (var backdrop = SKShader.CreateColor(new SKColor(material.LuminosityColor.R, material.LuminosityColor.G, material.LuminosityColor.B, material.LuminosityColor.A))) + using (var tintShader = SKShader.CreateColor(tint)) + using (var effectiveTint = SKShader.CreateCompose(backdrop, tintShader)) + using (var compose = SKShader.CreateCompose(effectiveTint, s_acrylicNoiseShader)) + { + paint.Shader = compose; + + if (material.BackgroundSource == AcrylicBackgroundSource.Digger) + { + paint.BlendMode = SKBlendMode.Src; + } + + return paintWrapper; + } + } + /// /// Creates paint wrapper for given brush. /// @@ -713,43 +792,6 @@ namespace Avalonia.Skia return paintWrapper; } - if (brush is IExperimentalAcrylicBrush acrylicBrush) - { - var tintOpacity = - acrylicBrush.BackgroundSource == AcrylicBackgroundSource.Digger ? - acrylicBrush.TintOpacity : 1; - - const double noiseOpcity = 0.0225; - - var tintColor = acrylicBrush.TintColor; - var tint = new SKColor(tintColor.R, tintColor.G, tintColor.B, tintColor.A); - - if (s_acrylicNoiseShader == null) - { - using(var stream = typeof(DrawingContextImpl).Assembly.GetManifestResourceStream("Avalonia.Skia.Assets.NoiseAsset_256X256_PNG.png")) - using (var bitmap = SKBitmap.Decode(stream)) - { - s_acrylicNoiseShader = SKShader.CreateBitmap(bitmap, SKShaderTileMode.Repeat, SKShaderTileMode.Repeat) - .WithColorFilter(CreateAlphaColorFilter(noiseOpcity)); - } - } - - using (var backdrop = SKShader.CreateColor(new SKColor(acrylicBrush.LuminosityColor.R, acrylicBrush.LuminosityColor.G, acrylicBrush.LuminosityColor.B, acrylicBrush.LuminosityColor.A))) - using (var tintShader = SKShader.CreateColor(tint)) - using (var effectiveTint = SKShader.CreateCompose (backdrop, tintShader)) - using (var compose = SKShader.CreateCompose(effectiveTint, s_acrylicNoiseShader)) - { - paint.Shader = compose; - - if (acrylicBrush.BackgroundSource == AcrylicBackgroundSource.Digger) - { - paint.BlendMode = SKBlendMode.Src; - } - - return paintWrapper; - } - } - paint.Color = new SKColor(255, 255, 255, (byte) (255 * opacity)); if (brush is IGradientBrush gradient) @@ -879,7 +921,7 @@ namespace Avalonia.Skia }; return new SurfaceRenderTarget(createInfo); - } + } /// /// Skia cached paint state. From f96ad2f7f6697c478420c0418e3e5c36d20fcd8a Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 12 Jun 2020 16:15:00 -0300 Subject: [PATCH 087/693] update acrylic page. --- samples/ControlCatalog/Pages/AcrylicPage.xaml | 112 +++++++++--------- 1 file changed, 57 insertions(+), 55 deletions(-) diff --git a/samples/ControlCatalog/Pages/AcrylicPage.xaml b/samples/ControlCatalog/Pages/AcrylicPage.xaml index f2785dfcfd..214d012aff 100644 --- a/samples/ControlCatalog/Pages/AcrylicPage.xaml +++ b/samples/ControlCatalog/Pages/AcrylicPage.xaml @@ -7,12 +7,12 @@ - - - + + - + @@ -21,113 +21,115 @@ - + + - - - + + - - + + - - - + + - - + + - - - + + - - + + + - - - + + - - + + - - - + + - - + + - - - + + - - + + - - - + + - - + + - - - + + - - + + - - - + + - - + + - - - + + - - + + From 65df446f2ab604a4971ae497bad82fb1aaa873ac Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 12 Jun 2020 16:22:45 -0300 Subject: [PATCH 088/693] use immutable solidcolor brush for fallback in drawing node. --- .../Rendering/SceneGraph/ExperimentalAcrylicNode.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/ExperimentalAcrylicNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/ExperimentalAcrylicNode.cs index 9969652a8b..336d11e3fd 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/ExperimentalAcrylicNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/ExperimentalAcrylicNode.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Media; +using Avalonia.Media.Immutable; using Avalonia.Platform; namespace Avalonia.Rendering.SceneGraph @@ -68,7 +69,7 @@ namespace Avalonia.Rendering.SceneGraph } else { - context.DrawRectangle(new SolidColorBrush(Material.FallbackColor), null, Rect); + context.DrawRectangle(new ImmutableSolidColorBrush(Material.FallbackColor), null, Rect); } } From 62b5932c11a8eede1c101eee40d7af1a67a94a77 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 12 Jun 2020 18:02:11 -0300 Subject: [PATCH 089/693] remove unused arguments. --- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 47139b19d9..cba96c4a26 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -249,7 +249,7 @@ namespace Avalonia.Skia if (material != null) { - using (var paint = CreateAcrylicPaint(_fillPaint, material, rect.Rect.Size)) + using (var paint = CreateAcrylicPaint(_fillPaint, material)) { if (isRounded) { @@ -726,7 +726,7 @@ namespace Avalonia.Skia ); } - internal PaintWrapper CreateAcrylicPaint (SKPaint paint, IExperimentalAcrylicMaterial material, Size targetSize, bool disposePaint = false) + internal PaintWrapper CreateAcrylicPaint (SKPaint paint, IExperimentalAcrylicMaterial material, bool disposePaint = false) { var paintWrapper = new PaintWrapper(paint, disposePaint); From 88b016970e006fa48a6a3832263ab62e031177a3 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 12 Jun 2020 18:02:22 -0300 Subject: [PATCH 090/693] tidy acrylic page. --- samples/ControlCatalog/Pages/AcrylicPage.xaml | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/samples/ControlCatalog/Pages/AcrylicPage.xaml b/samples/ControlCatalog/Pages/AcrylicPage.xaml index 214d012aff..14ce1cff55 100644 --- a/samples/ControlCatalog/Pages/AcrylicPage.xaml +++ b/samples/ControlCatalog/Pages/AcrylicPage.xaml @@ -13,13 +13,17 @@ TintColor="White" BackgroundSource="Digger" /> - - - - - - - + + + + + + + + + + + From d7f7acc66fdc067c337c87f2103397bccb25f831 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 12 Jun 2020 18:02:50 -0300 Subject: [PATCH 091/693] seperate into files acrylic media classes. --- .../Media/ExperimentalAcrylicMaterial.cs | 152 +++--------------- .../Media/IExperimentalAcrylicMaterial.cs | 15 ++ .../IMutableExperimentalAcrylicMaterial.cs | 14 ++ .../ImmutableExperimentalAcrylicMaterial.cs | 73 +++++++++ .../Media/MaterialExtensions.cs | 22 +++ 5 files changed, 149 insertions(+), 127 deletions(-) create mode 100644 src/Avalonia.Visuals/Media/IExperimentalAcrylicMaterial.cs create mode 100644 src/Avalonia.Visuals/Media/IMutableExperimentalAcrylicMaterial.cs create mode 100644 src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicMaterial.cs create mode 100644 src/Avalonia.Visuals/Media/MaterialExtensions.cs diff --git a/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs b/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs index 507931da5a..c96dfda4a0 100644 --- a/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs +++ b/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs @@ -2,118 +2,6 @@ namespace Avalonia.Media { - /// - /// Represents a mutable brush which can return an immutable clone of itself. - /// - public interface IMutableExperimentalAcrylicMaterial : IExperimentalAcrylicMaterial, IAffectsRender - { - /// - /// Creates an immutable clone of the brush. - /// - /// The immutable clone. - IExperimentalAcrylicMaterial ToImmutable(); - } - - public interface IExperimentalAcrylicMaterial - { - AcrylicBackgroundSource BackgroundSource { get; } - - Color TintColor { get; } - - Color LuminosityColor { get; } - - double TintOpacity { get; } - - Color FallbackColor { get; } - } - - public static class MaterialExtensions - { - /// - /// Converts a brush to an immutable brush. - /// - /// The brush. - /// - /// The result of calling if the brush is mutable, - /// otherwise . - /// - public static IExperimentalAcrylicMaterial ToImmutable(this IExperimentalAcrylicMaterial material) - { - Contract.Requires(material != null); - - return (material as IMutableExperimentalAcrylicMaterial)?.ToImmutable() ?? material; - } - } - - public readonly struct ImmutableExperimentalAcrylicMaterial : IExperimentalAcrylicMaterial, IEquatable - { - public ImmutableExperimentalAcrylicMaterial(IExperimentalAcrylicMaterial brush) - { - BackgroundSource = brush.BackgroundSource; - TintColor = brush.TintColor; - TintOpacity = brush.TintOpacity; - FallbackColor = brush.FallbackColor; - LuminosityColor = brush.LuminosityColor; - } - - public AcrylicBackgroundSource BackgroundSource { get; } - - public Color TintColor { get; } - - public Color LuminosityColor { get; } - - public double TintOpacity { get; } - - public Color FallbackColor { get; } - - public bool Equals(ImmutableExperimentalAcrylicMaterial other) - { - // ReSharper disable once CompareOfFloatsByEqualityOperator - return - TintColor == other.TintColor && - TintOpacity == other.TintOpacity && - BackgroundSource == other.BackgroundSource && - FallbackColor == other.FallbackColor && LuminosityColor == other.LuminosityColor; - - } - - public override bool Equals(object obj) - { - return obj is ImmutableExperimentalAcrylicMaterial other && Equals(other); - } - - public Color GetEffectiveTintColor() - { - return TintColor; - } - - public override int GetHashCode() - { - unchecked - { - int hash = 17; - - hash = (hash * 23) + TintColor.GetHashCode(); - hash = (hash * 23) + TintOpacity.GetHashCode(); - hash = (hash * 23) + BackgroundSource.GetHashCode(); - hash = (hash * 23) + FallbackColor.GetHashCode(); - hash = (hash * 23) + LuminosityColor.GetHashCode(); - - return hash; - } - } - - public static bool operator ==(ImmutableExperimentalAcrylicMaterial left, ImmutableExperimentalAcrylicMaterial right) - { - return left.Equals(right); - } - - public static bool operator !=(ImmutableExperimentalAcrylicMaterial left, ImmutableExperimentalAcrylicMaterial right) - { - return !left.Equals(right); - } - } - public class ExperimentalAcrylicMaterial : AvaloniaObject, IMutableExperimentalAcrylicMaterial { private Color _effectiveTintColor; @@ -266,12 +154,6 @@ namespace Avalonia.Media return tintColor; } - private static double AdjustOpacity(double opacity) - { - var result = Math.Max((1.0 - Math.Pow((1.0 - opacity), 3.85)), 0.92); - return result; - } - private static double GetTintOpacityModifier(Color tintColor) { // This method supresses the maximum allowable tint opacity depending on the luminosity and saturation of a color by @@ -282,9 +164,9 @@ namespace Avalonia.Media const double midPoint = 0.5; // Mid point of HsvV range that these calculations are based on. This is here for easy tuning. - double whiteMaxOpacity = 0.45; // 100% luminosity - double midPointMaxOpacity = 0.90; // 50% luminosity - double blackMaxOpacity = 0.85; // 0% luminosity + const double whiteMaxOpacity = 0.45; // 100% luminosity + const double midPointMaxOpacity = 0.50; // 50% luminosity + const double blackMaxOpacity = 0.85; // 0% luminosity var hsv = RgbToHsv(tintColor); @@ -316,7 +198,7 @@ namespace Avalonia.Media if (hsv.Saturation > 0) { // Dampen opacity suppression based on how much saturation there is - //maxOpacitySuppression *= Math.Max(1 - (hsv.Saturation * 2), 0.0); + maxOpacitySuppression *= Math.Max(1 - (hsv.Saturation * 2), 0.0); } double opacitySuppression = maxOpacitySuppression * normalizedDeviation; @@ -404,19 +286,35 @@ namespace Avalonia.Media return (byte)value; } - double Luminosity(Color color) + private static float RGBMax(Color color) { - return 0.299 * color.R + 0.587 * color.G + 0.114 * color.B; + if (color.R > color.G) + return (color.R > color.B) ? color.R : color.B; + else + return (color.G > color.B) ? color.G : color.B; + } + + private static float RGBMin(Color color) + { + if (color.R < color.G) + return (color.R < color.B) ? color.R : color.B; + else + return (color.G < color.B) ? color.G : color.B; } // The tintColor passed into this method should be the original, unmodified color created using user values for TintColor + TintOpacity Color GetLuminosityColor(double? luminosityOpacity) { - var luminosityColor = new Color(255, 127, 127, 127); + var hsv = RgbToHsv(TintColor); + + var max = (float)RGBMax(TintColor) / 255.0f; + var min = (float)RGBMin(TintColor) / 255.0f; + + var lightness = (max + min) / 2.0f; - var modifier = GetTintOpacityModifier(luminosityColor); + var luminosityColor = new Color(255, Trim(lightness), Trim(lightness), Trim(lightness)); - return new Color((byte)(255 * Math.Max(Math.Min(luminosityOpacity.Value * modifier, 1.0), 0.0)), luminosityColor.R, luminosityColor.G, luminosityColor.B); + return new Color((byte)(255 * Math.Max(Math.Min(((luminosityOpacity.Value)), 1.0), 0.0)), luminosityColor.R, luminosityColor.G, luminosityColor.B); } /// diff --git a/src/Avalonia.Visuals/Media/IExperimentalAcrylicMaterial.cs b/src/Avalonia.Visuals/Media/IExperimentalAcrylicMaterial.cs new file mode 100644 index 0000000000..bcd3bbcc0b --- /dev/null +++ b/src/Avalonia.Visuals/Media/IExperimentalAcrylicMaterial.cs @@ -0,0 +1,15 @@ +namespace Avalonia.Media +{ + public interface IExperimentalAcrylicMaterial + { + AcrylicBackgroundSource BackgroundSource { get; } + + Color TintColor { get; } + + Color LuminosityColor { get; } + + double TintOpacity { get; } + + Color FallbackColor { get; } + } +} diff --git a/src/Avalonia.Visuals/Media/IMutableExperimentalAcrylicMaterial.cs b/src/Avalonia.Visuals/Media/IMutableExperimentalAcrylicMaterial.cs new file mode 100644 index 0000000000..fcfe4631a6 --- /dev/null +++ b/src/Avalonia.Visuals/Media/IMutableExperimentalAcrylicMaterial.cs @@ -0,0 +1,14 @@ +namespace Avalonia.Media +{ + /// + /// Represents a mutable brush which can return an immutable clone of itself. + /// + public interface IMutableExperimentalAcrylicMaterial : IExperimentalAcrylicMaterial, IAffectsRender + { + /// + /// Creates an immutable clone of the brush. + /// + /// The immutable clone. + IExperimentalAcrylicMaterial ToImmutable(); + } +} diff --git a/src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicMaterial.cs b/src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicMaterial.cs new file mode 100644 index 0000000000..c5cf420509 --- /dev/null +++ b/src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicMaterial.cs @@ -0,0 +1,73 @@ +using System; + +namespace Avalonia.Media +{ + public readonly struct ImmutableExperimentalAcrylicMaterial : IExperimentalAcrylicMaterial, IEquatable + { + public ImmutableExperimentalAcrylicMaterial(IExperimentalAcrylicMaterial brush) + { + BackgroundSource = brush.BackgroundSource; + TintColor = brush.TintColor; + TintOpacity = brush.TintOpacity; + FallbackColor = brush.FallbackColor; + LuminosityColor = brush.LuminosityColor; + } + + public AcrylicBackgroundSource BackgroundSource { get; } + + public Color TintColor { get; } + + public Color LuminosityColor { get; } + + public double TintOpacity { get; } + + public Color FallbackColor { get; } + + public bool Equals(ImmutableExperimentalAcrylicMaterial other) + { + // ReSharper disable once CompareOfFloatsByEqualityOperator + return + TintColor == other.TintColor && + TintOpacity == other.TintOpacity && + BackgroundSource == other.BackgroundSource && + FallbackColor == other.FallbackColor && LuminosityColor == other.LuminosityColor; + + } + + public override bool Equals(object obj) + { + return obj is ImmutableExperimentalAcrylicMaterial other && Equals(other); + } + + public Color GetEffectiveTintColor() + { + return TintColor; + } + + public override int GetHashCode() + { + unchecked + { + int hash = 17; + + hash = (hash * 23) + TintColor.GetHashCode(); + hash = (hash * 23) + TintOpacity.GetHashCode(); + hash = (hash * 23) + BackgroundSource.GetHashCode(); + hash = (hash * 23) + FallbackColor.GetHashCode(); + hash = (hash * 23) + LuminosityColor.GetHashCode(); + + return hash; + } + } + + public static bool operator ==(ImmutableExperimentalAcrylicMaterial left, ImmutableExperimentalAcrylicMaterial right) + { + return left.Equals(right); + } + + public static bool operator !=(ImmutableExperimentalAcrylicMaterial left, ImmutableExperimentalAcrylicMaterial right) + { + return !left.Equals(right); + } + } +} diff --git a/src/Avalonia.Visuals/Media/MaterialExtensions.cs b/src/Avalonia.Visuals/Media/MaterialExtensions.cs new file mode 100644 index 0000000000..c0b445c357 --- /dev/null +++ b/src/Avalonia.Visuals/Media/MaterialExtensions.cs @@ -0,0 +1,22 @@ +using System; + +namespace Avalonia.Media +{ + public static class MaterialExtensions + { + /// + /// Converts a brush to an immutable brush. + /// + /// The brush. + /// + /// The result of calling if the brush is mutable, + /// otherwise . + /// + public static IExperimentalAcrylicMaterial ToImmutable(this IExperimentalAcrylicMaterial material) + { + Contract.Requires(material != null); + + return (material as IMutableExperimentalAcrylicMaterial)?.ToImmutable() ?? material; + } + } +} From effb994823c18ce8219a3cb8bdb45ec8bb1660f5 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 12 Jun 2020 18:28:31 -0300 Subject: [PATCH 092/693] Add a way for acrylic material to compensate for different transparency levels. --- .../ExperimentalAcrylicBorder.cs | 33 +++++++++++++++++++ .../Media/ExperimentalAcrylicMaterial.cs | 22 +++++++++++-- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs b/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs index 4a855b9838..2c013fd244 100644 --- a/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs +++ b/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs @@ -2,6 +2,7 @@ using Avalonia.Layout; using Avalonia.Media; using Avalonia.Platform; +using System; namespace Avalonia.Controls { @@ -38,6 +39,38 @@ namespace Avalonia.Controls set => SetValue(MaterialProperty, value); } + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + var tl = (e.Root as TopLevel); + + tl.GetObservable(TopLevel.ActualTransparencyLevelProperty) + .Subscribe(x => + { + switch (x) + { + case WindowTransparencyLevel.Transparent: + case WindowTransparencyLevel.None: + Material.PlatformTransparencyCompensationLevel = 1; + break; + + case WindowTransparencyLevel.Blur: + Material.PlatformTransparencyCompensationLevel = 0.85; + break; + + case WindowTransparencyLevel.AcrylicBlur: + Material.PlatformTransparencyCompensationLevel = 0; + break; + } + }); + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + } + public override void Render(DrawingContext context) { diff --git a/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs b/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs index c96dfda4a0..c0004e8fd1 100644 --- a/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs +++ b/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs @@ -13,7 +13,8 @@ namespace Avalonia.Media TintColorProperty, BackgroundSourceProperty, TintOpacityProperty, - TintLuminosityOpacityProperty); + TintLuminosityOpacityProperty, + PlatformTransparencyCompensationLevelProperty); TintColorProperty.Changed.AddClassHandler((b, e) => { @@ -32,6 +33,12 @@ namespace Avalonia.Media b._effectiveTintColor = GetEffectiveTintColor(b.TintColor, b.TintOpacity); b._effectiveLuminosityColor = b.GetEffectiveLuminosityColor(); }); + + PlatformTransparencyCompensationLevelProperty.Changed.AddClassHandler((b, e) => + { + b._effectiveTintColor = GetEffectiveTintColor(b.TintColor, b.TintOpacity); + b._effectiveLuminosityColor = b.GetEffectiveLuminosityColor(); + }); } /// @@ -49,6 +56,9 @@ namespace Avalonia.Media public static readonly StyledProperty TintLuminosityOpacityProperty = AvaloniaProperty.Register(nameof(TintLuminosityOpacity), 0.5); + public static readonly StyledProperty PlatformTransparencyCompensationLevelProperty = + AvaloniaProperty.Register(nameof(PlatformTransparencyCompensationLevel), 0.0); + public static readonly StyledProperty FallbackColorProperty = AvaloniaProperty.Register(nameof(FallbackColor)); @@ -85,6 +95,12 @@ namespace Avalonia.Media set => SetValue(TintLuminosityOpacityProperty, value); } + public double PlatformTransparencyCompensationLevel + { + get => GetValue(PlatformTransparencyCompensationLevelProperty); + set => SetValue(PlatformTransparencyCompensationLevelProperty, value); + } + Color IExperimentalAcrylicMaterial.LuminosityColor => _effectiveLuminosityColor; Color IExperimentalAcrylicMaterial.TintColor => _effectiveTintColor; @@ -314,7 +330,9 @@ namespace Avalonia.Media var luminosityColor = new Color(255, Trim(lightness), Trim(lightness), Trim(lightness)); - return new Color((byte)(255 * Math.Max(Math.Min(((luminosityOpacity.Value)), 1.0), 0.0)), luminosityColor.R, luminosityColor.G, luminosityColor.B); + var compensationMultiplier = 1 - PlatformTransparencyCompensationLevel; + + return new Color((byte)(255 * Math.Max(Math.Min(PlatformTransparencyCompensationLevel + ( luminosityOpacity.Value * compensationMultiplier), 1.0), 0.0)), luminosityColor.R, luminosityColor.G, luminosityColor.B); } /// From bba17006ecc657c47ce263ab7b6d5a7035b6c25e Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 12 Jun 2020 18:51:00 -0300 Subject: [PATCH 093/693] tweak platform compensation. --- src/Avalonia.Controls/ExperimentalAcrylicBorder.cs | 2 +- src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs b/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs index 2c013fd244..05d261a921 100644 --- a/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs +++ b/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs @@ -56,7 +56,7 @@ namespace Avalonia.Controls break; case WindowTransparencyLevel.Blur: - Material.PlatformTransparencyCompensationLevel = 0.85; + Material.PlatformTransparencyCompensationLevel = 0.80; break; case WindowTransparencyLevel.AcrylicBlur: diff --git a/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs b/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs index c0004e8fd1..35f186f504 100644 --- a/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs +++ b/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs @@ -321,12 +321,13 @@ namespace Avalonia.Media // The tintColor passed into this method should be the original, unmodified color created using user values for TintColor + TintOpacity Color GetLuminosityColor(double? luminosityOpacity) { - var hsv = RgbToHsv(TintColor); - + // Calculate the HSL lightness value of the color. var max = (float)RGBMax(TintColor) / 255.0f; var min = (float)RGBMin(TintColor) / 255.0f; - var lightness = (max + min) / 2.0f; + var lightness = (max + min) / 2.0; + + lightness = 0.125 + (lightness * 0.75); // centralise and reduce the range of the opaque layer. var luminosityColor = new Color(255, Trim(lightness), Trim(lightness), Trim(lightness)); From cc2aa697422217b6a727ee00f2a5e65c1b5af20c Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 12 Jun 2020 18:55:06 -0300 Subject: [PATCH 094/693] rename properties that control "tracking paper effect." --- samples/ControlCatalog/Pages/AcrylicPage.xaml | 20 ++++++++-------- .../Media/ExperimentalAcrylicMaterial.cs | 23 +++++++++++-------- .../Media/IExperimentalAcrylicMaterial.cs | 4 ++-- .../ImmutableExperimentalAcrylicMaterial.cs | 8 +++---- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 2 +- 5 files changed, 30 insertions(+), 27 deletions(-) diff --git a/samples/ControlCatalog/Pages/AcrylicPage.xaml b/samples/ControlCatalog/Pages/AcrylicPage.xaml index 14ce1cff55..fd9487190b 100644 --- a/samples/ControlCatalog/Pages/AcrylicPage.xaml +++ b/samples/ControlCatalog/Pages/AcrylicPage.xaml @@ -34,7 +34,7 @@ @@ -44,7 +44,7 @@ @@ -54,7 +54,7 @@ @@ -67,7 +67,7 @@ @@ -77,7 +77,7 @@ @@ -87,7 +87,7 @@ @@ -99,7 +99,7 @@ @@ -109,7 +109,7 @@ @@ -119,7 +119,7 @@ @@ -130,7 +130,7 @@ diff --git a/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs b/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs index 35f186f504..5d6e1348f9 100644 --- a/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs +++ b/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs @@ -13,7 +13,7 @@ namespace Avalonia.Media TintColorProperty, BackgroundSourceProperty, TintOpacityProperty, - TintLuminosityOpacityProperty, + MaterialOpacityProperty, PlatformTransparencyCompensationLevelProperty); TintColorProperty.Changed.AddClassHandler((b, e) => @@ -28,7 +28,7 @@ namespace Avalonia.Media b._effectiveLuminosityColor = b.GetEffectiveLuminosityColor(); }); - TintLuminosityOpacityProperty.Changed.AddClassHandler((b, e) => + MaterialOpacityProperty.Changed.AddClassHandler((b, e) => { b._effectiveTintColor = GetEffectiveTintColor(b.TintColor, b.TintOpacity); b._effectiveLuminosityColor = b.GetEffectiveLuminosityColor(); @@ -53,8 +53,8 @@ namespace Avalonia.Media public static readonly StyledProperty TintOpacityProperty = AvaloniaProperty.Register(nameof(TintOpacity), 0.8); - public static readonly StyledProperty TintLuminosityOpacityProperty = - AvaloniaProperty.Register(nameof(TintLuminosityOpacity), 0.5); + public static readonly StyledProperty MaterialOpacityProperty = + AvaloniaProperty.Register(nameof(MaterialOpacity), 0.5); public static readonly StyledProperty PlatformTransparencyCompensationLevelProperty = AvaloniaProperty.Register(nameof(PlatformTransparencyCompensationLevel), 0.0); @@ -89,10 +89,10 @@ namespace Avalonia.Media set => SetValue(FallbackColorProperty, value); } - public double TintLuminosityOpacity + public double MaterialOpacity { - get => GetValue(TintLuminosityOpacityProperty); - set => SetValue(TintLuminosityOpacityProperty, value); + get => GetValue(MaterialOpacityProperty); + set => SetValue(MaterialOpacityProperty, value); } public double PlatformTransparencyCompensationLevel @@ -101,7 +101,7 @@ namespace Avalonia.Media set => SetValue(PlatformTransparencyCompensationLevelProperty, value); } - Color IExperimentalAcrylicMaterial.LuminosityColor => _effectiveLuminosityColor; + Color IExperimentalAcrylicMaterial.MaterialColor => _effectiveLuminosityColor; Color IExperimentalAcrylicMaterial.TintColor => _effectiveTintColor; @@ -232,7 +232,7 @@ namespace Avalonia.Media // Purposely leaving out tint opacity modifier here because GetLuminosityColor needs the *original* tint opacity set by the user. var tintColor = new Color((byte)(Math.Round(TintColor.A * tintOpacity)), TintColor.R, TintColor.G, TintColor.B); - double? luminosityOpacity = TintLuminosityOpacity; + double? luminosityOpacity = MaterialOpacity; return GetLuminosityColor(luminosityOpacity); } @@ -327,7 +327,10 @@ namespace Avalonia.Media var lightness = (max + min) / 2.0; - lightness = 0.125 + (lightness * 0.75); // centralise and reduce the range of the opaque layer. + // centralise and reduce the range of the opaque layer. + // this is done so pure white and pure black still have the effect of being blended with something. + // i.e. they get a little darker / lighter. + lightness = 0.125 + (lightness * 0.75); var luminosityColor = new Color(255, Trim(lightness), Trim(lightness), Trim(lightness)); diff --git a/src/Avalonia.Visuals/Media/IExperimentalAcrylicMaterial.cs b/src/Avalonia.Visuals/Media/IExperimentalAcrylicMaterial.cs index bcd3bbcc0b..a656c75d46 100644 --- a/src/Avalonia.Visuals/Media/IExperimentalAcrylicMaterial.cs +++ b/src/Avalonia.Visuals/Media/IExperimentalAcrylicMaterial.cs @@ -6,10 +6,10 @@ Color TintColor { get; } - Color LuminosityColor { get; } - double TintOpacity { get; } + Color MaterialColor { get; } + Color FallbackColor { get; } } } diff --git a/src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicMaterial.cs b/src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicMaterial.cs index c5cf420509..f46d76cf3f 100644 --- a/src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicMaterial.cs +++ b/src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicMaterial.cs @@ -10,14 +10,14 @@ namespace Avalonia.Media TintColor = brush.TintColor; TintOpacity = brush.TintOpacity; FallbackColor = brush.FallbackColor; - LuminosityColor = brush.LuminosityColor; + MaterialColor = brush.MaterialColor; } public AcrylicBackgroundSource BackgroundSource { get; } public Color TintColor { get; } - public Color LuminosityColor { get; } + public Color MaterialColor { get; } public double TintOpacity { get; } @@ -30,7 +30,7 @@ namespace Avalonia.Media TintColor == other.TintColor && TintOpacity == other.TintOpacity && BackgroundSource == other.BackgroundSource && - FallbackColor == other.FallbackColor && LuminosityColor == other.LuminosityColor; + FallbackColor == other.FallbackColor && MaterialColor == other.MaterialColor; } @@ -54,7 +54,7 @@ namespace Avalonia.Media hash = (hash * 23) + TintOpacity.GetHashCode(); hash = (hash * 23) + BackgroundSource.GetHashCode(); hash = (hash * 23) + FallbackColor.GetHashCode(); - hash = (hash * 23) + LuminosityColor.GetHashCode(); + hash = (hash * 23) + MaterialColor.GetHashCode(); return hash; } diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index cba96c4a26..0573aeee1e 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -753,7 +753,7 @@ namespace Avalonia.Skia } } - using (var backdrop = SKShader.CreateColor(new SKColor(material.LuminosityColor.R, material.LuminosityColor.G, material.LuminosityColor.B, material.LuminosityColor.A))) + using (var backdrop = SKShader.CreateColor(new SKColor(material.MaterialColor.R, material.MaterialColor.G, material.MaterialColor.B, material.MaterialColor.A))) using (var tintShader = SKShader.CreateColor(tint)) using (var effectiveTint = SKShader.CreateCompose(backdrop, tintShader)) using (var compose = SKShader.CreateCompose(effectiveTint, s_acrylicNoiseShader)) From a6bc06f7ad4902ac1fa131122c5501b009259a10 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 12 Jun 2020 18:59:03 -0300 Subject: [PATCH 095/693] make names on acrylic page make sense. --- samples/ControlCatalog/Pages/AcrylicPage.xaml | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/samples/ControlCatalog/Pages/AcrylicPage.xaml b/samples/ControlCatalog/Pages/AcrylicPage.xaml index fd9487190b..96cfcc5288 100644 --- a/samples/ControlCatalog/Pages/AcrylicPage.xaml +++ b/samples/ControlCatalog/Pages/AcrylicPage.xaml @@ -20,9 +20,9 @@ - - - + + + @@ -34,7 +34,7 @@ @@ -44,7 +44,7 @@ @@ -54,7 +54,7 @@ @@ -67,7 +67,7 @@ @@ -77,7 +77,7 @@ @@ -87,7 +87,7 @@ @@ -99,7 +99,7 @@ @@ -109,7 +109,7 @@ @@ -119,7 +119,7 @@ @@ -130,7 +130,7 @@ From 3b5c7c53145931704bb453684b88eb6151a85729 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 13 Jun 2020 12:08:24 -0300 Subject: [PATCH 096/693] change tracing paper effect algorithm. --- .../ExperimentalAcrylicBorder.cs | 4 ++-- .../Media/ExperimentalAcrylicMaterial.cs | 15 +++++++-------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs b/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs index 05d261a921..a6e7b52cb0 100644 --- a/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs +++ b/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs @@ -56,11 +56,11 @@ namespace Avalonia.Controls break; case WindowTransparencyLevel.Blur: - Material.PlatformTransparencyCompensationLevel = 0.80; + Material.PlatformTransparencyCompensationLevel = 0.90; break; case WindowTransparencyLevel.AcrylicBlur: - Material.PlatformTransparencyCompensationLevel = 0; + Material.PlatformTransparencyCompensationLevel = 0.70; break; } }); diff --git a/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs b/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs index 5d6e1348f9..503554be2d 100644 --- a/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs +++ b/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs @@ -182,7 +182,7 @@ namespace Avalonia.Media const double whiteMaxOpacity = 0.45; // 100% luminosity const double midPointMaxOpacity = 0.50; // 50% luminosity - const double blackMaxOpacity = 0.85; // 0% luminosity + const double blackMaxOpacity = 0.45; // 0% luminosity var hsv = RgbToHsv(tintColor); @@ -327,16 +327,15 @@ namespace Avalonia.Media var lightness = (max + min) / 2.0; - // centralise and reduce the range of the opaque layer. - // this is done so pure white and pure black still have the effect of being blended with something. - // i.e. they get a little darker / lighter. - lightness = 0.125 + (lightness * 0.75); + // 0 opacity closer to white, 1 opacity, close to the lightness of the pixel. + // TintOpacity multiplied in, so we endup with pure white if there is no TintOpacity. + lightness = 1 - (1 - lightness) * luminosityOpacity.Value * TintOpacity; var luminosityColor = new Color(255, Trim(lightness), Trim(lightness), Trim(lightness)); - var compensationMultiplier = 1 - PlatformTransparencyCompensationLevel; - - return new Color((byte)(255 * Math.Max(Math.Min(PlatformTransparencyCompensationLevel + ( luminosityOpacity.Value * compensationMultiplier), 1.0), 0.0)), luminosityColor.R, luminosityColor.G, luminosityColor.B); + //var compensationMultiplier = 1 - PlatformTransparencyCompensationLevel; + //return new Color((byte)(255 * Math.Max(Math.Min(PlatformTransparencyCompensationLevel + ( luminosityOpacity.Value * compensationMultiplier), 1.0), 0.0)), luminosityColor.R, luminosityColor.G, luminosityColor.B); + return new Color((byte)(255 * Math.Max(Math.Min((luminosityOpacity.Value), 1.0), 0.0)), luminosityColor.R, luminosityColor.G, luminosityColor.B); } /// From 79bad75e6c63f58313178d8fb7ce0c8fac0fa7a9 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 13 Jun 2020 13:27:49 -0300 Subject: [PATCH 097/693] very good settings for tracing paper effect. --- .../Media/ExperimentalAcrylicMaterial.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs b/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs index 503554be2d..dd74269297 100644 --- a/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs +++ b/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs @@ -180,9 +180,9 @@ namespace Avalonia.Media const double midPoint = 0.5; // Mid point of HsvV range that these calculations are based on. This is here for easy tuning. - const double whiteMaxOpacity = 0.45; // 100% luminosity - const double midPointMaxOpacity = 0.50; // 50% luminosity - const double blackMaxOpacity = 0.45; // 0% luminosity + const double whiteMaxOpacity = 0.15; // 100% luminosity + const double midPointMaxOpacity = 0.45; // 50% luminosity + const double blackMaxOpacity = 0.40; // 0% luminosity var hsv = RgbToHsv(tintColor); @@ -325,13 +325,15 @@ namespace Avalonia.Media var max = (float)RGBMax(TintColor) / 255.0f; var min = (float)RGBMin(TintColor) / 255.0f; - var lightness = (max + min) / 2.0; + var lightness = (max + min) / 2.0; // 0 opacity closer to white, 1 opacity, close to the lightness of the pixel. - // TintOpacity multiplied in, so we endup with pure white if there is no TintOpacity. - lightness = 1 - (1 - lightness) * luminosityOpacity.Value * TintOpacity; + lightness = 1 -((1 - lightness) * luminosityOpacity.Value); - var luminosityColor = new Color(255, Trim(lightness), Trim(lightness), Trim(lightness)); + lightness = 0.13 + (lightness * 0.74); + var luminosityColor = new Color(255, Trim(lightness), Trim(lightness), Trim(lightness)); + + //luminosityColor = new Color(255, Trim(lightness * modifier), Trim(lightness * modifier), Trim(lightness * modifier)); //var compensationMultiplier = 1 - PlatformTransparencyCompensationLevel; //return new Color((byte)(255 * Math.Max(Math.Min(PlatformTransparencyCompensationLevel + ( luminosityOpacity.Value * compensationMultiplier), 1.0), 0.0)), luminosityColor.R, luminosityColor.G, luminosityColor.B); From ed2fd5f3ee5be679d9341032cb7d21f9679a5498 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 13 Jun 2020 14:10:13 -0300 Subject: [PATCH 098/693] use blending technique to colourise the tracing paper. --- .../Media/ExperimentalAcrylicMaterial.cs | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs b/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs index dd74269297..614417ed20 100644 --- a/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs +++ b/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs @@ -318,6 +318,28 @@ namespace Avalonia.Media return (color.G < color.B) ? color.G : color.B; } + static byte Blend(byte leftColor, byte leftAlpha, byte rightColor, byte rightAlpha) + { + var ca = leftColor / 255d; + var aa = leftAlpha / 255d; + var cb = rightColor / 255d; + var ab = rightAlpha / 255d; + var r = (ca * aa + cb * ab * (1 - aa)) / (aa + ab * (1 - aa)); + return (byte)(r * 255); + } + + static Color Blend(Color left, Color right) + { + var aa = left.A / 255d; + var ab = right.A / 255d; + return new Color( + (byte)((aa + ab * (1 - aa)) * 255), + Blend(left.R, left.A, right.R, right.A), + Blend(left.G, left.A, right.G, right.A), + Blend(left.B, left.A, right.B, right.A) + ); + } + // The tintColor passed into this method should be the original, unmodified color created using user values for TintColor + TintOpacity Color GetLuminosityColor(double? luminosityOpacity) { @@ -325,15 +347,15 @@ namespace Avalonia.Media var max = (float)RGBMax(TintColor) / 255.0f; var min = (float)RGBMin(TintColor) / 255.0f; - var lightness = (max + min) / 2.0; + var lightness = (max + min) / 2.0; - // 0 opacity closer to white, 1 opacity, close to the lightness of the pixel. - lightness = 1 -((1 - lightness) * luminosityOpacity.Value); + lightness = 1 - ((1 - lightness) * luminosityOpacity.Value); lightness = 0.13 + (lightness * 0.74); - var luminosityColor = new Color(255, Trim(lightness), Trim(lightness), Trim(lightness)); - //luminosityColor = new Color(255, Trim(lightness * modifier), Trim(lightness * modifier), Trim(lightness * modifier)); + var luminosityColor = new Color(255, Trim(lightness), Trim(lightness), Trim(lightness)); + + luminosityColor = Blend(luminosityColor, new Color((byte)Math.Round(255 * TintOpacity), TintColor.R, TintColor.G, TintColor.B)); //var compensationMultiplier = 1 - PlatformTransparencyCompensationLevel; //return new Color((byte)(255 * Math.Max(Math.Min(PlatformTransparencyCompensationLevel + ( luminosityOpacity.Value * compensationMultiplier), 1.0), 0.0)), luminosityColor.R, luminosityColor.G, luminosityColor.B); From 1e9873e6d4a891fcd9f2b40d0dc7f5adef384236 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 13 Jun 2020 14:33:06 -0300 Subject: [PATCH 099/693] fix blend. --- src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs b/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs index 614417ed20..9c253df9dd 100644 --- a/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs +++ b/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs @@ -355,7 +355,7 @@ namespace Avalonia.Media var luminosityColor = new Color(255, Trim(lightness), Trim(lightness), Trim(lightness)); - luminosityColor = Blend(luminosityColor, new Color((byte)Math.Round(255 * TintOpacity), TintColor.R, TintColor.G, TintColor.B)); + luminosityColor = Blend(luminosityColor, new Color(255, TintColor.R, TintColor.G, TintColor.B)); //var compensationMultiplier = 1 - PlatformTransparencyCompensationLevel; //return new Color((byte)(255 * Math.Max(Math.Min(PlatformTransparencyCompensationLevel + ( luminosityOpacity.Value * compensationMultiplier), 1.0), 0.0)), luminosityColor.R, luminosityColor.G, luminosityColor.B); From f6162fa54298695395f4fa16355425ab06692c74 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 13 Jun 2020 14:33:21 -0300 Subject: [PATCH 100/693] dampen saturated opacities. --- src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs b/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs index 9c253df9dd..964102e50e 100644 --- a/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs +++ b/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs @@ -181,7 +181,7 @@ namespace Avalonia.Media const double midPoint = 0.5; // Mid point of HsvV range that these calculations are based on. This is here for easy tuning. const double whiteMaxOpacity = 0.15; // 100% luminosity - const double midPointMaxOpacity = 0.45; // 50% luminosity + const double midPointMaxOpacity = 0.40; // 50% luminosity const double blackMaxOpacity = 0.40; // 0% luminosity var hsv = RgbToHsv(tintColor); From dd17ffe0b7aed279c95063b03dbdbd631e8ca669 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 13 Jun 2020 14:59:53 -0300 Subject: [PATCH 101/693] restore platform compensation. --- src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs b/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs index 964102e50e..7d2023cb21 100644 --- a/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs +++ b/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs @@ -357,9 +357,8 @@ namespace Avalonia.Media luminosityColor = Blend(luminosityColor, new Color(255, TintColor.R, TintColor.G, TintColor.B)); - //var compensationMultiplier = 1 - PlatformTransparencyCompensationLevel; - //return new Color((byte)(255 * Math.Max(Math.Min(PlatformTransparencyCompensationLevel + ( luminosityOpacity.Value * compensationMultiplier), 1.0), 0.0)), luminosityColor.R, luminosityColor.G, luminosityColor.B); - return new Color((byte)(255 * Math.Max(Math.Min((luminosityOpacity.Value), 1.0), 0.0)), luminosityColor.R, luminosityColor.G, luminosityColor.B); + var compensationMultiplier = 1 - PlatformTransparencyCompensationLevel; + return new Color((byte)(255 * Math.Max(Math.Min(PlatformTransparencyCompensationLevel + ( luminosityOpacity.Value * compensationMultiplier), 1.0), 0.0)), luminosityColor.R, luminosityColor.G, luminosityColor.B); } /// From 666892a52f4f2d55123c3ff6e01c641926b613b2 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 13 Jun 2020 15:03:39 -0300 Subject: [PATCH 102/693] transparency level ajustments. --- src/Avalonia.Controls/ExperimentalAcrylicBorder.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs b/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs index a6e7b52cb0..6c283665f1 100644 --- a/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs +++ b/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs @@ -56,11 +56,11 @@ namespace Avalonia.Controls break; case WindowTransparencyLevel.Blur: - Material.PlatformTransparencyCompensationLevel = 0.90; + Material.PlatformTransparencyCompensationLevel = 0.80; break; case WindowTransparencyLevel.AcrylicBlur: - Material.PlatformTransparencyCompensationLevel = 0.70; + Material.PlatformTransparencyCompensationLevel = 0.0; break; } }); From 00db860270782d1963d3dbbf9bcf57ef65f368b7 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 13 Jun 2020 15:16:40 -0300 Subject: [PATCH 103/693] keep color balancing proportional to win ui. --- src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs b/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs index 7d2023cb21..7071df063a 100644 --- a/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs +++ b/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs @@ -180,9 +180,9 @@ namespace Avalonia.Media const double midPoint = 0.5; // Mid point of HsvV range that these calculations are based on. This is here for easy tuning. - const double whiteMaxOpacity = 0.15; // 100% luminosity - const double midPointMaxOpacity = 0.40; // 50% luminosity - const double blackMaxOpacity = 0.40; // 0% luminosity + const double whiteMaxOpacity = 0.2; // 100% luminosity + const double midPointMaxOpacity = 0.45; // 50% luminosity + const double blackMaxOpacity = 0.42; // 0% luminosity var hsv = RgbToHsv(tintColor); From 6b1f62777fe1dac330e00f801f2ac7d77c6ef5b7 Mon Sep 17 00:00:00 2001 From: amwx Date: Thu, 11 Jun 2020 20:03:43 -0500 Subject: [PATCH 104/693] Add DateTimeFormatter class --- .../DateTimePickers/DateTimeFormatter.cs | 335 ++++++++++++++++++ 1 file changed, 335 insertions(+) create mode 100644 src/Avalonia.Controls/DateTimePickers/DateTimeFormatter.cs diff --git a/src/Avalonia.Controls/DateTimePickers/DateTimeFormatter.cs b/src/Avalonia.Controls/DateTimePickers/DateTimeFormatter.cs new file mode 100644 index 0000000000..46b1c07ebc --- /dev/null +++ b/src/Avalonia.Controls/DateTimePickers/DateTimeFormatter.cs @@ -0,0 +1,335 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Avalonia.Controls +{ + /// + /// Formats a DateTimeOffset by the specified pattern + /// Based on, but not replica, the Windows.Globalization.DateTimeFormatting.DateTimeFormatter + /// https://docs.microsoft.com/en-us/uwp/api/windows.globalization.datetimeformatting.datetimeformatter?view=winrt-19041 + /// + /// Currently only the Gregorian Calendar is supported, and regions and languages are not supported. + /// Formatting timezones is also not supported, that's a large task + /// + /// + /// /// Most formats used in UWP/WinUI are compatible here, but not all + /// This DateTimeFormatter will also work with TimeSpans (only patterns though), + /// in addition to the default DateTime/DateTimeOffset + /// + /// + /// Formats are broken down into Patterns and Templates, which are "complete" patterns + /// If a Template is used, only 1 may be provided and cannot be mixed with anything else + /// Multiple patterns can be specified, and can be mixed with other text. All patterns + /// must be enclosed in curly braces {}, e.g. {dayofweek}, or in xaml "{}{dayofweek}" + /// NOTE: Formats and Templates are case sensitive + /// + /// + public sealed class DateTimeFormatter + { + public DateTimeFormatter(string formatString) + { + _format = formatString; + var reg = new Regex("{([^{}]*)}"); + var results = reg.Matches(formatString).Cast().Select(m => m.Groups[1].Value).Distinct().ToList(); + Formats = results; + } + + public string Clock + { + get + { + if (_clock == null) + { + var timePattern = CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern; + if (timePattern.IndexOf("H") != -1) + return "24HourClock"; + return "12HourClock"; + } + return _clock; + } + set + { + _clock = value; + } + } + public List Formats { get; } + + /// + /// Formats a DateTimeOffset object by the format of the + /// + public string Format(DateTimeOffset toFormat) + { + string ret = _format; + + if (Formats.Count == 0) + { + return GetFormatTemplate(toFormat); + } + + foreach (var item in Formats) + { + var p = $"{{{item}}}"; + var ns = Regex.Replace(ret, $"{{{Regex.Escape(item)}}}", GetFormatValue(item, toFormat)); + ret = ns; + } + + return ret; + } + + /// + /// Formats a TimeSpan object by the format of the + /// + public string Format(TimeSpan toFormat) + { + string ret = _format; + + foreach (var item in Formats) + { + var p = $"{{{item}}}"; + var ns = Regex.Replace(ret, $"{{{Regex.Escape(item)}}}", GetFormatValue(item, toFormat)); + ret = ns; + } + + return ret; + } + + private string GetFormatValue(string pattern, DateTimeOffset dt) + { + var sp = pattern.Split(new[] { "." }, StringSplitOptions.None); + var type = sp[0].Trim(); + var desc = sp[1].Trim(); + var len = desc.Contains("(") ? int.Parse(desc.Substring(desc.IndexOf("(") + 1, desc.Length - desc.IndexOf(")"))) : -1; + if (type.Equals("era")) + { + return ""; + } + else if (type.Equals("year")) + { + var yr = dt.Year; + if (len == -1) + return yr.ToString(); + else if (len <= 2) + return yr.ToString().Substring(2); + else + return yr.ToString(); + } + else if (type.Equals("month")) + { + var mon = dt.Month; + var fmt = CultureInfo.CurrentCulture.DateTimeFormat; + if (len == -1) + return desc == "full" ? fmt.GetMonthName(dt.Month) : fmt.GetAbbreviatedMonthName(dt.Month); + var nm = desc == "full" ? fmt.GetMonthName(dt.Month) : fmt.GetAbbreviatedMonthName(dt.Month); + len = Math.Min(nm.Length, Math.Max(0, len)); + return nm.Substring(0, len); + } + else if (type.Equals("dayofweek")) + { + var dow = dt.DayOfWeek; + var fmt = CultureInfo.CurrentCulture.DateTimeFormat; + if (len == -1) + return desc == "full" ? fmt.GetDayName(dt.DayOfWeek) : fmt.GetAbbreviatedDayName(dt.DayOfWeek); + var nm = desc == "full" ? fmt.GetDayName(dt.DayOfWeek) : fmt.GetAbbreviatedDayName(dt.DayOfWeek); + len = Math.Min(nm.Length, Math.Max(0, len)); + return nm.Substring(0, len); + } + else if (type.Equals("day")) + { + var dy = dt.Day; + if (len == -1) + return dy.ToString(); + if (len < 1 || len > 2) + len = 2; + return dy.ToString($"D{len}"); + } + else if (type.Equals("period")) + { + if (Clock == "24HourClock") + return ""; + + var hr = dt.Hour; + if (hr >= 12) + return CultureInfo.CurrentCulture.DateTimeFormat.PMDesignator; + else + return CultureInfo.CurrentCulture.DateTimeFormat.AMDesignator; + } + else if (type.Equals("hour")) + { + var shortTimePattern = CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern.ToLower(); + var resolvedLength = 1; + //valid Hours: h, hh + if (shortTimePattern.Contains("hh")) + { + resolvedLength = 2; + } + var hr = dt.Hour; + + if (Clock == "12HourClock") + hr = hr >= 13 ? hr - 12 : hr; + + if (len == -1) + return hr.ToString($"D{resolvedLength}"); + if (len < 1 || len > 2) + len = resolvedLength; + + return hr.ToString($"D{len}"); + } + else if (type.Equals("minute")) + { + var shortTimePattern = CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern.ToLower(); + var resolvedLength = 1; + //valid mintues: m, mm + if (shortTimePattern.Contains("hh")) + { + resolvedLength = 2; + } + + var dy = dt.Minute; + if (len == -1) + return dy.ToString($"D{resolvedLength}"); + if (len < 1 || len > 2) + len = resolvedLength; + + return dy.ToString($"D{len}"); + } + else if (type.Equals("second")) + { + var shortTimePattern = CultureInfo.CurrentCulture.DateTimeFormat.LongTimePattern.ToLower(); + var resolvedLength = 1; + //valid seconds: s, ss + if (shortTimePattern.Contains("ss")) + { + resolvedLength = 2; + } + + var dy = dt.Second; + if (len == -1) + return dy.ToString($"D{resolvedLength}"); + if (len < 1 || len > 2) + len = resolvedLength; + + return dy.ToString($"D{len}"); + } + else if (type.Equals("timezone")) + { + //Timezones aren't supported yet, that's a HUGE task + //Multiple timezones can exist for a given offset from UTC, + //So info about region is necessary to obtain it + //It'd be nice if MS would move stuff from WinRT globalization namespace to .net + throw new NotSupportedException("Timezones aren't supported yet"); + } + + throw new ArgumentException("Invalid format"); + + } + + private string GetFormatValue(string pattern, TimeSpan ts) + { + var sp = pattern.Split(new[] { "." }, StringSplitOptions.None); + var type = sp[0].Trim(); + var desc = sp.Count() > 1 ? sp[1].Trim() : ""; + var len = desc.Contains("(") ? int.Parse(desc.Substring(desc.IndexOf("(") + 1, desc.Length - desc.IndexOf(")"))) : -1; + + if (type.Equals("hour")) + { + var shortTimePattern = CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern.ToLower(); + var resolvedLength = 1; + //valid Hours: h, hh + if (shortTimePattern.Contains("hh")) + { + resolvedLength = 2; + } + var hr = ts.Hours; + + if (Clock == "12HourClock") + hr = hr >= 13 ? hr - 12 : hr; + + if (len == -1) + return hr.ToString($"D{resolvedLength}"); + if (len < 1 || len > 2) + len = resolvedLength; + + return hr.ToString($"D{len}"); + } + else if (type.Equals("minute")) + { + var shortTimePattern = CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern.ToLower(); + var resolvedLength = 1; + //valid mintues: m, mm + if (shortTimePattern.Contains("hh")) + { + resolvedLength = 2; + } + + var dy = ts.Minutes; + if (len == -1) + return dy.ToString($"D{resolvedLength}"); + if (len < 1 || len > 2) + len = resolvedLength; + + return dy.ToString($"D{len}"); + } + else if (type.Equals("second")) + { + var shortTimePattern = CultureInfo.CurrentCulture.DateTimeFormat.LongTimePattern.ToLower(); + var resolvedLength = 1; + //valid seconds: s, ss + if (shortTimePattern.Contains("ss")) + { + resolvedLength = 2; + } + + var dy = ts.Seconds; + if (len == -1) + return dy.ToString($"D{resolvedLength}"); + if (len < 1 || len > 2) + len = resolvedLength; + + return dy.ToString($"D{len}"); + } + else if (type.Equals("period")) + { + if (Clock == "24HourClock") + return ""; + + var hr = ts.Hours; + if (hr >= 12) + return CultureInfo.CurrentCulture.DateTimeFormat.PMDesignator; + else + return CultureInfo.CurrentCulture.DateTimeFormat.AMDesignator; + } + + throw new ArgumentException("Invalid format"); + + } + + private string GetFormatTemplate(DateTimeOffset dt) + { + switch (_format) + { + case "longdate": + return dt.ToString(CultureInfo.CurrentCulture.DateTimeFormat.LongDatePattern); + case "shortdate": + return dt.ToString(CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern); + case "longtime": + return dt.ToString(CultureInfo.CurrentCulture.DateTimeFormat.LongTimePattern); + case "shorttime": + return dt.ToString(CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern); + case "iso8601": + case "sortable": + return dt.ToString(CultureInfo.CurrentCulture.DateTimeFormat.SortableDateTimePattern); + case "universalsortable": + return dt.ToString(CultureInfo.CurrentCulture.DateTimeFormat.UniversalSortableDateTimePattern); + case "rfc1123": + return dt.ToUniversalTime().ToString(CultureInfo.CurrentCulture.DateTimeFormat.RFC1123Pattern); + } + throw new ArgumentException("Invalid template"); + } + + private string _format; + private string _clock; + } +} From 427e1f62bab81c78b347780aaea0a7b6ab7f9729 Mon Sep 17 00:00:00 2001 From: amwx Date: Thu, 11 Jun 2020 20:43:03 -0500 Subject: [PATCH 105/693] Add LoopingSelector control --- .../DateTimePickers/LoopingPanel.cs | 282 +++++ .../DateTimePickers/LoopingSelector.cs | 641 +++++++++++ .../DateTimePickers/LoopingSelectorItem.cs | 98 ++ .../DateTimePickers/DateTimeFormatter.cs | 335 ++++++ .../DateTimePickers/LoopingSelector.cs | 10 + .../Templates/NumericUpDown/NumericUpDown.cs | 1020 +++++++++++++++++ .../NumericUpDownValueChangedEventArgs.cs | 16 + 7 files changed, 2402 insertions(+) create mode 100644 src/Avalonia.Controls/DateTimePickers/LoopingPanel.cs create mode 100644 src/Avalonia.Controls/DateTimePickers/LoopingSelector.cs create mode 100644 src/Avalonia.Controls/DateTimePickers/LoopingSelectorItem.cs create mode 100644 src/Avalonia.Controls/Templates/DateTimePickers/DateTimeFormatter.cs create mode 100644 src/Avalonia.Controls/Templates/DateTimePickers/LoopingSelector.cs create mode 100644 src/Avalonia.Controls/Templates/NumericUpDown/NumericUpDown.cs create mode 100644 src/Avalonia.Controls/Templates/NumericUpDown/NumericUpDownValueChangedEventArgs.cs diff --git a/src/Avalonia.Controls/DateTimePickers/LoopingPanel.cs b/src/Avalonia.Controls/DateTimePickers/LoopingPanel.cs new file mode 100644 index 0000000000..0709df7cf1 --- /dev/null +++ b/src/Avalonia.Controls/DateTimePickers/LoopingPanel.cs @@ -0,0 +1,282 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using System; + +namespace Avalonia.Controls.Primitives +{ + /// + /// Defines the Panel used by the + /// + public sealed class LoopingPanel : Panel, ILogicalScrollable + { + public LoopingPanel(LoopingSelector owner) + { + _owner = owner; + } + + protected override Size MeasureOverride(Size availableSize) + { + //It's assumed here that availableSize will have finite values + //If used in the DatePickerPresenter or TimePickerPresenter, this + //is met and works fine. If used elsewhere, ensure this is met + //Width is required for the Items & height is required for the viewportsize + if (double.IsInfinity(availableSize.Width) || double.IsInfinity(availableSize.Height)) + throw new InvalidOperationException("LoopingPanel needs finite bounds"); + + //For the measure pass, remember we only have a subset of the total items + //available. So we need to ask the LoopingSelector for it's Item count + //return a size based on the extent of all items + + var itmHgt = _owner.ItemHeight; + var itemCt = _owner.ItemCount; + var children = Children; + + var itemWid = availableSize.Width; + for (int i = 0; i < children.Count; i++) + { + //Ensure we have a proper size when measuring + (children[i] as LoopingSelectorItem).Width = itemWid; + (children[i] as LoopingSelectorItem).Height = itmHgt; + children[i].Measure(availableSize); + } + + var hei = itmHgt * itemCt; + + if (_owner.ShouldLoop) + { + //WinUI preps for somewhere around 1000 items? in loop mode, then positions the offset in the middle + //based on the SelectedItem index, that way scrolling is enabled in both directions + //Here we prep for 10 * ItemCount & position in middle to start + _extent = new Size(0, 10 * (itmHgt * itemCt) + (availableSize.Height - itmHgt)); + _viewport = new Size(0, availableSize.Height); + + if (!_hasInitLoop) + { + var selIndex = _owner.SelectedIndex; + selIndex = selIndex == -1 ? 0 : selIndex; + + //We know we are measuring for 10x items, + //so our init index is the selecteditems' index * 5 + if (double.IsNaN(initOffset)) + { + _offset = new Vector(0, (selIndex * itmHgt) * 5); + } + else + { + _offset = new Vector(0, initOffset); + initOffset = double.NaN; + } + _hasInitLoop = true; + } + } + else + { + //SelectedItem is in the middle of the LoopingSelector, so we need to account for that + //so all items can end up in this position + _extent = new Size(0, hei + (availableSize.Height - itmHgt)); + _viewport = new Size(0, availableSize.Height); + + if (!double.IsNaN(initOffset)) + { + _offset = new Vector(0, initOffset); + initOffset = double.NaN; + } + } + + //Total items visible, whether fully or partially visible + _totalItemsInViewport = (int)Math.Ceiling(_viewport.Height / itmHgt); + + RaiseScrollInvalidated(null); + return _extent; + } + + protected override Size ArrangeOverride(Size finalSize) + { + if (_owner == null || Children.Count == 0) + return base.ArrangeOverride(finalSize); + + var itemWid = finalSize.Width; + var itemHgt = _owner.ItemHeight; + var initY = (finalSize.Height / 2.0) - (itemHgt / 2.0); + var children = Children; + var offY = Offset.Y; + + //Item arranging: SelectedItem is placed in the middle of the viewport + //There are _totalItemsInViewport above & below + //In default behavior of Date/Time Picker, 9 items are visible in viewport, + //so 19 items are arrange, 9 above, 9 below, and the selected item inbetween + //The exception is if we are NOT looping & we're near the ends of the available + //items, then we only have what's left before/after selecteditem visible + if (_owner.ShouldLoop) + { + //Correct our starting Y for not starting in the middle when looping + initY -= (itemHgt * _totalItemsInViewport); + + //Sort of limitation when looping, we just place the containers & + //swap the content. With logical scrolling enabled, and the way its + //handled in Avalonia currently (both pointer wheel & gestures) + //you won't notice a difference. Though this may need updating later + //if this changes + Rect rc; + for (int i = 0; i < children.Count; i++) + { + rc = new Rect(0, initY, itemWid, itemHgt); + children[i].Arrange(rc); + initY += itemHgt; + } + } + else + { + int firstIndex = Math.Max(0, _owner.SelectedIndex - _totalItemsInViewport); + Rect rc; + for (int i = 0; i < children.Count; i++) + { + rc = new Rect(0, initY - offY + firstIndex * itemHgt, itemWid, itemHgt); + children[i].Arrange(rc); + initY += itemHgt; + } + } + + return new Size(itemWid, _extent.Height); + } + + public bool CanHorizontallyScroll { get => false; set => _ = value; } + + public bool CanVerticallyScroll { get => true; set => _ = value; } + + public bool IsLogicalScrollEnabled => true; + + public Size ScrollSize => new Size(0, _owner?.ItemHeight ?? 32); + + //4 items + public Size PageScrollSize => new Size(0, _owner?.ItemHeight * 4 ?? 128); + + public Size Extent => _extent; + + public Vector Offset + { + get => _offset; + set + { + //If we try setting the offset before we've intialized + //store the value now & set it when MeasureOverride is called + if (Extent.Height == 0) + { + initOffset = value.Y; + return; + } + + var old = _offset; + _offset = value; + + if (Children.Count == 0) + return; + + _owner.SetSelectedIndexFromOffset(value.Y); + + var itemHgt = _owner.ItemHeight; + var totalItemCount = _owner.ItemCount; + var initY = (Bounds.Height / 2) - (itemHgt / 2); + + if (_owner.ShouldLoop) + { + //If we're looping, we need to detect when we're approaching + //the min/max bounds of the scrollviewer & reset to the otherside + //to make sure we always have scrolling + //To do this, since we plan for 10x total items, we move if we're + //in the first or last "block" of items & return it to somewhere near the middle + if (value.Y > old.Y) //Scrolling Down + { + var extentOne = totalItemCount * itemHgt; + var scrollableHeight = (_extent.Height - _viewport.Height); + if (value.Y >= scrollableHeight - extentOne) + _offset = new Vector(0, value.Y - (extentOne * 5)); + } + else if (value.Y < old.Y) //Scrolling Up + { + var extentOne = totalItemCount * itemHgt; + var scrollableHeight = (_extent.Height - _viewport.Height); + if (value.Y < extentOne) + _offset = new Vector(0, value.Y + (extentOne * 5)); + } + + firstIndex = _owner.SelectedIndex - _totalItemsInViewport; + } + else + { + + var numItemsAboveSelected = (int)Math.Ceiling(initY / itemHgt); + int logicalOffset = (int)(_offset.Y / itemHgt); + + firstIndex = Math.Max(0, Math.Min(logicalOffset - numItemsAboveSelected, totalItemCount)); + + //When not looping, we actually move the containers, so if we get one + //out of bounds, recycle it to the other side + if (_offset.Y > old.Y) + { + var recycleThreshold = initY - (_totalItemsInViewport * itemHgt); + + //ScrollDown + var ct = Children.Count; + for (int i = ct - 1; i >= 0; i--) + { + if (Children[i].Bounds.Bottom <= recycleThreshold) + { + Children.Move(i, ct - 1); + } + } + } + else if (_offset.Y < old.Y) + { + var recycleThreshold = (initY + itemHgt) + (_totalItemsInViewport * itemHgt); + //ScrollUp + var ct = Children.Count; + var bottom = Bounds.Height; + for (int i = ct - 1; i >= 0; i--) + { + if (Children[i].Bounds.Top >= recycleThreshold) + { + Children.Move(i, 0); + } + } + } + } + + RaiseScrollInvalidated(EventArgs.Empty); + InvalidateArrange(); + } + } + + public Size Viewport => _viewport; + + //Not used + public bool BringIntoView(IControl target, Rect targetRect) + { + return false; + } + + //Not used + public IControl GetControlInDirection(NavigationDirection direction, IControl from) + { + return null; + } + + public void RaiseScrollInvalidated(EventArgs e) + { + ScrollInvalidated?.Invoke(this, e); + } + + private double initOffset = double.NaN; + int firstIndex = 0; + private LoopingSelector _owner; + private Size _extent; + private Size _viewport; + private Vector _offset; + private bool _hasInitLoop; + private int _totalItemsInViewport; + + public event EventHandler ScrollInvalidated; + } +} diff --git a/src/Avalonia.Controls/DateTimePickers/LoopingSelector.cs b/src/Avalonia.Controls/DateTimePickers/LoopingSelector.cs new file mode 100644 index 0000000000..283edf9df7 --- /dev/null +++ b/src/Avalonia.Controls/DateTimePickers/LoopingSelector.cs @@ -0,0 +1,641 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using Avalonia.Controls.Utils; +using Avalonia.Input; +using Avalonia.Interactivity; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; + +namespace Avalonia.Controls.Primitives +{ + + public delegate void SelectionChangedEventHandler(object sender, SelectionChangedEventArgs e); + + /// + /// An items control with the ability for infinite looping + /// + /// Supports UI virtualization, though doesn't inherit from ItemsControl so we manage containers and + /// realized items ourselves + /// + /// + /// In WinUI, this control isn't usable outside of the Date/Time Pickers, but here it technically can be + /// Note, it's default behavior is for the pickers though. + /// + /// + public class LoopingSelector : TemplatedControl + { + public LoopingSelector() + { + _panel = new LoopingPanel(this); + LogicalChildren.Add(_panel); + AddHandler(LoopingSelectorItem.SelectedEvent, OnItemSelected); + this.GetObservable(BoundsProperty).Subscribe(x => OnBoundsChanged(x)); + } + + static LoopingSelector() + { + FocusableProperty.OverrideDefaultValue(true); + ItemsProperty.Changed.AddClassHandler((x, v) => x.OnItemsChanged(v)); + } + + /// + /// Defines the Property + /// + public static readonly DirectProperty ItemsProperty = + AvaloniaProperty.RegisterDirect("Items", + x => x.Items, (x, v) => x.Items = v); + + /// + /// Defines the Property + /// + public static readonly DirectProperty ItemCountProperty = + AvaloniaProperty.RegisterDirect("ItemCount", + x => x.ItemCount); + + /// + /// Defines the Property + /// + public static readonly DirectProperty SelectedItemProperty = + AvaloniaProperty.RegisterDirect("SelectedItem", + x => x.SelectedItem, (x, v) => x.SelectedItem = v); + + /// + /// Defines the Property + /// + public static readonly DirectProperty SelectedIndexProperty = + AvaloniaProperty.RegisterDirect("SelectedIndex", + x => x.SelectedIndex, (x, v) => x.SelectedIndex = v); + + //UWP/WinUI has ItemWidth, ignoring, will have items just fill the width of the container + + /// + /// Defines the Property + /// + public static readonly DirectProperty ItemHeightProperty = + AvaloniaProperty.RegisterDirect("ItemHeight", + x => x.ItemHeight, (x, v) => x.ItemHeight = v); + + /// + /// Defines the Property + /// + public static readonly StyledProperty ItemTemplateProperty = + AvaloniaProperty.Register("ItemTemplate"); + + /// + /// Defines the Property + /// + public static readonly DirectProperty ShouldLoopProperty = + AvaloniaProperty.RegisterDirect("ShouldLoop", + x => x.ShouldLoop, (x, v) => x.ShouldLoop = v); + + /// + /// Gets or sets the Items + /// + public IEnumerable Items + { + get => _items; + set => SetAndRaise(ItemsProperty, ref _items, value); + } + + /// + /// Gets the number of items + /// + public int ItemCount + { + get => _itemCount; + private set => SetAndRaise(ItemCountProperty, ref _itemCount, value); + } + + /// + /// Gets or sets the SelectedIndex + /// + public int SelectedIndex + { + get => _selectedIndex; + set + { + if (Items == null || ItemCount == 0) + return; + var old = _selectedIndex; + SetAndRaise(SelectedIndexProperty, ref _selectedIndex, value); + + var oldItem = _selectedItem; + if (value == -1) + { + _selectedItem = null; + } + else + { + if (Items is IList l) + _selectedItem = l[value]; + else + _selectedItem = Items.ElementAt(value); + } + RaisePropertyChanged(SelectedItemProperty, oldItem, _selectedItem); + if (!_preventMovingScrollWhenSelecting) + UpdateOffset(); + + SelectionChangedEventArgs args = new SelectionChangedEventArgs(null, new object[] { oldItem }, new object[] { _selectedItem }); + OnSelectionChanged(this, args); + } + } + + /// + /// Gets or sets the SelectedItem + /// + public object SelectedItem + { + get + { + if (SelectedIndex == -1) + return null; + if (Items is IList l) + return l[SelectedIndex]; + else + return Items.ElementAt(SelectedIndex); + } + set + { + if (value == null) + { + SelectedIndex = -1; + } + else + { + if (Items is IList l) + SelectedIndex = l.IndexOf(value); + else + SelectedIndex = Items.IndexOf(value); + } + } + } + + /// + /// Gets or sets the height of the items + /// + public double ItemHeight + { + get => _itemHeight; + set + { + SetAndRaise(ItemHeightProperty, ref _itemHeight, value); + } + } + + /// + /// Gets or sets the item template + /// + public IDataTemplate ItemTemplate + { + get => GetValue(ItemTemplateProperty); + set => SetValue(ItemTemplateProperty, value); + } + + /// + /// Gets or sets whether the items should loop + /// + public bool ShouldLoop + { + get => _shouldLoop; + set + { + SetAndRaise(ShouldLoopProperty, ref _shouldLoop, value); + } + } + + /// + /// Raised when the SelectedItem/Index changes + /// + public event SelectionChangedEventHandler SelectionChanged; + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + _scroller = e.NameScope.Find("Scroller"); + + _scroller.Content = _panel; + + _upButton = e.NameScope.Find("UpButton"); + if (_upButton != null) + { + _upButton.Click += OnUpButtonClick; + } + + _downButton = e.NameScope.Find("DownButton"); + if (_downButton != null) + { + _downButton.Click += OnDownButtonClick; + } + } + + protected override void OnKeyDown(KeyEventArgs e) + { + switch (e.Key) + { + case Key.Up: + if (ShouldLoop) + { + var selIndex = SelectedIndex; + selIndex--; + if (selIndex < 0) + selIndex += ItemCount; + SelectedIndex = selIndex; + } + else + { + SelectedIndex = Math.Max(0, SelectedIndex - 1); + } + e.Handled = true; + break; + case Key.Down: + if (ShouldLoop) + { + var selIndex = SelectedIndex; + selIndex++; + if (selIndex >= ItemCount) + selIndex -= ItemCount; + SelectedIndex = selIndex; + } + else + { + SelectedIndex = Math.Min(ItemCount, SelectedIndex + 1); + } + e.Handled = true; + break; + case Key.PageUp: + if (ShouldLoop) + { + var selIndex = SelectedIndex; + selIndex -= 4; + if (selIndex < 0) + selIndex += ItemCount; + SelectedIndex = selIndex; + } + else + { + SelectedIndex = Math.Max(0, SelectedIndex - 4); + } + e.Handled = true; + break; + } + base.OnKeyDown(e); + } + + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + base.OnPointerPressed(e); + KeyboardDevice.Instance.SetFocusedElement(this, NavigationMethod.Pointer, KeyModifiers.None); + FocusManager.Instance.Focus(this, NavigationMethod.Pointer, KeyModifiers.None); + } + + private void OnDownButtonClick(object sender, RoutedEventArgs e) + { + var selIndex = SelectedIndex; + if (selIndex == ItemCount - 1) + SelectedIndex = 0; + else + SelectedIndex++; + e.Handled = true; + } + + private void OnUpButtonClick(object sender, RoutedEventArgs e) + { + var selIndex = SelectedIndex; + if (selIndex == 0) + SelectedIndex = ItemCount - 1; + else + SelectedIndex--; + e.Handled = true; + } + + private void OnItemsChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.OldValue is INotifyCollectionChanged oldC) + { + oldC.CollectionChanged -= OnItemsCollectionChanged; + } + if (e.NewValue is INotifyCollectionChanged newC) + { + newC.CollectionChanged += OnItemsCollectionChanged; + } + + //When the entire items list changes, reset selection quietly + _selectedIndex = -1; + _selectedItem = null; + + if (Items is IList l) + { + ItemCount = l.Count; + } + else + { + ItemCount = Items.Count(); + } + + EnsureContainers(); + UpdateOffset(); + } + + private void OnItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + ItemCount += e.NewItems.Count; + var index = e.NewStartingIndex; + //if (IsContainerIndexLoaded(index)) + //{ + // AddContainers(index, e.NewItems); + //} + EnsureContainers(); + UpdateOffset(); + break; + case NotifyCollectionChangedAction.Remove: + ItemCount -= e.OldItems.Count; + EnsureContainers(); + UpdateOffset(); + break; + case NotifyCollectionChangedAction.Reset: + _selectedIndex = 0; + _selectedItem = 0; + ItemCount = 0; + _panel.Children.Clear(); + UpdateOffset(); + break; + + //TODO - items source should be static anyway + case NotifyCollectionChangedAction.Replace: + case NotifyCollectionChangedAction.Move: + throw new NotSupportedException("Can't move or replace items in ItemsCollection"); + } + } + + /// + /// Ensures we have the correct number of containers in the LoopingSelectorPanel + /// This will add, remove, or clear the panel as necessary + /// + private void EnsureContainers() + { + if (Bounds.Height == 0) + return; + + int itemCount = ItemCount; + //How many containers we ideally want + int desiredItemsLoaded = (_totalItemsInViewport * 2) + 1; + //How many we can actually load + int itemsToLoad = itemCount < desiredItemsLoaded ? itemCount : desiredItemsLoaded; + + var realizedContainerCount = _panel.Children.Count; + + if (ShouldLoop) + { + //When looping we should ALWAYS have desiredItemsLoaded number of containers + //available + var delta = Math.Abs(realizedContainerCount - desiredItemsLoaded); + if (realizedContainerCount < desiredItemsLoaded) //Add more containers + { + List panelItems = new List(); + for (int i = 0; i < delta; i++) + { + LoopingSelectorItem lsi = new LoopingSelectorItem(); + lsi.Height = ItemHeight; + lsi.ContentTemplate = ItemTemplate; + panelItems.Add(lsi); + } + _panel.Children.AddRange(panelItems); + } + else if (realizedContainerCount > desiredItemsLoaded) //Remove extra containers + { + _panel.Children.RemoveRange(realizedContainerCount - delta, delta); + } + } + else + { + //When we're not looping, things are a little trickier, if we're near the bounds of scrolling, + //We may not have desiredItemsLoaded containers, so we need to account for that + //NumContainers here should be in range [_totalItemsInViewport, desiredItemsLoaded] + + //First index of realized items + int selIndex = SelectedIndex; + selIndex = selIndex == -1 ? 0 : selIndex; + //TODO: Clean this up... + int numItemsAboveSelected = _totalItemsInViewport; + if (selIndex - numItemsAboveSelected < 0) + numItemsAboveSelected = selIndex; + + int numItemsBelowSelected = _totalItemsInViewport; + if (selIndex + _totalItemsInViewport >= ItemCount) + numItemsBelowSelected = ItemCount - selIndex - 1; + + int neededContainers = numItemsBelowSelected + numItemsAboveSelected + 1; + int desiredContainers = (_totalItemsInViewport * 2) + 1; + int currentCount = _panel.Children.Count; + + + //Do we need containers? + var numContsToAddRemove = neededContainers - currentCount; + // Debug.WriteLine($"Above: {numItemsAboveSelected} | Below: {numItemsBelowSelected} | Add/Remove: {numContsToAddRemove}"); + + if (numContsToAddRemove > 0) //Add Containers + { + List panelItems = new List(); + + for (int i = 0; i < numContsToAddRemove; i++) + { + LoopingSelectorItem lsi = new LoopingSelectorItem(); + lsi.Height = ItemHeight; + lsi.ContentTemplate = ItemTemplate; + panelItems.Add(lsi); + } + _panel.Children.AddRange(panelItems); + } + else if (numContsToAddRemove < 0) //Remove containers + { + numContsToAddRemove = Math.Abs(numContsToAddRemove); + _panel.Children.RemoveRange(currentCount - numContsToAddRemove, numContsToAddRemove); + //Debug.WriteLine($"Containers removed {numContsToAddRemove}"); + + } + } + + if (ItemCount > 0) + SetItemContent(); + } + + /// + /// Sets the content of the loaded containers + /// + private void SetItemContent() + { + int itemCount = ItemCount; + + if (ShouldLoop) + { + var selIndex = SelectedIndex; + var panelItems = _panel.Children; + + int c = 0; + int index = selIndex == -1 ? -_totalItemsInViewport : selIndex - _totalItemsInViewport; + + while (c < panelItems.Count) + { + if (index >= ItemCount) + index -= ItemCount; + if (index < 0) + index += ItemCount; + + if (index == selIndex) + (panelItems[c] as LoopingSelectorItem).IsSelected = true; + else + (panelItems[c] as LoopingSelectorItem).IsSelected = false; + + (panelItems[c] as LoopingSelectorItem).Content = GetElementAt(index); + c++; + index++; + } + } + else + { + //We first need the first item in the realized items... + var selIndex = SelectedIndex; + var firstIndex = Math.Max(0, selIndex - _totalItemsInViewport); + var panelItems = _panel.Children; + + for (int i = 0; i < panelItems.Count; i++) + { + if (firstIndex == selIndex) + (panelItems[i] as LoopingSelectorItem).IsSelected = true; + else + (panelItems[i] as LoopingSelectorItem).IsSelected = false; + + (panelItems[i] as LoopingSelectorItem).Content = GetElementAt(firstIndex); + firstIndex++; + } + + } + } + + private object GetElementAt(int index) + { + if (index < 0 || index >= ItemCount) + return null; + + if (Items is IList l) + return l[index]; + else + return Items.Cast().ToList()[index]; + } + + /// + /// Updates the scrollviewer offset when the selectedindex changed + /// + private void UpdateOffset() + { + if (_panel == null || ItemCount == 0) + return; + + _preventUpdateSelection = true; + + if (ShouldLoop) + { + //We measure for 10x as many items, so when we set the SelectedIndex + //and need to change the offset, should set it towards the middle + //so we preserve scrolling in both directions + int selIndex = SelectedIndex; + selIndex = selIndex == -1 ? 0 : selIndex; + var extent = ItemCount * ItemHeight; + _panel.Offset = new Vector(0, (selIndex * ItemHeight) + (extent * 5)); + + } + else + { + //Not looping, just convert the SelectedIndex to an offset + //if -1, set to 0; + int selIndex = SelectedIndex; + if (ItemCount == 0 || SelectedIndex == -1) + _panel.Offset = new Vector(0, 0); + else + _panel.Offset = new Vector(0, selIndex * ItemHeight); + } + + EnsureContainers(); + + _preventUpdateSelection = false; + } + + /// + /// Updates the SelectedIndex when scrolling occurs + /// + /// + internal void SetSelectedIndexFromOffset(double offsetY) + { + if (_preventUpdateSelection) + return; + + _preventMovingScrollWhenSelecting = true; + + if (ShouldLoop) + { + var extent = ItemCount * ItemHeight; + var numExtents = offsetY / extent; + numExtents = numExtents < 0 ? 0 : Math.Truncate(numExtents); + var pixelOffset = offsetY - extent * numExtents; + + SelectedIndex = (int)(pixelOffset / ItemHeight); + } + else + { + SelectedIndex = (int)(offsetY / ItemHeight); + } + + EnsureContainers(); + + _preventMovingScrollWhenSelecting = false; + } + + private void OnItemSelected(object sender, RoutedEventArgs e) + { + var item = (e.Source as LoopingSelectorItem).Content; + SelectedItem = item; + } + + protected virtual void OnSelectionChanged(object sender, SelectionChangedEventArgs e) + { + SelectionChanged?.Invoke(sender, e); + } + + private void OnBoundsChanged(Rect x) + { + //Ideally we always want this to be odd, since the selected item is placed in the middle, + //so we have the same number of items above and below at all times + _totalItemsInViewport = (int)Math.Ceiling(x.Height / ItemHeight); + if (_totalItemsInViewport % 2 == 0) + _totalItemsInViewport += 1; + + EnsureContainers(); + } + + + //TemplateItems + private RepeatButton _downButton; + private RepeatButton _upButton; + private ScrollViewer _scroller; + + private LoopingPanel _panel; + + private int _totalItemsInViewport; + private IEnumerable _items; + private int _itemCount; + private int _selectedIndex = -1; + private object _selectedItem; + private double _itemHeight = 32; + private bool _shouldLoop = true; + private bool _preventUpdateSelection; + private bool _preventMovingScrollWhenSelecting; + } +} diff --git a/src/Avalonia.Controls/DateTimePickers/LoopingSelectorItem.cs b/src/Avalonia.Controls/DateTimePickers/LoopingSelectorItem.cs new file mode 100644 index 0000000000..8e1d242561 --- /dev/null +++ b/src/Avalonia.Controls/DateTimePickers/LoopingSelectorItem.cs @@ -0,0 +1,98 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using System; + +namespace Avalonia.Controls.Primitives +{ + /// + /// Defines the containers used by the + /// + public sealed class LoopingSelectorItem : ContentControl + { + static LoopingSelectorItem() + { + IsPressedProperty.Changed.AddClassHandler((x, e) => x.OnIsPressedChanged(e)); + IsSelectedProperty.Changed.AddClassHandler((x, e) => x.OnIsSelectedChanged(e)); + } + + /// + /// Defines the Property + /// + public static readonly StyledProperty IsPressedProperty = + AvaloniaProperty.Register("IsPressed"); + + /// + /// Defines the Property + /// + internal static readonly StyledProperty IsSelectedProperty = + AvaloniaProperty.Register("IsSelected"); + + /// + /// Gets whether the item is currently pressed + /// + public bool IsPressed + { + get => GetValue(IsPressedProperty); + private set => SetValue(IsPressedProperty, value); + } + + internal bool IsSelected + { + get => GetValue(IsSelectedProperty); + set => SetValue(IsSelectedProperty, value); + } + + private void OnIsPressedChanged(AvaloniaPropertyChangedEventArgs e) + { + PseudoClasses.Set(":pressed", (bool)e.NewValue); + } + + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + base.OnPointerPressed(e); + if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) + { + IsPressed = true; + } + } + + protected override void OnPointerMoved(PointerEventArgs e) + { + base.OnPointerMoved(e); + if (IsPressed) + { + var pt = e.GetPosition(this); + if (pt.X < 0 || pt.Y < 0 || pt.X > Bounds.Width || pt.Y > Bounds.Height) + IsPressed = false; + } + } + + protected override void OnPointerReleased(PointerReleasedEventArgs e) + { + base.OnPointerReleased(e); + if (IsPressed && e.GetCurrentPoint(this).Properties.PointerUpdateKind == PointerUpdateKind.LeftButtonReleased) + { + IsPressed = false; + //The selection event only raises when invoked by pointer events + RaiseEvent(new RoutedEventArgs(SelectedEvent, this)); + } + } + + private void OnIsSelectedChanged(AvaloniaPropertyChangedEventArgs e) + { + var newValue = (bool)e.NewValue; + PseudoClasses.Set(":selected", newValue); + } + + public static readonly RoutedEvent SelectedEvent = + RoutedEvent.Register("Selected", RoutingStrategies.Bubble); + + public event EventHandler Selected + { + add => AddHandler(SelectedEvent, value); + remove => RemoveHandler(SelectedEvent, value); + } + } +} diff --git a/src/Avalonia.Controls/Templates/DateTimePickers/DateTimeFormatter.cs b/src/Avalonia.Controls/Templates/DateTimePickers/DateTimeFormatter.cs new file mode 100644 index 0000000000..46b1c07ebc --- /dev/null +++ b/src/Avalonia.Controls/Templates/DateTimePickers/DateTimeFormatter.cs @@ -0,0 +1,335 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Avalonia.Controls +{ + /// + /// Formats a DateTimeOffset by the specified pattern + /// Based on, but not replica, the Windows.Globalization.DateTimeFormatting.DateTimeFormatter + /// https://docs.microsoft.com/en-us/uwp/api/windows.globalization.datetimeformatting.datetimeformatter?view=winrt-19041 + /// + /// Currently only the Gregorian Calendar is supported, and regions and languages are not supported. + /// Formatting timezones is also not supported, that's a large task + /// + /// + /// /// Most formats used in UWP/WinUI are compatible here, but not all + /// This DateTimeFormatter will also work with TimeSpans (only patterns though), + /// in addition to the default DateTime/DateTimeOffset + /// + /// + /// Formats are broken down into Patterns and Templates, which are "complete" patterns + /// If a Template is used, only 1 may be provided and cannot be mixed with anything else + /// Multiple patterns can be specified, and can be mixed with other text. All patterns + /// must be enclosed in curly braces {}, e.g. {dayofweek}, or in xaml "{}{dayofweek}" + /// NOTE: Formats and Templates are case sensitive + /// + /// + public sealed class DateTimeFormatter + { + public DateTimeFormatter(string formatString) + { + _format = formatString; + var reg = new Regex("{([^{}]*)}"); + var results = reg.Matches(formatString).Cast().Select(m => m.Groups[1].Value).Distinct().ToList(); + Formats = results; + } + + public string Clock + { + get + { + if (_clock == null) + { + var timePattern = CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern; + if (timePattern.IndexOf("H") != -1) + return "24HourClock"; + return "12HourClock"; + } + return _clock; + } + set + { + _clock = value; + } + } + public List Formats { get; } + + /// + /// Formats a DateTimeOffset object by the format of the + /// + public string Format(DateTimeOffset toFormat) + { + string ret = _format; + + if (Formats.Count == 0) + { + return GetFormatTemplate(toFormat); + } + + foreach (var item in Formats) + { + var p = $"{{{item}}}"; + var ns = Regex.Replace(ret, $"{{{Regex.Escape(item)}}}", GetFormatValue(item, toFormat)); + ret = ns; + } + + return ret; + } + + /// + /// Formats a TimeSpan object by the format of the + /// + public string Format(TimeSpan toFormat) + { + string ret = _format; + + foreach (var item in Formats) + { + var p = $"{{{item}}}"; + var ns = Regex.Replace(ret, $"{{{Regex.Escape(item)}}}", GetFormatValue(item, toFormat)); + ret = ns; + } + + return ret; + } + + private string GetFormatValue(string pattern, DateTimeOffset dt) + { + var sp = pattern.Split(new[] { "." }, StringSplitOptions.None); + var type = sp[0].Trim(); + var desc = sp[1].Trim(); + var len = desc.Contains("(") ? int.Parse(desc.Substring(desc.IndexOf("(") + 1, desc.Length - desc.IndexOf(")"))) : -1; + if (type.Equals("era")) + { + return ""; + } + else if (type.Equals("year")) + { + var yr = dt.Year; + if (len == -1) + return yr.ToString(); + else if (len <= 2) + return yr.ToString().Substring(2); + else + return yr.ToString(); + } + else if (type.Equals("month")) + { + var mon = dt.Month; + var fmt = CultureInfo.CurrentCulture.DateTimeFormat; + if (len == -1) + return desc == "full" ? fmt.GetMonthName(dt.Month) : fmt.GetAbbreviatedMonthName(dt.Month); + var nm = desc == "full" ? fmt.GetMonthName(dt.Month) : fmt.GetAbbreviatedMonthName(dt.Month); + len = Math.Min(nm.Length, Math.Max(0, len)); + return nm.Substring(0, len); + } + else if (type.Equals("dayofweek")) + { + var dow = dt.DayOfWeek; + var fmt = CultureInfo.CurrentCulture.DateTimeFormat; + if (len == -1) + return desc == "full" ? fmt.GetDayName(dt.DayOfWeek) : fmt.GetAbbreviatedDayName(dt.DayOfWeek); + var nm = desc == "full" ? fmt.GetDayName(dt.DayOfWeek) : fmt.GetAbbreviatedDayName(dt.DayOfWeek); + len = Math.Min(nm.Length, Math.Max(0, len)); + return nm.Substring(0, len); + } + else if (type.Equals("day")) + { + var dy = dt.Day; + if (len == -1) + return dy.ToString(); + if (len < 1 || len > 2) + len = 2; + return dy.ToString($"D{len}"); + } + else if (type.Equals("period")) + { + if (Clock == "24HourClock") + return ""; + + var hr = dt.Hour; + if (hr >= 12) + return CultureInfo.CurrentCulture.DateTimeFormat.PMDesignator; + else + return CultureInfo.CurrentCulture.DateTimeFormat.AMDesignator; + } + else if (type.Equals("hour")) + { + var shortTimePattern = CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern.ToLower(); + var resolvedLength = 1; + //valid Hours: h, hh + if (shortTimePattern.Contains("hh")) + { + resolvedLength = 2; + } + var hr = dt.Hour; + + if (Clock == "12HourClock") + hr = hr >= 13 ? hr - 12 : hr; + + if (len == -1) + return hr.ToString($"D{resolvedLength}"); + if (len < 1 || len > 2) + len = resolvedLength; + + return hr.ToString($"D{len}"); + } + else if (type.Equals("minute")) + { + var shortTimePattern = CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern.ToLower(); + var resolvedLength = 1; + //valid mintues: m, mm + if (shortTimePattern.Contains("hh")) + { + resolvedLength = 2; + } + + var dy = dt.Minute; + if (len == -1) + return dy.ToString($"D{resolvedLength}"); + if (len < 1 || len > 2) + len = resolvedLength; + + return dy.ToString($"D{len}"); + } + else if (type.Equals("second")) + { + var shortTimePattern = CultureInfo.CurrentCulture.DateTimeFormat.LongTimePattern.ToLower(); + var resolvedLength = 1; + //valid seconds: s, ss + if (shortTimePattern.Contains("ss")) + { + resolvedLength = 2; + } + + var dy = dt.Second; + if (len == -1) + return dy.ToString($"D{resolvedLength}"); + if (len < 1 || len > 2) + len = resolvedLength; + + return dy.ToString($"D{len}"); + } + else if (type.Equals("timezone")) + { + //Timezones aren't supported yet, that's a HUGE task + //Multiple timezones can exist for a given offset from UTC, + //So info about region is necessary to obtain it + //It'd be nice if MS would move stuff from WinRT globalization namespace to .net + throw new NotSupportedException("Timezones aren't supported yet"); + } + + throw new ArgumentException("Invalid format"); + + } + + private string GetFormatValue(string pattern, TimeSpan ts) + { + var sp = pattern.Split(new[] { "." }, StringSplitOptions.None); + var type = sp[0].Trim(); + var desc = sp.Count() > 1 ? sp[1].Trim() : ""; + var len = desc.Contains("(") ? int.Parse(desc.Substring(desc.IndexOf("(") + 1, desc.Length - desc.IndexOf(")"))) : -1; + + if (type.Equals("hour")) + { + var shortTimePattern = CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern.ToLower(); + var resolvedLength = 1; + //valid Hours: h, hh + if (shortTimePattern.Contains("hh")) + { + resolvedLength = 2; + } + var hr = ts.Hours; + + if (Clock == "12HourClock") + hr = hr >= 13 ? hr - 12 : hr; + + if (len == -1) + return hr.ToString($"D{resolvedLength}"); + if (len < 1 || len > 2) + len = resolvedLength; + + return hr.ToString($"D{len}"); + } + else if (type.Equals("minute")) + { + var shortTimePattern = CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern.ToLower(); + var resolvedLength = 1; + //valid mintues: m, mm + if (shortTimePattern.Contains("hh")) + { + resolvedLength = 2; + } + + var dy = ts.Minutes; + if (len == -1) + return dy.ToString($"D{resolvedLength}"); + if (len < 1 || len > 2) + len = resolvedLength; + + return dy.ToString($"D{len}"); + } + else if (type.Equals("second")) + { + var shortTimePattern = CultureInfo.CurrentCulture.DateTimeFormat.LongTimePattern.ToLower(); + var resolvedLength = 1; + //valid seconds: s, ss + if (shortTimePattern.Contains("ss")) + { + resolvedLength = 2; + } + + var dy = ts.Seconds; + if (len == -1) + return dy.ToString($"D{resolvedLength}"); + if (len < 1 || len > 2) + len = resolvedLength; + + return dy.ToString($"D{len}"); + } + else if (type.Equals("period")) + { + if (Clock == "24HourClock") + return ""; + + var hr = ts.Hours; + if (hr >= 12) + return CultureInfo.CurrentCulture.DateTimeFormat.PMDesignator; + else + return CultureInfo.CurrentCulture.DateTimeFormat.AMDesignator; + } + + throw new ArgumentException("Invalid format"); + + } + + private string GetFormatTemplate(DateTimeOffset dt) + { + switch (_format) + { + case "longdate": + return dt.ToString(CultureInfo.CurrentCulture.DateTimeFormat.LongDatePattern); + case "shortdate": + return dt.ToString(CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern); + case "longtime": + return dt.ToString(CultureInfo.CurrentCulture.DateTimeFormat.LongTimePattern); + case "shorttime": + return dt.ToString(CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern); + case "iso8601": + case "sortable": + return dt.ToString(CultureInfo.CurrentCulture.DateTimeFormat.SortableDateTimePattern); + case "universalsortable": + return dt.ToString(CultureInfo.CurrentCulture.DateTimeFormat.UniversalSortableDateTimePattern); + case "rfc1123": + return dt.ToUniversalTime().ToString(CultureInfo.CurrentCulture.DateTimeFormat.RFC1123Pattern); + } + throw new ArgumentException("Invalid template"); + } + + private string _format; + private string _clock; + } +} diff --git a/src/Avalonia.Controls/Templates/DateTimePickers/LoopingSelector.cs b/src/Avalonia.Controls/Templates/DateTimePickers/LoopingSelector.cs new file mode 100644 index 0000000000..9f9be174a8 --- /dev/null +++ b/src/Avalonia.Controls/Templates/DateTimePickers/LoopingSelector.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Avalonia.Controls.DateTimePickers +{ + class LoopingSelector + { + } +} diff --git a/src/Avalonia.Controls/Templates/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/Templates/NumericUpDown/NumericUpDown.cs new file mode 100644 index 0000000000..aae041071d --- /dev/null +++ b/src/Avalonia.Controls/Templates/NumericUpDown/NumericUpDown.cs @@ -0,0 +1,1020 @@ +using System; +using System.Globalization; +using System.IO; +using System.Linq; +using Avalonia.Controls.Primitives; +using Avalonia.Data; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Threading; +using Avalonia.Utilities; + +namespace Avalonia.Controls +{ + /// + /// Control that represents a TextBox with button spinners that allow incrementing and decrementing numeric values. + /// + public class NumericUpDown : TemplatedControl + { + /// + /// Defines the property. + /// + public static readonly StyledProperty AllowSpinProperty = + ButtonSpinner.AllowSpinProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ButtonSpinnerLocationProperty = + ButtonSpinner.ButtonSpinnerLocationProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty ShowButtonSpinnerProperty = + ButtonSpinner.ShowButtonSpinnerProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly DirectProperty ClipValueToMinMaxProperty = + AvaloniaProperty.RegisterDirect(nameof(ClipValueToMinMax), + updown => updown.ClipValueToMinMax, (updown, b) => updown.ClipValueToMinMax = b); + + /// + /// Defines the property. + /// + public static readonly DirectProperty CultureInfoProperty = + AvaloniaProperty.RegisterDirect(nameof(CultureInfo), o => o.CultureInfo, + (o, v) => o.CultureInfo = v, CultureInfo.CurrentCulture); + + /// + /// Defines the property. + /// + public static readonly StyledProperty FormatStringProperty = + AvaloniaProperty.Register(nameof(FormatString), string.Empty); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IncrementProperty = + AvaloniaProperty.Register(nameof(Increment), 1.0d, coerce: OnCoerceIncrement); + + /// + /// Defines the property. + /// + public static readonly StyledProperty IsReadOnlyProperty = + AvaloniaProperty.Register(nameof(IsReadOnly)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MaximumProperty = + AvaloniaProperty.Register(nameof(Maximum), double.MaxValue, coerce: OnCoerceMaximum); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MinimumProperty = + AvaloniaProperty.Register(nameof(Minimum), double.MinValue, coerce: OnCoerceMinimum); + + /// + /// Defines the property. + /// + public static readonly DirectProperty ParsingNumberStyleProperty = + AvaloniaProperty.RegisterDirect(nameof(ParsingNumberStyle), + updown => updown.ParsingNumberStyle, (updown, style) => updown.ParsingNumberStyle = style); + + /// + /// Defines the property. + /// + public static readonly DirectProperty TextProperty = + AvaloniaProperty.RegisterDirect(nameof(Text), o => o.Text, (o, v) => o.Text = v, + defaultBindingMode: BindingMode.TwoWay); + + /// + /// Defines the property. + /// + public static readonly DirectProperty ValueProperty = + AvaloniaProperty.RegisterDirect(nameof(Value), updown => updown.Value, + (updown, v) => updown.Value = v, defaultBindingMode: BindingMode.TwoWay); + + /// + /// Defines the property. + /// + public static readonly StyledProperty WatermarkProperty = + AvaloniaProperty.Register(nameof(Watermark)); + + private IDisposable _textBoxTextChangedSubscription; + + private double _value; + private string _text; + private bool _internalValueSet; + private bool _clipValueToMinMax; + private bool _isSyncingTextAndValueProperties; + private bool _isTextChangedFromUI; + private CultureInfo _cultureInfo; + private NumberStyles _parsingNumberStyle = NumberStyles.Any; + + /// + /// Gets the Spinner template part. + /// + private Spinner Spinner { get; set; } + + /// + /// Gets the TextBox template part. + /// + private TextBox TextBox { get; set; } + + /// + /// Gets or sets the ability to perform increment/decrement operations via the keyboard, button spinners, or mouse wheel. + /// + public bool AllowSpin + { + get { return GetValue(AllowSpinProperty); } + set { SetValue(AllowSpinProperty, value); } + } + + /// + /// Gets or sets current location of the . + /// + public Location ButtonSpinnerLocation + { + get { return GetValue(ButtonSpinnerLocationProperty); } + set { SetValue(ButtonSpinnerLocationProperty, value); } + } + + /// + /// Gets or sets a value indicating whether the spin buttons should be shown. + /// + public bool ShowButtonSpinner + { + get { return GetValue(ShowButtonSpinnerProperty); } + set { SetValue(ShowButtonSpinnerProperty, value); } + } + + /// + /// Gets or sets if the value should be clipped when minimum/maximum is reached. + /// + public bool ClipValueToMinMax + { + get { return _clipValueToMinMax; } + set { SetAndRaise(ClipValueToMinMaxProperty, ref _clipValueToMinMax, value); } + } + + /// + /// Gets or sets the current CultureInfo. + /// + public CultureInfo CultureInfo + { + get { return _cultureInfo; } + set { SetAndRaise(CultureInfoProperty, ref _cultureInfo, value); } + } + + /// + /// Gets or sets the display format of the . + /// + public string FormatString + { + get { return GetValue(FormatStringProperty); } + set { SetValue(FormatStringProperty, value); } + } + + /// + /// Gets or sets the amount in which to increment the . + /// + public double Increment + { + get { return GetValue(IncrementProperty); } + set { SetValue(IncrementProperty, value); } + } + + /// + /// Gets or sets if the control is read only. + /// + public bool IsReadOnly + { + get { return GetValue(IsReadOnlyProperty); } + set { SetValue(IsReadOnlyProperty, value); } + } + + /// + /// Gets or sets the maximum allowed value. + /// + public double Maximum + { + get { return GetValue(MaximumProperty); } + set { SetValue(MaximumProperty, value); } + } + + /// + /// Gets or sets the minimum allowed value. + /// + public double Minimum + { + get { return GetValue(MinimumProperty); } + set { SetValue(MinimumProperty, value); } + } + + /// + /// Gets or sets the parsing style (AllowLeadingWhite, Float, AllowHexSpecifier, ...). By default, Any. + /// + public NumberStyles ParsingNumberStyle + { + get { return _parsingNumberStyle; } + set { SetAndRaise(ParsingNumberStyleProperty, ref _parsingNumberStyle, value); } + } + + /// + /// Gets or sets the formatted string representation of the value. + /// + public string Text + { + get { return _text; } + set { SetAndRaise(TextProperty, ref _text, value); } + } + + /// + /// Gets or sets the value. + /// + public double Value + { + get { return _value; } + set + { + value = OnCoerceValue(value); + SetAndRaise(ValueProperty, ref _value, value); + } + } + + /// + /// Gets or sets the object to use as a watermark if the is null. + /// + public string Watermark + { + get { return GetValue(WatermarkProperty); } + set { SetValue(WatermarkProperty, value); } + } + + /// + /// Initializes new instance of class. + /// + public NumericUpDown() + { + Initialized += (sender, e) => + { + if (!_internalValueSet && IsInitialized) + { + SyncTextAndValueProperties(false, null, true); + } + + SetValidSpinDirection(); + }; + } + + /// + /// Initializes static members of the class. + /// + static NumericUpDown() + { + CultureInfoProperty.Changed.Subscribe(OnCultureInfoChanged); + FormatStringProperty.Changed.Subscribe(FormatStringChanged); + IncrementProperty.Changed.Subscribe(IncrementChanged); + IsReadOnlyProperty.Changed.Subscribe(OnIsReadOnlyChanged); + MaximumProperty.Changed.Subscribe(OnMaximumChanged); + MinimumProperty.Changed.Subscribe(OnMinimumChanged); + TextProperty.Changed.Subscribe(OnTextChanged); + ValueProperty.Changed.Subscribe(OnValueChanged); + } + + /// + protected override void OnLostFocus(RoutedEventArgs e) + { + CommitInput(); + base.OnLostFocus(e); + } + + /// + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + if (TextBox != null) + { + TextBox.PointerPressed -= TextBoxOnPointerPressed; + _textBoxTextChangedSubscription?.Dispose(); + } + TextBox = e.NameScope.Find("PART_TextBox"); + if (TextBox != null) + { + TextBox.Text = Text; + TextBox.PointerPressed += TextBoxOnPointerPressed; + _textBoxTextChangedSubscription = TextBox.GetObservable(TextBox.TextProperty).Subscribe(txt => TextBoxOnTextChanged()); + } + + if (Spinner != null) + { + Spinner.Spin -= OnSpinnerSpin; + } + + Spinner = e.NameScope.Find("PART_Spinner"); + + if (Spinner != null) + { + Spinner.Spin += OnSpinnerSpin; + } + + SetValidSpinDirection(); + } + + /// + protected override void OnKeyDown(KeyEventArgs e) + { + switch (e.Key) + { + case Key.Enter: + var commitSuccess = CommitInput(); + e.Handled = !commitSuccess; + break; + } + } + + /// + /// Called when the property value changed. + /// + /// The old value. + /// The new value. + protected virtual void OnCultureInfoChanged(CultureInfo oldValue, CultureInfo newValue) + { + if (IsInitialized) + { + SyncTextAndValueProperties(false, null); + } + } + + /// + /// Called when the property value changed. + /// + /// The old value. + /// The new value. + protected virtual void OnFormatStringChanged(string oldValue, string newValue) + { + if (IsInitialized) + { + SyncTextAndValueProperties(false, null); + } + } + + /// + /// Called when the property value changed. + /// + /// The old value. + /// The new value. + protected virtual void OnIncrementChanged(double oldValue, double newValue) + { + if (IsInitialized) + { + SetValidSpinDirection(); + } + } + + /// + /// Called when the property value changed. + /// + /// The old value. + /// The new value. + protected virtual void OnIsReadOnlyChanged(bool oldValue, bool newValue) + { + SetValidSpinDirection(); + } + + /// + /// Called when the property value changed. + /// + /// The old value. + /// The new value. + protected virtual void OnMaximumChanged(double oldValue, double newValue) + { + if (IsInitialized) + { + SetValidSpinDirection(); + } + if (ClipValueToMinMax) + { + Value = MathUtilities.Clamp(Value, Minimum, Maximum); + } + } + + /// + /// Called when the property value changed. + /// + /// The old value. + /// The new value. + protected virtual void OnMinimumChanged(double oldValue, double newValue) + { + if (IsInitialized) + { + SetValidSpinDirection(); + } + if (ClipValueToMinMax) + { + Value = MathUtilities.Clamp(Value, Minimum, Maximum); + } + } + + /// + /// Called when the property value changed. + /// + /// The old value. + /// The new value. + protected virtual void OnTextChanged(string oldValue, string newValue) + { + if (IsInitialized) + { + SyncTextAndValueProperties(true, Text); + } + } + + /// + /// Called when the property value changed. + /// + /// The old value. + /// The new value. + protected virtual void OnValueChanged(double oldValue, double newValue) + { + if (!_internalValueSet && IsInitialized) + { + SyncTextAndValueProperties(false, null, true); + } + + SetValidSpinDirection(); + + RaiseValueChangedEvent(oldValue, newValue); + } + + /// + /// Called when the property has to be coerced. + /// + /// The value. + protected virtual double OnCoerceIncrement(double baseValue) + { + return baseValue; + } + + /// + /// Called when the property has to be coerced. + /// + /// The value. + protected virtual double OnCoerceMaximum(double baseValue) + { + return Math.Max(baseValue, Minimum); + } + + /// + /// Called when the property has to be coerced. + /// + /// The value. + protected virtual double OnCoerceMinimum(double baseValue) + { + return Math.Min(baseValue, Maximum); + } + + /// + /// Called when the property has to be coerced. + /// + /// The value. + protected virtual double OnCoerceValue(double baseValue) + { + return baseValue; + } + + /// + /// Raises the OnSpin event when spinning is initiated by the end-user. + /// + /// The event args. + protected virtual void OnSpin(SpinEventArgs e) + { + if (e == null) + { + throw new ArgumentNullException(nameof(e)); + } + + var handler = Spinned; + handler?.Invoke(this, e); + + if (e.Direction == SpinDirection.Increase) + { + DoIncrement(); + } + else + { + DoDecrement(); + } + } + + /// + /// Raises the event. + /// + /// The old value. + /// The new value. + protected virtual void RaiseValueChangedEvent(double oldValue, double newValue) + { + var e = new NumericUpDownValueChangedEventArgs(ValueChangedEvent, oldValue, newValue); + RaiseEvent(e); + } + + /// + /// Converts the formatted text to a value. + /// + private double ConvertTextToValue(string text) + { + double result = 0; + + if (string.IsNullOrEmpty(text)) + { + return result; + } + + // Since the conversion from Value to text using a FormatString may not be parsable, + // we verify that the already existing text is not the exact same value. + var currentValueText = ConvertValueToText(); + if (Equals(currentValueText, text)) + { + return Value; + } + + result = ConvertTextToValueCore(currentValueText, text); + + if (ClipValueToMinMax) + { + return MathUtilities.Clamp(result, Minimum, Maximum); + } + + ValidateMinMax(result); + + return result; + } + + /// + /// Converts the value to formatted text. + /// + /// + private string ConvertValueToText() + { + //Manage FormatString of type "{}{0:N2} °" (in xaml) or "{0:N2} °" in code-behind. + if (FormatString.Contains("{0")) + { + return string.Format(CultureInfo, FormatString, Value); + } + + return Value.ToString(FormatString, CultureInfo); + } + + /// + /// Called by OnSpin when the spin direction is SpinDirection.Increase. + /// + private void OnIncrement() + { + var result = Value + Increment; + Value = MathUtilities.Clamp(result, Minimum, Maximum); + } + + /// + /// Called by OnSpin when the spin direction is SpinDirection.Decrease. + /// + private void OnDecrement() + { + var result = Value - Increment; + Value = MathUtilities.Clamp(result, Minimum, Maximum); + } + + /// + /// Sets the valid spin directions. + /// + private void SetValidSpinDirection() + { + var validDirections = ValidSpinDirections.None; + + // Zero increment always prevents spin. + if (Increment != 0 && !IsReadOnly) + { + if (Value < Maximum) + { + validDirections = validDirections | ValidSpinDirections.Increase; + } + + if (Value > Minimum) + { + validDirections = validDirections | ValidSpinDirections.Decrease; + } + } + + if (Spinner != null) + { + Spinner.ValidSpinDirection = validDirections; + } + } + + /// + /// Called when the property value changed. + /// + /// The event args. + private static void OnCultureInfoChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is NumericUpDown upDown) + { + var oldValue = (CultureInfo)e.OldValue; + var newValue = (CultureInfo)e.NewValue; + upDown.OnCultureInfoChanged(oldValue, newValue); + } + } + + /// + /// Called when the property value changed. + /// + /// The event args. + private static void IncrementChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is NumericUpDown upDown) + { + var oldValue = (double)e.OldValue; + var newValue = (double)e.NewValue; + upDown.OnIncrementChanged(oldValue, newValue); + } + } + + /// + /// Called when the property value changed. + /// + /// The event args. + private static void FormatStringChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is NumericUpDown upDown) + { + var oldValue = (string)e.OldValue; + var newValue = (string)e.NewValue; + upDown.OnFormatStringChanged(oldValue, newValue); + } + } + + /// + /// Called when the property value changed. + /// + /// The event args. + private static void OnIsReadOnlyChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is NumericUpDown upDown) + { + var oldValue = (bool)e.OldValue; + var newValue = (bool)e.NewValue; + upDown.OnIsReadOnlyChanged(oldValue, newValue); + } + } + + /// + /// Called when the property value changed. + /// + /// The event args. + private static void OnMaximumChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is NumericUpDown upDown) + { + var oldValue = (double)e.OldValue; + var newValue = (double)e.NewValue; + upDown.OnMaximumChanged(oldValue, newValue); + } + } + + /// + /// Called when the property value changed. + /// + /// The event args. + private static void OnMinimumChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is NumericUpDown upDown) + { + var oldValue = (double)e.OldValue; + var newValue = (double)e.NewValue; + upDown.OnMinimumChanged(oldValue, newValue); + } + } + + /// + /// Called when the property value changed. + /// + /// The event args. + private static void OnTextChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is NumericUpDown upDown) + { + var oldValue = (string)e.OldValue; + var newValue = (string)e.NewValue; + upDown.OnTextChanged(oldValue, newValue); + } + } + + /// + /// Called when the property value changed. + /// + /// The event args. + private static void OnValueChanged(AvaloniaPropertyChangedEventArgs e) + { + if (e.Sender is NumericUpDown upDown) + { + var oldValue = (double)e.OldValue; + var newValue = (double)e.NewValue; + upDown.OnValueChanged(oldValue, newValue); + } + } + + private void SetValueInternal(double value) + { + _internalValueSet = true; + try + { + Value = value; + } + finally + { + _internalValueSet = false; + } + } + + private static double OnCoerceMaximum(IAvaloniaObject instance, double value) + { + if (instance is NumericUpDown upDown) + { + return upDown.OnCoerceMaximum(value); + } + + return value; + } + + private static double OnCoerceMinimum(IAvaloniaObject instance, double value) + { + if (instance is NumericUpDown upDown) + { + return upDown.OnCoerceMinimum(value); + } + + return value; + } + + private static double OnCoerceIncrement(IAvaloniaObject instance, double value) + { + if (instance is NumericUpDown upDown) + { + return upDown.OnCoerceIncrement(value); + } + + return value; + } + + private void TextBoxOnTextChanged() + { + try + { + _isTextChangedFromUI = true; + if (TextBox != null) + { + Text = TextBox.Text; + } + } + finally + { + _isTextChangedFromUI = false; + } + } + + private void OnSpinnerSpin(object sender, SpinEventArgs e) + { + if (AllowSpin && !IsReadOnly) + { + var spin = !e.UsingMouseWheel; + spin |= ((TextBox != null) && TextBox.IsFocused); + + if (spin) + { + e.Handled = true; + OnSpin(e); + } + } + } + + private void DoDecrement() + { + if (Spinner == null || (Spinner.ValidSpinDirection & ValidSpinDirections.Decrease) == ValidSpinDirections.Decrease) + { + OnDecrement(); + } + } + + private void DoIncrement() + { + if (Spinner == null || (Spinner.ValidSpinDirection & ValidSpinDirections.Increase) == ValidSpinDirections.Increase) + { + OnIncrement(); + } + } + + public event EventHandler Spinned; + + private void TextBoxOnPointerPressed(object sender, PointerPressedEventArgs e) + { + if (e.Pointer.Captured != Spinner) + { + Dispatcher.UIThread.InvokeAsync(() => { e.Pointer.Capture(Spinner); }, DispatcherPriority.Input); + } + } + + /// + /// Defines the event. + /// + public static readonly RoutedEvent ValueChangedEvent = + RoutedEvent.Register(nameof(ValueChanged), RoutingStrategies.Bubble); + + /// + /// Raised when the changes. + /// + public event EventHandler ValueChanged + { + add { AddHandler(ValueChangedEvent, value); } + remove { RemoveHandler(ValueChangedEvent, value); } + } + + private bool CommitInput() + { + return SyncTextAndValueProperties(true, Text); + } + + /// + /// Synchronize and properties. + /// + /// If value should be updated from text. + /// The text. + private bool SyncTextAndValueProperties(bool updateValueFromText, string text) + { + return SyncTextAndValueProperties(updateValueFromText, text, false); + } + + /// + /// Synchronize and properties. + /// + /// If value should be updated from text. + /// The text. + /// Force text update. + private bool SyncTextAndValueProperties(bool updateValueFromText, string text, bool forceTextUpdate) + { + if (_isSyncingTextAndValueProperties) + return true; + + _isSyncingTextAndValueProperties = true; + var parsedTextIsValid = true; + try + { + if (updateValueFromText) + { + if (!string.IsNullOrEmpty(text)) + { + try + { + var newValue = ConvertTextToValue(text); + if (!Equals(newValue, Value)) + { + SetValueInternal(newValue); + } + } + catch + { + parsedTextIsValid = false; + } + } + } + + // Do not touch the ongoing text input from user. + if (!_isTextChangedFromUI) + { + var keepEmpty = !forceTextUpdate && string.IsNullOrEmpty(Text); + if (!keepEmpty) + { + var newText = ConvertValueToText(); + if (!Equals(Text, newText)) + { + Text = newText; + } + } + + // Sync Text and textBox + if (TextBox != null) + { + TextBox.Text = Text; + } + } + + if (_isTextChangedFromUI && !parsedTextIsValid) + { + // Text input was made from the user and the text + // represents an invalid value. Disable the spinner in this case. + if (Spinner != null) + { + Spinner.ValidSpinDirection = ValidSpinDirections.None; + } + } + else + { + SetValidSpinDirection(); + } + } + finally + { + _isSyncingTextAndValueProperties = false; + } + return parsedTextIsValid; + } + + private double ConvertTextToValueCore(string currentValueText, string text) + { + double result; + + if (IsPercent(FormatString)) + { + result = decimal.ToDouble(ParsePercent(text, CultureInfo)); + } + else + { + // Problem while converting new text + if (!double.TryParse(text, ParsingNumberStyle, CultureInfo, out var outputValue)) + { + var shouldThrow = true; + + // Check if CurrentValueText is also failing => it also contains special characters. ex : 90° + if (!double.TryParse(currentValueText, ParsingNumberStyle, CultureInfo, out var _)) + { + // extract non-digit characters + var currentValueTextSpecialCharacters = currentValueText.Where(c => !char.IsDigit(c)); + var textSpecialCharacters = text.Where(c => !char.IsDigit(c)); + // same non-digit characters on currentValueText and new text => remove them on new Text to parse it again. + if (currentValueTextSpecialCharacters.Except(textSpecialCharacters).ToList().Count == 0) + { + foreach (var character in textSpecialCharacters) + { + text = text.Replace(character.ToString(), string.Empty); + } + // if without the special characters, parsing is good, do not throw + if (double.TryParse(text, ParsingNumberStyle, CultureInfo, out outputValue)) + { + shouldThrow = false; + } + } + } + + if (shouldThrow) + { + throw new InvalidDataException("Input string was not in a correct format."); + } + } + result = outputValue; + } + return result; + } + + private void ValidateMinMax(double value) + { + if (value < Minimum) + { + throw new ArgumentOutOfRangeException(nameof(value), string.Format("Value must be greater than Minimum value of {0}", Minimum)); + } + else if (value > Maximum) + { + throw new ArgumentOutOfRangeException(nameof(value), string.Format("Value must be less than Maximum value of {0}", Maximum)); + } + } + + /// + /// Parse percent format text + /// + /// Text to parse. + /// The culture info. + private static decimal ParsePercent(string text, IFormatProvider cultureInfo) + { + var info = NumberFormatInfo.GetInstance(cultureInfo); + text = text.Replace(info.PercentSymbol, null); + var result = decimal.Parse(text, NumberStyles.Any, info); + result = result / 100; + return result; + } + + + private bool IsPercent(string stringToTest) + { + var PIndex = stringToTest.IndexOf("P", StringComparison.Ordinal); + if (PIndex >= 0) + { + //stringToTest contains a "P" between 2 "'", it's considered as text, not percent + var isText = stringToTest.Substring(0, PIndex).Contains("'") + && stringToTest.Substring(PIndex, FormatString.Length - PIndex).Contains("'"); + + return !isText; + } + return false; + } + } +} diff --git a/src/Avalonia.Controls/Templates/NumericUpDown/NumericUpDownValueChangedEventArgs.cs b/src/Avalonia.Controls/Templates/NumericUpDown/NumericUpDownValueChangedEventArgs.cs new file mode 100644 index 0000000000..e994ffdd15 --- /dev/null +++ b/src/Avalonia.Controls/Templates/NumericUpDown/NumericUpDownValueChangedEventArgs.cs @@ -0,0 +1,16 @@ +using Avalonia.Interactivity; + +namespace Avalonia.Controls +{ + public class NumericUpDownValueChangedEventArgs : RoutedEventArgs + { + public NumericUpDownValueChangedEventArgs(RoutedEvent routedEvent, double oldValue, double newValue) : base(routedEvent) + { + OldValue = oldValue; + NewValue = newValue; + } + + public double OldValue { get; } + public double NewValue { get; } + } +} From e7d6a2a0abd0aac4469e24a2659526f1916f01a8 Mon Sep 17 00:00:00 2001 From: amwx Date: Thu, 11 Jun 2020 21:56:23 -0500 Subject: [PATCH 106/693] Add DatePicker and related classes --- .../DateTimePickers/DatePicker.cs | 532 ++++++++++ .../DateTimePickers/DatePickerPresenter.cs | 971 ++++++++++++++++++ .../DatePickerPresenterItem.cs | 31 + ...DatePickerSelectedValueChangedEventArgs.cs | 19 + .../DatePickerValueChangedEventArgs.cs | 19 + .../DateTimePickers/PickerPresenterBase.cs | 58 ++ .../DateTimePickers/DateTimeFormatter.cs | 335 ------ .../DateTimePickers/LoopingSelector.cs | 10 - 8 files changed, 1630 insertions(+), 345 deletions(-) create mode 100644 src/Avalonia.Controls/DateTimePickers/DatePicker.cs create mode 100644 src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs create mode 100644 src/Avalonia.Controls/DateTimePickers/DatePickerPresenterItem.cs create mode 100644 src/Avalonia.Controls/DateTimePickers/DatePickerSelectedValueChangedEventArgs.cs create mode 100644 src/Avalonia.Controls/DateTimePickers/DatePickerValueChangedEventArgs.cs create mode 100644 src/Avalonia.Controls/DateTimePickers/PickerPresenterBase.cs delete mode 100644 src/Avalonia.Controls/Templates/DateTimePickers/DateTimeFormatter.cs delete mode 100644 src/Avalonia.Controls/Templates/DateTimePickers/LoopingSelector.cs diff --git a/src/Avalonia.Controls/DateTimePickers/DatePicker.cs b/src/Avalonia.Controls/DateTimePickers/DatePicker.cs new file mode 100644 index 0000000000..e27af94a2d --- /dev/null +++ b/src/Avalonia.Controls/DateTimePickers/DatePicker.cs @@ -0,0 +1,532 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Shapes; +using Avalonia.Controls.Templates; +using Avalonia.Input; +using System; +using System.Text.RegularExpressions; + +namespace Avalonia.Controls +{ + /// + /// A control to allow the user to select a date + /// + public class DatePicker : TemplatedControl + { + public DatePicker() + { + PseudoClasses.Set(":hasnodate", true); + _presenter = new DatePickerPresenter(); + var now = DateTimeOffset.Now; + _minYear = new DateTimeOffset(now.Date.Year - 100, 1, 1, 0, 0, 0, now.Offset); + _maxYear = new DateTimeOffset(now.Date.Year + 100, 12, 31, 0, 0, 0, now.Offset); + + _presenter.DatePicked += OnPresenterDatePicked; + } + + /// + /// Define the Property + /// + public static readonly DirectProperty DayFormatProperty = + AvaloniaProperty.RegisterDirect("DayFormat", + x => x.DayFormat, (x, v) => x.DayFormat = v); + + /// + /// Defines the Property + /// + public static readonly DirectProperty DayVisibleProperty = + AvaloniaProperty.RegisterDirect("DayVisible", + x => x.DayVisible, (x, v) => x.DayVisible = v); + + /// + /// Defines the Property + /// + public static readonly StyledProperty HeaderProperty = + AvaloniaProperty.Register("Header"); + + /// + /// Defines the Property + /// + public static readonly StyledProperty HeaderTemplateProperty = + AvaloniaProperty.Register("HeaderTemplate"); + + /// + /// Defines the Property + /// + public static readonly DirectProperty MaxYearProperty = + AvaloniaProperty.RegisterDirect("MaxYear", x => x.MaxYear, (x, v) => x.MaxYear = v); + + /// + /// Defines the Property + /// + public static readonly DirectProperty MinYearProperty = + AvaloniaProperty.RegisterDirect("MinYear", x => x.MinYear, (x, v) => x.MinYear = v); + + /// + /// Defines the Property + /// + public static readonly DirectProperty MonthFormatProperty = + AvaloniaProperty.RegisterDirect("MonthFormat", x => x.MonthFormat, (x, v) => x.MonthFormat = v); + + /// + /// Defines the Property + /// + public static readonly DirectProperty MonthVisibleProperty = + AvaloniaProperty.RegisterDirect("MonthVisible", x => x.MonthVisible, (x, v) => x.MonthVisible = v); + + /// + /// Defiens the Property + /// + public static readonly DirectProperty YearFormatProperty = + AvaloniaProperty.RegisterDirect("YearFormat", x => x.YearFormat, (x, v) => x.YearFormat = v); + + /// + /// Defines the Property + /// + public static readonly DirectProperty YearVisibleProperty = + AvaloniaProperty.RegisterDirect("YearVisible", x => x.YearVisible, (x, v) => x.YearVisible = v); + + /// + /// Defines the Property + /// + public static readonly DirectProperty SelectedDateProperty = + AvaloniaProperty.RegisterDirect("SelectedDate", x => x.SelectedDate, (x, v) => x.SelectedDate = v); + + /// + /// Gets or sets the day format + /// + public string DayFormat + { + get => _dayFormat; + set => SetAndRaise(DayFormatProperty, ref _dayFormat, value); + } + + /// + /// Gets or sets whether the day is visible + /// + public bool DayVisible + { + get => _dayVisible; + set + { + SetAndRaise(DayVisibleProperty, ref _dayVisible, value); + SetGrid(); + } + } + + /// + /// Gets or sets the DatePicker header + /// + public object Header + { + get => GetValue(HeaderProperty); + set => SetValue(HeaderProperty, value); + } + + /// + /// Gets or sets the header template + /// + public IDataTemplate HeaderTemplate + { + get => GetValue(HeaderTemplateProperty); + set => SetValue(HeaderTemplateProperty, value); + } + + /// + /// Gets or sets the maximum year for the picker + /// + public DateTimeOffset MaxYear + { + get => _maxYear; + set + { + if (value < MinYear) + throw new InvalidOperationException("MaxDate cannot be less than MinDate"); + SetAndRaise(MaxYearProperty, ref _maxYear, value); + + if (SelectedDate.HasValue && SelectedDate.Value > value) + SelectedDate = value; + } + } + + /// + /// Gets or sets the minimum year for the picker + /// + public DateTimeOffset MinYear + { + get => _minYear; + set + { + if (value > MaxYear) + throw new InvalidOperationException("MinDate cannot be greater than MaxDate"); + SetAndRaise(MinYearProperty, ref _minYear, value); + + if (SelectedDate.HasValue && SelectedDate.Value < value) + SelectedDate = value; + } + } + + /// + /// Gets or sets the month format + /// + public string MonthFormat + { + get => _monthFormat; + set => SetAndRaise(MonthFormatProperty, ref _monthFormat, value); + } + + /// + /// Gets or sets whether the month is visible + /// + public bool MonthVisible + { + get => _monthVisible; + set + { + SetAndRaise(MonthVisibleProperty, ref _monthVisible, value); + SetGrid(); + } + } + + /// + /// Gets or sets the year format + /// + public string YearFormat + { + get => _yearFormat; + set => SetAndRaise(YearFormatProperty, ref _yearFormat, value); + } + + /// + /// Gets or sets whether the year is visible + /// + public bool YearVisible + { + get => _yearVisible; + set + { + SetAndRaise(YearVisibleProperty, ref _yearVisible, value); + SetGrid(); + } + } + + /// + /// Gets or sets the Selected Date for the picker, can be null + /// + public DateTimeOffset? SelectedDate + { + get => _selectedDate; + set + { + var old = _selectedDate; + SetAndRaise(SelectedDateProperty, ref _selectedDate, value); + SetSelectedDateText(); + OnSelectedDateChanged(this, new DatePickerSelectedValueChangedEventArgs(old, value)); + } + } + + /// + /// Raised when the changes + /// + public event EventHandler SelectedDateChanged; + + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + _flyoutButton = e.NameScope.Find("FlyoutButton"); + _dayText = e.NameScope.Find("DayText"); + _monthText = e.NameScope.Find("MonthText"); + _yearText = e.NameScope.Find("YearText"); + _container = e.NameScope.Find("ButtonContentGrid"); + _spacer1 = e.NameScope.Find("FirstSpacer"); + _spacer2 = e.NameScope.Find("SecondSpacer"); + + _areControlsAvailable = true; + + SetGrid(); + + SetSelectedDateText(); + + if (_flyoutButton != null) + { + _flyoutButton.Click += OnFlyoutButtonClicked; + } + + + } + + protected override void OnKeyDown(KeyEventArgs e) + { + //switch (e.Key) + //{ + // case Key.Tab: + // //FocusManager.Instance.Focus(_presenter, NavigationMethod.Tab); + // //Debug.WriteLine(FocusManager.Instance.Current.ToString()); + // e.Handled = true; + // break; + //} + base.OnKeyDown(e); + } + + /// + /// Sets up the container grid and makes sure all label and spacers are placed correctly + /// + private void SetGrid() + { + //Brute force method to setup the container grid, probably a better way to do this + //but it works... + + if (!_areControlsAvailable) //hopefully this never happens + return; + + if (!_hasInit) + { + //Display order of date is based on user's culture, we attempt + //to figure out the normal date pattern + //TODO: Find better way to do this, but for now it works... + var fmt = System.Globalization.CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern; + var monthfmt = Regex.Match(fmt, "(M|MM)"); + var yearfmt = Regex.Match(fmt, "(Y|YY|YYY|YYYY|y|yy|yyy|yyyy)"); + var dayfmt = Regex.Match(fmt, "(d|dd)"); + + //Default is M-D-Y (en-us), this gives us fallback if pattern matching fails + _monthIndex = 0; + _yearIndex = 2; + _dayIndex = 1; + + if (monthfmt.Success && yearfmt.Success && dayfmt.Success) + { + _monthIndex = monthfmt.Index; + _yearIndex = yearfmt.Index; + _dayIndex = dayfmt.Index; + } + //Six possible combos, some probably don't actually exist, + //but prep for them anyway + //M d y [x] + //M y d [x] + //d M y [x] + //d y M [x] + //y d M [x] + //y M d [x] + + _hasInit = true; + } + + bool showMonth = MonthVisible; + bool showDay = DayVisible; + bool showYear = YearVisible; + + _container.ColumnDefinitions.Clear(); + + if (showMonth && !showDay && !showYear) //Month Only + { + _container.ColumnDefinitions.Add(new ColumnDefinition(132, GridUnitType.Star)); + _monthText.IsVisible = true; + _dayText.IsVisible = false; + _yearText.IsVisible = false; + _spacer1.IsVisible = false; + _spacer2.IsVisible = false; + + Grid.SetColumn(_monthText, 0); + } + else if (!showMonth && showDay && !showYear) //Day Only + { + _container.ColumnDefinitions.Add(new ColumnDefinition(132, GridUnitType.Star)); + _monthText.IsVisible = false; + _dayText.IsVisible = true; + _yearText.IsVisible = false; + _spacer1.IsVisible = false; + _spacer2.IsVisible = false; + + Grid.SetColumn(_dayText, 0); + } + else if (!showMonth && !showDay && showYear) //Year Only + { + _container.ColumnDefinitions.Add(new ColumnDefinition(132, GridUnitType.Star)); + _monthText.IsVisible = false; + _dayText.IsVisible = false; + _yearText.IsVisible = true; + _spacer1.IsVisible = false; + _spacer2.IsVisible = false; + + Grid.SetColumn(_yearText, 0); + } + else if (showMonth && showDay && !showYear) //Month and Day Only + { + _container.ColumnDefinitions.Add(new ColumnDefinition(_monthIndex < _dayIndex ? 132 : 78, GridUnitType.Star)); + _container.ColumnDefinitions.Add(new ColumnDefinition(0, GridUnitType.Auto)); + _container.ColumnDefinitions.Add(new ColumnDefinition(_monthIndex < _dayIndex ? 78 : 132, GridUnitType.Star)); + + _monthText.IsVisible = true; + _dayText.IsVisible = true; + _yearText.IsVisible = false; + _spacer1.IsVisible = true; + _spacer2.IsVisible = false; + + Grid.SetColumn(_monthText, _monthIndex < _dayIndex ? 0 : 2); + Grid.SetColumn(_dayText, _monthIndex < _dayIndex ? 2 : 0); + Grid.SetColumn(_spacer1, 1); + } + else if (showMonth && !showDay && showYear) //Month and Year Only + { + _container.ColumnDefinitions.Add(new ColumnDefinition(_monthIndex < _yearIndex ? 132 : 78, GridUnitType.Star)); + _container.ColumnDefinitions.Add(new ColumnDefinition(0, GridUnitType.Auto)); + _container.ColumnDefinitions.Add(new ColumnDefinition(_monthIndex < _yearIndex ? 78 : 132, GridUnitType.Star)); + + _monthText.IsVisible = true; + _dayText.IsVisible = false; + _yearText.IsVisible = true; + _spacer1.IsVisible = true; + _spacer2.IsVisible = false; + + Grid.SetColumn(_monthText, _monthIndex < _yearIndex ? 0 : 2); + Grid.SetColumn(_yearText, _monthIndex < _yearIndex ? 2 : 0); + Grid.SetColumn(_spacer1, 1); + } + else if (!showMonth && showDay && showYear) //Day and Year Only + { + _container.ColumnDefinitions.Add(new ColumnDefinition(78, GridUnitType.Star)); + _container.ColumnDefinitions.Add(new ColumnDefinition(0, GridUnitType.Auto)); + _container.ColumnDefinitions.Add(new ColumnDefinition(78, GridUnitType.Star)); + + _monthText.IsVisible = false; + _dayText.IsVisible = true; + _yearText.IsVisible = true; + _spacer1.IsVisible = true; + _spacer2.IsVisible = false; + + Grid.SetColumn(_yearText, _dayIndex < _yearIndex ? 2 : 0); + Grid.SetColumn(_dayText, _dayIndex < _yearIndex ? 0 : 2); + Grid.SetColumn(_spacer1, 1); + } + else if (showMonth && showDay && showYear) //All Visible + { + bool isMonthFirst = _monthIndex < _dayIndex && _monthIndex < _yearIndex; + bool isMonthSecond = (_monthIndex > _dayIndex && _monthIndex < _yearIndex) || + (_monthIndex < _dayIndex && _monthIndex > _yearIndex); + + _container.ColumnDefinitions.Add(new ColumnDefinition(isMonthFirst ? 138 : 78, GridUnitType.Star)); + _container.ColumnDefinitions.Add(new ColumnDefinition(0, GridUnitType.Auto)); + _container.ColumnDefinitions.Add(new ColumnDefinition(isMonthSecond ? 138 : 78, GridUnitType.Star)); + _container.ColumnDefinitions.Add(new ColumnDefinition(0, GridUnitType.Auto)); + _container.ColumnDefinitions.Add(new ColumnDefinition((!isMonthFirst && !isMonthSecond) ? 138 : 78, GridUnitType.Star)); + + _monthText.IsVisible = true; + _dayText.IsVisible = true; + _yearText.IsVisible = true; + _spacer1.IsVisible = true; + _spacer2.IsVisible = true; + + bool isDayFirst = !isMonthFirst && _dayIndex < _yearIndex; + bool isDaySecond = (_dayIndex > _monthIndex && _dayIndex < _yearIndex) || + (_dayIndex < _monthIndex && _dayIndex > _yearIndex); + + bool isYearFirst = !isDayFirst && !isMonthFirst; + bool isYearSecond = (_yearIndex > _monthIndex && _yearIndex < _dayIndex) || + (_yearIndex < _monthIndex && _yearIndex > _dayIndex); + + Grid.SetColumn(_monthText, isMonthFirst ? 0 : isMonthSecond ? 2 : 4); + Grid.SetColumn(_yearText, isYearFirst ? 0 : (isMonthSecond || isDaySecond) ? 4 : 2); + Grid.SetColumn(_dayText, isDayFirst ? 0 : (isMonthSecond || isYearSecond) ? 4 : 2); + + Grid.SetColumn(_spacer1, 1); + Grid.SetColumn(_spacer2, 3); + } + else + { + _monthText.IsVisible = false; + _dayText.IsVisible = false; + _yearText.IsVisible = false; + _spacer1.IsVisible = false; + _spacer2.IsVisible = false; + } + } + + /// + /// Sets the TextBlocks when the SelectedDate changes + /// + private void SetSelectedDateText() + { + if (!_areControlsAvailable) + return; + + if (SelectedDate.HasValue) + { + PseudoClasses.Set(":hasnodate", false); + var selDate = SelectedDate.Value; + _monthText.Text = new DateTimeFormatter(MonthFormat).Format(selDate); + _yearText.Text = new DateTimeFormatter(YearFormat).Format(selDate); + _dayText.Text = new DateTimeFormatter(DayFormat).Format(selDate); + } + else + { + PseudoClasses.Set(":hasnodate", true); + _monthText.Text = "month"; + _yearText.Text = "year"; + _dayText.Text = "day"; + } + } + + private void OnFlyoutButtonClicked(object sender, Avalonia.Interactivity.RoutedEventArgs e) + { + //Need to position the popup before displaying it + //We want to position the selected item overtop of the DatePicker + //We want to see the DatePicker left/right borders on either side of the popup + //_flyout.Width = this.Width - 2; + //_flyout.IsOpen = true; + + _presenter.YearFormat = YearFormat; + _presenter.DayFormat = DayFormat; + _presenter.MonthFormat = MonthFormat; + _presenter.MonthVisible = MonthVisible; + _presenter.YearVisible = YearVisible; + _presenter.DayVisible = DayVisible; + //If SelectedDate hasn't been set, fallback to now + _presenter.Date = SelectedDate.HasValue ? SelectedDate.Value : DateTimeOffset.Now; + _presenter.MaxYear = MaxYear; + _presenter.MinYear = MinYear; + + _presenter.ShowAt(this); + } + + private void OnPresenterDatePicked(object sender, DatePickerValueChangedEventArgs args) + { + SelectedDate = args.NewDate; + } + + protected virtual void OnSelectedDateChanged(object sender, DatePickerSelectedValueChangedEventArgs args) + { + SelectedDateChanged?.Invoke(sender, args); + } + + + //Template Items + private Avalonia.Controls.Button _flyoutButton; + private TextBlock _dayText; + private TextBlock _monthText; + private TextBlock _yearText; + private Grid _container; + private Rectangle _spacer1; + private Rectangle _spacer2; + + private DatePickerPresenter _presenter; + + private bool _hasInit; + private bool _areControlsAvailable; + private int _monthIndex; + private int _dayIndex; + private int _yearIndex; + + private string _dayFormat = "{day.integer}"; + private bool _dayVisible = true; + private DateTimeOffset _maxYear; + private DateTimeOffset _minYear; + private string _monthFormat = "{month.full}"; + private bool _monthVisible = true; + private string _yearFormat = "{year.full}"; + private bool _yearVisible = true; + private DateTimeOffset? _selectedDate; + } +} diff --git a/src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs b/src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs new file mode 100644 index 0000000000..063e6c3779 --- /dev/null +++ b/src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs @@ -0,0 +1,971 @@ +using Avalonia; +using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Shapes; +using Avalonia.Controls.Templates; +using Avalonia.Input; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Text.RegularExpressions; + +namespace Avalonia.Controls +{ + /// + /// Defines the presenter used for selecting a date. Intended for use with + /// but can be used independently. Combines + /// DatePickerFlyout and DatePickerFlyoutPresenter + /// + public class DatePickerPresenter : PickerPresenterBase + { + public DatePickerPresenter() + { + var now = DateTimeOffset.Now; + _minYear = new DateTimeOffset(now.Year - 100, 1, 1, 0, 0, 0, now.Offset); + _maxYear = new DateTimeOffset(now.Year + 100, 12, 31, 0, 0, 0, now.Offset); + _date = now; + + KeyboardNavigation.SetTabNavigation(this, KeyboardNavigationMode.Cycle); + } + + /// + /// Defines the Property + /// + public static readonly DirectProperty DateProperty = + AvaloniaProperty.RegisterDirect("Date", x => x.Date, (x, v) => x.Date = v); + + /// + /// Defines the Property + /// + public static readonly DirectProperty DayFormatProperty = + AvaloniaProperty.RegisterDirect("DayFormat", x => x.DayFormat, (x, v) => x.DayFormat = v); + + /// + /// Defines the Property + /// + public static readonly DirectProperty DayVisibleProperty = + AvaloniaProperty.RegisterDirect("DayVisible", x => x.DayVisible, (x, v) => x.DayVisible = v); + + /// + /// Defines the Property + /// + public static readonly DirectProperty MaxYearProperty = + AvaloniaProperty.RegisterDirect("MaxYear", x => x.MaxYear, (x, v) => x.MaxYear = v); + + /// + /// Defines the Property + /// + public static readonly DirectProperty MinYearProperty = + AvaloniaProperty.RegisterDirect("MinYear", x => x.MinYear, (x, v) => x.MinYear = v); + + /// + /// Defines the Property + /// + public static readonly DirectProperty MonthFormatProperty = + AvaloniaProperty.RegisterDirect("MonthFormat", x => x.MonthFormat, (x, v) => x.MonthFormat = v); + + /// + /// Defines the Property + /// + public static readonly DirectProperty MonthVisibleProperty = + AvaloniaProperty.RegisterDirect("MonthVisible", x => x.MonthVisible, (x, v) => x.MonthVisible = v); + + /// + /// Defines the Property + /// + public static readonly DirectProperty YearFormatProperty = + AvaloniaProperty.RegisterDirect("YearFormat", x => x.YearFormat, (x, v) => x.YearFormat = v); + + /// + /// Defines the Property + /// + public static readonly DirectProperty YearVisibleProperty = + AvaloniaProperty.RegisterDirect("YearVisible", x => x.YearVisible, (x, v) => x.YearVisible = v); + + //These aren't in WinUI + /// + /// Defines the Property + /// + public static readonly StyledProperty YearSelectorItemTemplateProperty = + AvaloniaProperty.Register("YearSelectorItemTemplate"); + + /// + /// Defines the Property + /// + public static readonly StyledProperty MonthSelectorItemTemplateProperty = + AvaloniaProperty.Register("MonthSelectorItemTemplate"); + + /// + /// Defines the Property + /// + public static readonly StyledProperty DaySelectorItemTemplateProperty = + AvaloniaProperty.Register("DaySelectorItemTemplate"); + + /// + /// Gets or sets the current Date for the picker + /// + public DateTimeOffset Date + { + get => _date; + set => SetAndRaise(DateProperty, ref _date, value); + } + + /// + /// Gets or sets the DayFormat + /// + public string DayFormat + { + get => _dayFormat; + set => SetAndRaise(DayFormatProperty, ref _dayFormat, value); + } + + /// + /// Get or sets whether the Day selector is visible + /// + public bool DayVisible + { + get => _dayVisible; + set + { + SetAndRaise(DayVisibleProperty, ref _dayVisible, value); + } + } + + /// + /// Gets or sets the maximum pickable year + /// + public DateTimeOffset MaxYear + { + get => _maxYear; + set + { + SetAndRaise(MaxYearProperty, ref _maxYear, value); + //Coerce date if needed + } + } + + /// + /// Gets or sets the minimum pickable year + /// + public DateTimeOffset MinYear + { + get => _minYear; + set + { + SetAndRaise(MinYearProperty, ref _minYear, value); + //Coerce date if needed + } + } + + /// + /// Gets or sets the month format + /// + public string MonthFormat + { + get => _monthFormat; + set => SetAndRaise(MonthFormatProperty, ref _monthFormat, value); + } + + /// + /// Gets or sets whether the month selector is visible + /// + public bool MonthVisible + { + get => _monthVisible; + set + { + SetAndRaise(MonthVisibleProperty, ref _monthVisible, value); + } + } + + /// + /// Gets or sets the year format + /// + public string YearFormat + { + get => _yearFormat; + set => SetAndRaise(YearFormatProperty, ref _yearFormat, value); + } + + /// + /// Gets or sets whether the year selector is visible + /// + public bool YearVisible + { + get => _yearVisible; + set + { + SetAndRaise(YearVisibleProperty, ref _yearVisible, value); + } + } + + /// + /// Gets or sets the item template for the YearSelector items + /// + public IDataTemplate YearSelectorItemTemplate + { + get => GetValue(YearSelectorItemTemplateProperty); + set => SetValue(YearSelectorItemTemplateProperty, value); + } + + /// + /// Gets or sets the item template for the MonthSelector items + /// + public IDataTemplate MonthSelectorItemTemplate + { + get => GetValue(MonthSelectorItemTemplateProperty); + set => SetValue(MonthSelectorItemTemplateProperty, value); + } + + /// + /// Gets or sets the item template for the DaySelector items + /// + public IDataTemplate DaySelectorItemTemplate + { + get => GetValue(DaySelectorItemTemplateProperty); + set => SetValue(DaySelectorItemTemplateProperty, value); + } + + /// + /// Raised when the AcceptButton is clicked or Enter is pressed + /// + public event EventHandler DatePicked; + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + //This is a requirement, so throw if not found + _pickerContainer = e.NameScope.Get("PickerContainer"); + + _acceptButton = e.NameScope.Find