diff --git a/Directory.Build.targets b/Directory.Build.targets index 73954c7f4d..e8d4baba11 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -1,5 +1,5 @@ - + $(DefineConstants);NET7SDK diff --git a/native/Avalonia.Native/src/OSX/AvnView.mm b/native/Avalonia.Native/src/OSX/AvnView.mm index fdc144e3a5..6d1ff7cf12 100644 --- a/native/Avalonia.Native/src/OSX/AvnView.mm +++ b/native/Avalonia.Native/src/OSX/AvnView.mm @@ -12,7 +12,6 @@ { ComPtr _parent; NSTrackingArea* _area; - NSMutableAttributedString* _markedText; bool _isLeftPressed, _isMiddlePressed, _isRightPressed, _isXButton1Pressed, _isXButton2Pressed; AvnInputModifiers _modifierState; NSEvent* _lastMouseDownEvent; @@ -22,8 +21,9 @@ AvnPlatformResizeReason _resizeReason; AvnAccessibilityElement* _accessibilityChild; NSRect _cursorRect; - NSMutableString* _text; - NSRange _selection; + NSMutableAttributedString* _text; + NSRange _selectedRange; + NSRange _markedRange; } - (void)onClosed @@ -59,6 +59,11 @@ [self registerForDraggedTypes: @[@"public.data", GetAvnCustomDataType()]]; _modifierState = AvnInputModifiersNone; + + _text = [[NSMutableAttributedString alloc] initWithString:@""]; + _markedRange = NSMakeRange(0, 0); + _selectedRange = NSMakeRange(0, 0); + return self; } @@ -521,9 +526,13 @@ - (void)keyDown:(NSEvent *)event { - [self keyboardEvent:event withType:KeyDown]; - _lastKeyHandled = [[self inputContext] handleEvent:event]; - [super keyDown:event]; + _lastKeyHandled = false; + + [[self inputContext] handleEvent:event]; + + if(!_lastKeyHandled){ + [self keyboardEvent:event withType:KeyDown]; + } } - (void)keyUp:(NSEvent *)event @@ -532,6 +541,10 @@ [super keyUp:event]; } +- (void) doCommandBySelector:(SEL)selector{ + +} + - (AvnInputModifiers)getModifiers:(NSEventModifierFlags)mod { unsigned int rv = 0; @@ -561,50 +574,52 @@ - (BOOL)hasMarkedText { - return [_markedText length] > 0; + return _markedRange.length > 0; } - (NSRange)markedRange { - if([_markedText length] > 0) - return NSMakeRange(0, [_markedText length] - 1); - return NSMakeRange(NSNotFound, 0); + return _markedRange; } - (NSRange)selectedRange { - return _selection; + return _selectedRange; } - (void)setMarkedText:(id)string selectedRange:(NSRange)selectedRange replacementRange:(NSRange)replacementRange { + _lastKeyHandled = true; + + NSString* markedText; + if([string isKindOfClass:[NSAttributedString class]]) { - _markedText = [[NSMutableAttributedString alloc] initWithAttributedString:string]; + markedText = [string string]; } else { - _markedText = [[NSMutableAttributedString alloc] initWithString:string]; + markedText = (NSString*) string; } - if(!_parent->InputMethod->IsActive()){ - return; + _markedRange = NSMakeRange(_selectedRange.location, [markedText length]); + + if(_parent->InputMethod->IsActive()){ + _parent->InputMethod->Client->SetPreeditText((char*)[markedText UTF8String]); } - - _parent->InputMethod->Client->SetPreeditText((char*)[_markedText.string UTF8String]); } - (void)unmarkText { - [[_markedText mutableString] setString:@""]; + if(_parent->InputMethod->IsActive()){ + _parent->InputMethod->Client->SetPreeditText(nullptr); + } - [[self inputContext] discardMarkedText]; + _markedRange = NSMakeRange(_selectedRange.location, 0); - if(!_parent->InputMethod->IsActive()){ - return; + if([self inputContext]) { + [[self inputContext] discardMarkedText]; } - - _parent->InputMethod->Client->SetPreeditText(nullptr); } - (NSArray *)validAttributesForMarkedText @@ -614,19 +629,38 @@ - (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)range actualRange:(NSRangePointer)actualRange { - return nullptr; + if(actualRange){ + range = *actualRange; + } + + NSAttributedString* subString = [_text attributedSubstringFromRange:range]; + + return subString; } - (void)insertText:(id)string replacementRange:(NSRange)replacementRange { - [self unmarkText]; - - if(_parent != nullptr) + if(_parent == nullptr){ + return; + } + + NSString* text; + + if([string isKindOfClass:[NSAttributedString class]]) { - _lastKeyHandled = _parent->BaseEvents->RawTextInputEvent(0, [string UTF8String]); + text = [string string]; + } + else + { + text = (NSString*) string; } - [[self inputContext] invalidateCharacterCoordinates]; + [self unmarkText]; + + uint32_t timestamp = static_cast([NSDate timeIntervalSinceReferenceDate] * 1000); + + _lastKeyHandled = _parent->BaseEvents->RawTextInputEvent(timestamp, [text UTF8String]); + } - (NSUInteger)characterIndexForPoint:(NSPoint)point @@ -746,15 +780,11 @@ } - (void) setText:(NSString *)text{ - [_text setString:text]; - - [[self inputContext] discardMarkedText]; + [[_text mutableString] setString:text]; } - (void) setSelection:(int)start :(int)end{ - _selection = NSMakeRange(start, end - start); - - [[self inputContext] invalidateCharacterCoordinates]; + _selectedRange = NSMakeRange(start, end - start); } - (void) setCursorRect:(AvnRect)rect{ @@ -766,7 +796,9 @@ _cursorRect = windowRectOnScreen; - [[self inputContext] invalidateCharacterCoordinates]; + if([self inputContext]) { + [[self inputContext] invalidateCharacterCoordinates]; + } } @end diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index 40232947d9..630c532686 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -273,6 +273,8 @@ partial class Build : NukeBuild if(!Numerge.NugetPackageMerger.Merge(Parameters.NugetIntermediateRoot, Parameters.NugetRoot, config, new NumergeNukeLogger())) throw new Exception("Package merge failed"); + RefAssemblyGenerator.GenerateRefAsmsInPackage(Parameters.NugetRoot / "Avalonia." + + Parameters.Version + ".nupkg"); }); Target RunTests => _ => _ diff --git a/nukebuild/Helpers.cs b/nukebuild/Helpers.cs new file mode 100644 index 0000000000..d8d06559bf --- /dev/null +++ b/nukebuild/Helpers.cs @@ -0,0 +1,24 @@ +using System; +using System.IO; +using Nuke.Common.Utilities; + +class Helpers +{ + public static IDisposable UseTempDir(out string dir) + { + var path = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(path); + dir = path; + return DelegateDisposable.CreateBracket(null, () => + { + try + { + Directory.Delete(path, true); + } + catch + { + // ignore + } + }); + } +} diff --git a/nukebuild/RefAssemblyGenerator.cs b/nukebuild/RefAssemblyGenerator.cs new file mode 100644 index 0000000000..cbe5236bca --- /dev/null +++ b/nukebuild/RefAssemblyGenerator.cs @@ -0,0 +1,171 @@ +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using ILRepacking; +using Mono.Cecil; +using Mono.Cecil.Cil; + +public class RefAssemblyGenerator +{ + class Resolver : DefaultAssemblyResolver, IAssemblyResolver + { + private readonly string _dir; + Dictionary _cache = new(); + + public Resolver(string dir) + { + _dir = dir; + } + + public override AssemblyDefinition Resolve(AssemblyNameReference name, ReaderParameters parameters) + { + if (_cache.TryGetValue(name.Name, out var asm)) + return asm; + var path = Path.Combine(_dir, name.Name + ".dll"); + if (File.Exists(path)) + return _cache[name.Name] = AssemblyDefinition.ReadAssembly(path, parameters); + return base.Resolve(name, parameters); + } + } + + public static void PatchRefAssembly(string file) + { + var reader = typeof(RefAssemblyGenerator).Assembly.GetManifestResourceStream("avalonia.snk"); + var snk = new byte[reader.Length]; + reader.Read(snk, 0, snk.Length); + + var def = AssemblyDefinition.ReadAssembly(file, new ReaderParameters + { + ReadWrite = true, + InMemory = true, + ReadSymbols = true, + SymbolReaderProvider = new DefaultSymbolReaderProvider(false), + AssemblyResolver = new Resolver(Path.GetDirectoryName(file)) + }); + + var obsoleteAttribute = def.MainModule.ImportReference(new TypeReference("System", "ObsoleteAttribute", def.MainModule, + def.MainModule.TypeSystem.CoreLibrary)); + var obsoleteCtor = def.MainModule.ImportReference(new MethodReference(".ctor", + def.MainModule.TypeSystem.Void, obsoleteAttribute) + { + Parameters = { new ParameterDefinition(def.MainModule.TypeSystem.String) } + }); + + foreach(var t in def.MainModule.Types) + ProcessType(t, obsoleteCtor); + def.Write(file, new WriterParameters() + { + StrongNameKeyBlob = snk, + WriteSymbols = def.MainModule.HasSymbols, + SymbolWriterProvider = new EmbeddedPortablePdbWriterProvider(), + DeterministicMvid = def.MainModule.HasSymbols + }); + } + + static void ProcessType(TypeDefinition type, MethodReference obsoleteCtor) + { + foreach (var nested in type.NestedTypes) + ProcessType(nested, obsoleteCtor); + if (type.IsInterface) + { + var hideMethods = type.Name.EndsWith("Impl") + || (type.HasCustomAttributes && type.CustomAttributes.Any(a => + a.AttributeType.FullName == "Avalonia.Metadata.PrivateApiAttribute")); + + var injectMethod = hideMethods + || type.CustomAttributes.Any(a => + a.AttributeType.FullName == "Avalonia.Metadata.NotClientImplementableAttribute"); + + if (hideMethods) + { + foreach (var m in type.Methods) + { + var dflags = MethodAttributes.Public | MethodAttributes.Family | MethodAttributes.FamORAssem | + MethodAttributes.FamANDAssem | MethodAttributes.Assembly; + m.Attributes = ((m.Attributes | dflags) ^ dflags) | MethodAttributes.Assembly; + } + } + + if(injectMethod) + { + type.Methods.Add(new MethodDefinition("NotClientImplementable", + MethodAttributes.Assembly + | MethodAttributes.Abstract + | MethodAttributes.NewSlot + | MethodAttributes.HideBySig, type.Module.TypeSystem.Void)); + } + + var forceUnstable = type.CustomAttributes.Any(a => + a.AttributeType.FullName == "Avalonia.Metadata.UnstableAttribute"); + + foreach (var m in type.Methods) + MarkAsUnstable(m, obsoleteCtor, forceUnstable); + foreach (var m in type.Properties) + MarkAsUnstable(m, obsoleteCtor, forceUnstable); + foreach (var m in type.Events) + MarkAsUnstable(m, obsoleteCtor, forceUnstable); + + } + } + + static void MarkAsUnstable(IMemberDefinition def, MethodReference obsoleteCtor, bool force) + { + if (!force && ( + def.HasCustomAttributes == false + || def.CustomAttributes.All(a => a.AttributeType.FullName != "Avalonia.Metadata.UnstableAttribute"))) + return; + + if (def.CustomAttributes.Any(a => a.AttributeType.FullName == "System.ObsoleteAttribute")) + return; + + def.CustomAttributes.Add(new CustomAttribute(obsoleteCtor) + { + ConstructorArguments = + { + new CustomAttributeArgument(obsoleteCtor.Module.TypeSystem.String, + "This is a part of unstable API and can be changed in minor releases. You have been warned") + } + }); + } + + public static void GenerateRefAsmsInPackage(string packagePath) + { + using (var archive = new ZipArchive(File.Open(packagePath, FileMode.Open, FileAccess.ReadWrite), + ZipArchiveMode.Update)) + { + foreach (var entry in archive.Entries.ToList()) + { + if (entry.FullName.StartsWith("ref/")) + entry.Delete(); + } + + foreach (var entry in archive.Entries.ToList()) + { + if (entry.FullName.StartsWith("lib/") && entry.Name.EndsWith(".xml")) + { + var newEntry = archive.CreateEntry("ref/" + entry.FullName.Substring(4), + CompressionLevel.Optimal); + using (var src = entry.Open()) + using (var dst = newEntry.Open()) + src.CopyTo(dst); + } + } + + var libs = archive.Entries.Where(e => e.FullName.StartsWith("lib/") && e.FullName.EndsWith(".dll")) + .Select((e => new { s = e.FullName.Split('/'), e = e })) + .Select(e => new { Tfm = e.s[1], Name = e.s[2], Entry = e.e }) + .GroupBy(x => x.Tfm); + foreach(var tfm in libs) + using (Helpers.UseTempDir(out var temp)) + { + foreach (var l in tfm) + l.Entry.ExtractToFile(Path.Combine(temp, l.Name)); + foreach (var l in tfm) + PatchRefAssembly(Path.Combine(temp, l.Name)); + foreach (var l in tfm) + archive.CreateEntryFromFile(Path.Combine(temp, l.Name), $"ref/{l.Tfm}/{l.Name}"); + } + } + } +} \ No newline at end of file diff --git a/nukebuild/_build.csproj b/nukebuild/_build.csproj index 13bac4b7db..d03746766e 100644 --- a/nukebuild/_build.csproj +++ b/nukebuild/_build.csproj @@ -31,18 +31,11 @@ - - - - - - - + + - - - + diff --git a/src/Avalonia.Base/Input/PointerEventArgs.cs b/src/Avalonia.Base/Input/PointerEventArgs.cs index 28a3c3aefb..beb953ce8f 100644 --- a/src/Avalonia.Base/Input/PointerEventArgs.cs +++ b/src/Avalonia.Base/Input/PointerEventArgs.cs @@ -77,14 +77,14 @@ namespace Avalonia.Input /// /// Gets the pointer position relative to a control. /// - /// The control. + /// The visual whose coordinate system to use. Pass null for toplevel coordinate system /// The pointer position in the control's coordinates. public Point GetPosition(Visual? relativeTo) => GetPosition(_rootVisualPosition, relativeTo); /// /// Returns the PointerPoint associated with the current event /// - /// The visual which coordinate system to use. Pass null for toplevel coordinate system + /// The visual whose coordinate system to use. Pass null for toplevel coordinate system /// public PointerPoint GetCurrentPoint(Visual? relativeTo) => new PointerPoint(Pointer, GetPosition(relativeTo), _properties); diff --git a/src/Avalonia.Base/Media/PolyLineSegment.cs b/src/Avalonia.Base/Media/PolyLineSegment.cs index 5c48c11e19..d17a621348 100644 --- a/src/Avalonia.Base/Media/PolyLineSegment.cs +++ b/src/Avalonia.Base/Media/PolyLineSegment.cs @@ -10,8 +10,8 @@ namespace Avalonia.Media /// /// Defines the property. /// - public static readonly StyledProperty PointsProperty - = AvaloniaProperty.Register(nameof(Points)); + public static readonly StyledProperty> PointsProperty + = AvaloniaProperty.Register>(nameof(Points)); /// /// Gets or sets the points. @@ -19,7 +19,7 @@ namespace Avalonia.Media /// /// The points. /// - public Points Points + public IList Points { get => GetValue(PointsProperty); set => SetValue(PointsProperty, value); @@ -37,9 +37,9 @@ namespace Avalonia.Media /// Initializes a new instance of the class. /// /// The points. - public PolyLineSegment(IEnumerable points) : this() + public PolyLineSegment(IEnumerable points) { - Points.AddRange(points); + Points = new Points(points); } protected internal override void ApplyTo(StreamGeometryContext ctx) diff --git a/src/Avalonia.Base/Media/PolylineGeometry.cs b/src/Avalonia.Base/Media/PolylineGeometry.cs index dd3c298b5b..b0229b6455 100644 --- a/src/Avalonia.Base/Media/PolylineGeometry.cs +++ b/src/Avalonia.Base/Media/PolylineGeometry.cs @@ -14,8 +14,8 @@ namespace Avalonia.Media /// /// Defines the property. /// - public static readonly DirectProperty PointsProperty = - AvaloniaProperty.RegisterDirect(nameof(Points), g => g.Points, (g, f) => g.Points = f); + public static readonly DirectProperty> PointsProperty = + AvaloniaProperty.RegisterDirect>(nameof(Points), g => g.Points, (g, f) => g.Points = f); /// /// Defines the property. @@ -23,13 +23,13 @@ namespace Avalonia.Media public static readonly StyledProperty IsFilledProperty = AvaloniaProperty.Register(nameof(IsFilled)); - private Points _points; + private IList _points; private IDisposable? _pointsObserver; static PolylineGeometry() { AffectsGeometry(IsFilledProperty); - PointsProperty.Changed.AddClassHandler((s, e) => s.OnPointsChanged(e.NewValue as Points)); + PointsProperty.Changed.AddClassHandler((s, e) => s.OnPointsChanged(e.NewValue as IList)); } /// @@ -43,9 +43,9 @@ namespace Avalonia.Media /// /// Initializes a new instance of the class. /// - public PolylineGeometry(IEnumerable points, bool isFilled) : this() + public PolylineGeometry(IEnumerable points, bool isFilled) { - Points.AddRange(points); + _points = new Points(points); IsFilled = isFilled; } @@ -56,7 +56,7 @@ namespace Avalonia.Media /// The points. /// [Content] - public Points Points + public IList Points { get => _points; set => SetAndRaise(PointsProperty, ref _points, value); @@ -97,10 +97,10 @@ namespace Avalonia.Media return geometry; } - private void OnPointsChanged(Points? newValue) + private void OnPointsChanged(IList? newValue) { _pointsObserver?.Dispose(); - _pointsObserver = newValue?.ForEachItem( + _pointsObserver = (newValue as IAvaloniaList)?.ForEachItem( _ => InvalidateGeometry(), _ => InvalidateGeometry(), InvalidateGeometry); diff --git a/src/Avalonia.Base/Metadata/PrivateApiAttribute.cs b/src/Avalonia.Base/Metadata/PrivateApiAttribute.cs new file mode 100644 index 0000000000..3f60940c5e --- /dev/null +++ b/src/Avalonia.Base/Metadata/PrivateApiAttribute.cs @@ -0,0 +1,9 @@ +using System; + +namespace Avalonia.Metadata; + +[AttributeUsage(AttributeTargets.Interface)] +public sealed class PrivateApiAttribute : Attribute +{ + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Platform/ICursorFactory.cs b/src/Avalonia.Base/Platform/ICursorFactory.cs index fff1f92d53..99a9a9d7fa 100644 --- a/src/Avalonia.Base/Platform/ICursorFactory.cs +++ b/src/Avalonia.Base/Platform/ICursorFactory.cs @@ -1,9 +1,11 @@ using Avalonia.Input; +using Avalonia.Metadata; #nullable enable namespace Avalonia.Platform { + [PrivateApi] public interface ICursorFactory { ICursorImpl GetCursor(StandardCursorType cursorType); diff --git a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs index 81fe2c046f..6f62c3be1d 100644 --- a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs @@ -11,7 +11,7 @@ namespace Avalonia.Platform /// /// Defines the main platform-specific interface for the rendering subsystem. /// - [Unstable] + [Unstable, PrivateApi] public interface IPlatformRenderInterface { /// @@ -201,7 +201,7 @@ namespace Avalonia.Platform bool IsSupportedBitmapPixelFormat(PixelFormat format); } - [Unstable] + [Unstable, PrivateApi] public interface IPlatformRenderInterfaceContext : IOptionalFeatureProvider, IDisposable { /// diff --git a/src/Avalonia.Base/Points.cs b/src/Avalonia.Base/Points.cs index b655dbcb38..2f88ecd80f 100644 --- a/src/Avalonia.Base/Points.cs +++ b/src/Avalonia.Base/Points.cs @@ -1,6 +1,18 @@ +using System.Collections.Generic; using Avalonia.Collections; namespace Avalonia { - public sealed class Points : AvaloniaList { } + public sealed class Points : AvaloniaList + { + public Points() + { + + } + + public Points(IEnumerable points) : base(points) + { + + } + } } diff --git a/src/Avalonia.Base/Threading/IDispatcherImpl.cs b/src/Avalonia.Base/Threading/IDispatcherImpl.cs index 4c30e2eb2c..ccbe3baf9a 100644 --- a/src/Avalonia.Base/Threading/IDispatcherImpl.cs +++ b/src/Avalonia.Base/Threading/IDispatcherImpl.cs @@ -6,7 +6,7 @@ using Avalonia.Platform; namespace Avalonia.Threading; -[Unstable] +[PrivateApi] public interface IDispatcherImpl { bool CurrentThreadIsLoopThread { get; } @@ -19,7 +19,7 @@ public interface IDispatcherImpl void UpdateTimer(long? dueTimeInMs); } -[Unstable] +[PrivateApi] public interface IDispatcherImplWithPendingInput : IDispatcherImpl { // Checks if dispatcher implementation can @@ -28,14 +28,14 @@ public interface IDispatcherImplWithPendingInput : IDispatcherImpl bool HasPendingInput { get; } } -[Unstable] +[PrivateApi] public interface IDispatcherImplWithExplicitBackgroundProcessing : IDispatcherImpl { event Action ReadyForBackgroundProcessing; void RequestBackgroundProcessing(); } -[Unstable] +[PrivateApi] public interface IControlledDispatcherImpl : IDispatcherImplWithPendingInput { // Runs the event loop diff --git a/src/Avalonia.Controls/Platform/IPlatformIconLoader.cs b/src/Avalonia.Controls/Platform/IPlatformIconLoader.cs index 4c844ce30f..2ff74cc582 100644 --- a/src/Avalonia.Controls/Platform/IPlatformIconLoader.cs +++ b/src/Avalonia.Controls/Platform/IPlatformIconLoader.cs @@ -3,7 +3,7 @@ using Avalonia.Metadata; namespace Avalonia.Platform { - [Unstable] + [Unstable, PrivateApi] public interface IPlatformIconLoader { IWindowIconImpl LoadIcon(string fileName); diff --git a/src/Avalonia.Controls/Platform/IWindowingPlatform.cs b/src/Avalonia.Controls/Platform/IWindowingPlatform.cs index 5acc5adccd..f6cf8c604e 100644 --- a/src/Avalonia.Controls/Platform/IWindowingPlatform.cs +++ b/src/Avalonia.Controls/Platform/IWindowingPlatform.cs @@ -2,7 +2,7 @@ using Avalonia.Metadata; namespace Avalonia.Platform { - [Unstable] + [Unstable, PrivateApi] public interface IWindowingPlatform { IWindowImpl CreateWindow(); diff --git a/src/Avalonia.Controls/Primitives/Thumb.cs b/src/Avalonia.Controls/Primitives/Thumb.cs index 993d054f87..9854bdbea6 100644 --- a/src/Avalonia.Controls/Primitives/Thumb.cs +++ b/src/Avalonia.Controls/Primitives/Thumb.cs @@ -85,7 +85,7 @@ namespace Avalonia.Controls.Primitives { if (_lastPoint.HasValue) { - var point = e.GetPosition(this.GetVisualParent()); + var point = e.GetPosition(null); var ev = new VectorEventArgs { RoutedEvent = DragDeltaEvent, @@ -100,7 +100,7 @@ namespace Avalonia.Controls.Primitives protected override void OnPointerPressed(PointerPressedEventArgs e) { e.Handled = true; - _lastPoint = e.GetPosition(this.GetVisualParent()); + _lastPoint = e.GetPosition(null); var ev = new VectorEventArgs { @@ -123,7 +123,7 @@ namespace Avalonia.Controls.Primitives var ev = new VectorEventArgs { RoutedEvent = DragCompletedEvent, - Vector = (Vector)e.GetPosition(this.GetVisualParent()), + Vector = (Vector)e.GetPosition(null), }; RaiseEvent(ev); diff --git a/src/Avalonia.Controls/Shapes/Polygon.cs b/src/Avalonia.Controls/Shapes/Polygon.cs index 70a45f3516..78def84448 100644 --- a/src/Avalonia.Controls/Shapes/Polygon.cs +++ b/src/Avalonia.Controls/Shapes/Polygon.cs @@ -13,10 +13,15 @@ namespace Avalonia.Controls.Shapes AffectsGeometry(PointsProperty); } + public Polygon() + { + Points = new Points(); + } + public IList Points { - get { return GetValue(PointsProperty); } - set { SetValue(PointsProperty, value); } + get => GetValue(PointsProperty); + set => SetValue(PointsProperty, value); } protected override Geometry CreateDefiningGeometry() diff --git a/src/Avalonia.Controls/Shapes/Polyline.cs b/src/Avalonia.Controls/Shapes/Polyline.cs index 4b4bb3ffd0..2533794f89 100644 --- a/src/Avalonia.Controls/Shapes/Polyline.cs +++ b/src/Avalonia.Controls/Shapes/Polyline.cs @@ -14,10 +14,15 @@ namespace Avalonia.Controls.Shapes AffectsGeometry(PointsProperty); } + public Polyline() + { + Points = new Points(); + } + public IList Points { - get { return GetValue(PointsProperty); } - set { SetValue(PointsProperty, value); } + get => GetValue(PointsProperty); + set => SetValue(PointsProperty, value); } protected override Geometry CreateDefiningGeometry() diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AstNodes/AvaloniaXamlIlArrayConstantAstNode.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AstNodes/AvaloniaXamlIlArrayConstantAstNode.cs new file mode 100644 index 0000000000..339b720d10 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AstNodes/AvaloniaXamlIlArrayConstantAstNode.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using System.Reflection.Emit; +using Avalonia.Controls; +using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; +using XamlX; +using XamlX.Ast; +using XamlX.Emit; +using XamlX.IL; +using XamlX.Transform; +using XamlX.TypeSystem; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.AstNodes +{ + class AvaloniaXamlIlArrayConstantAstNode : XamlAstNode, IXamlAstValueNode, IXamlAstILEmitableNode + { + private readonly IXamlType _elementType; + private readonly IReadOnlyList _values; + + public AvaloniaXamlIlArrayConstantAstNode(IXamlLineInfo lineInfo, IXamlType arrayType, IXamlType elementType, IReadOnlyList values) : base(lineInfo) + { + _elementType = elementType; + _values = values; + + Type = new XamlAstClrTypeReference(lineInfo, arrayType, false); + + foreach (var element in values) + { + if (!elementType.IsAssignableFrom(element.Type.GetClrType())) + { + throw new XamlParseException("x:Array element is not assignable to the array element type!", lineInfo); + } + } + } + + public IXamlAstTypeReference Type { get; } + + public XamlILNodeEmitResult Emit(XamlEmitContext context, IXamlILEmitter codeGen) + { + codeGen.Ldc_I4(_values.Count) + .Newarr(_elementType); + + for (var index = 0; index < _values.Count; index++) + { + var value = _values[index]; + + codeGen + .Dup() + .Ldc_I4(index); + + context.Emit(value, codeGen, _elementType); + + if (value.Type.GetClrType() is { IsValueType: true } valTypeInObjArr) + { + if (!_elementType.IsValueType) + { + codeGen.Box(valTypeInObjArr); + } + // It seems like ASM codegen for "stelem valuetype" and "stelem.i4" is identical, + // so we don't need to try to optimize it here. + codeGen.Emit(OpCodes.Stelem, valTypeInObjArr); + } + else + { + codeGen.Stelem_ref(); + } + } + + return XamlILNodeEmitResult.Type(0, Type.GetClrType()); + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs index d8524cfd88..cd005ce24d 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs @@ -198,6 +198,29 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions throw new XamlX.XamlLoadException($"Unable to parse \"{text}\" as a grid length", node); } } + + if (type.Equals(types.ColumnDefinition) || type.Equals(types.RowDefinition)) + { + try + { + var gridLength = GridLength.Parse(text); + + result = new AvaloniaXamlIlGridLengthAstNode(node, types, gridLength); + + var definitionConstructorGridLength = type.GetConstructor(new List {types.GridLength}); + var lengthNode = new AvaloniaXamlIlGridLengthAstNode(node, types, gridLength); + var definitionTypeRef = new XamlAstClrTypeReference(node, type, false); + + result = new XamlAstNewClrObjectNode(node, definitionTypeRef, + definitionConstructorGridLength, new List {lengthNode}); + + return true; + } + catch + { + throw new XamlX.XamlLoadException($"Unable to parse \"{text}\" as a grid length", node); + } + } if (type.Equals(types.Cursor)) { @@ -211,16 +234,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions } } - if (type.Equals(types.ColumnDefinitions)) - { - return ConvertDefinitionList(node, text, types, types.ColumnDefinitions, types.ColumnDefinition, "column definitions", out result); - } - - if (type.Equals(types.RowDefinitions)) - { - return ConvertDefinitionList(node, text, types, types.RowDefinitions, types.RowDefinition, "row definitions", out result); - } - if (types.IBrush.IsAssignableFrom(type)) { if (Color.TryParse(text, out Color color)) @@ -295,46 +308,89 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions } } - result = null; - return false; - } - - private static bool ConvertDefinitionList( - IXamlAstValueNode node, - string text, - AvaloniaXamlIlWellKnownTypes types, - IXamlType listType, - IXamlType elementType, - string errorDisplayName, - out IXamlAstValueNode result) - { - try + // Keep it in the end, so more specific parsers can be applied. + var elementType = GetElementType(type, context.Configuration.WellKnownTypes); + if (elementType is not null) { - var lengths = GridLength.ParseLengths(text); - - var definitionTypeRef = new XamlAstClrTypeReference(node, elementType, false); + string[] items; + // Normalize special case of Points collection. + if (elementType == types.Point) + { + var pointParts = text.Split(new[] { ",", " " }, StringSplitOptions.RemoveEmptyEntries); + if (pointParts.Length % 2 == 0) + { + items = new string[pointParts.Length / 2]; + for (int i = 0; i < pointParts.Length; i += 2) + { + items[i / 2] = string.Format(CultureInfo.InvariantCulture, "{0} {1}", pointParts[i], + pointParts[i + 1]); + } + } + else + { + throw new XamlX.XamlLoadException($"Invalid PointsList.", node); + } + } + else + { + const StringSplitOptions trimOption = (StringSplitOptions)2; // StringSplitOptions.TrimEntries + var separators = new[] { "," }; + var splitOptions = StringSplitOptions.RemoveEmptyEntries | trimOption; - var definitionConstructorGridLength = elementType.GetConstructor(new List {types.GridLength}); + items = text.Split(separators, splitOptions ^ trimOption); + // Compiler targets netstandard, so we need to emulate StringSplitOptions.TrimEntries, if it was requested. + if (splitOptions.HasFlag(trimOption)) + { + items = items.Select(i => i.Trim()).ToArray(); + } + } - IXamlAstValueNode CreateDefinitionNode(GridLength length) + var nodes = new IXamlAstValueNode[items.Length]; + for (var index = 0; index < items.Length; index++) { - var lengthNode = new AvaloniaXamlIlGridLengthAstNode(node, types, length); + var success = XamlTransformHelpers.TryGetCorrectlyTypedValue( + context, + new XamlAstTextNode(node, items[index], true, context.Configuration.WellKnownTypes.String), + elementType, out var itemNode); + if (!success) + { + result = null; + return false; + } - return new XamlAstNewClrObjectNode(node, definitionTypeRef, - definitionConstructorGridLength, new List {lengthNode}); + nodes[index] = itemNode; } - var definitionNodes = - new List(lengths.Select(CreateDefinitionNode)); - - result = new AvaloniaXamlIlAvaloniaListConstantAstNode(node, types, listType, elementType, definitionNodes); + if (types.AvaloniaList.MakeGenericType(elementType).IsAssignableFrom(type)) + { + result = new AvaloniaXamlIlAvaloniaListConstantAstNode(node, types, type, elementType, nodes); + return true; + } + else if (type.IsArray) + { + result = new AvaloniaXamlIlArrayConstantAstNode(node, elementType.MakeArrayType(1), elementType, nodes); + return true; + } + else if (type == context.Configuration.WellKnownTypes.IListOfT.MakeGenericType(elementType)) + { + var listType = context.Configuration.WellKnownTypes.IListOfT.MakeGenericType(elementType); + result = new AvaloniaXamlIlArrayConstantAstNode(node, listType, elementType, nodes); + return true; + } - return true; - } - catch - { - throw new XamlX.XamlLoadException($"Unable to parse \"{text}\" as a {errorDisplayName}", node); + result = null; + return false; } + + result = null; + return false; + } + + private static IXamlType GetElementType(IXamlType type, XamlTypeWellKnownTypes types) + { + return type.GetAllInterfaces().FirstOrDefault(i => + i.FullName.StartsWith(types.IEnumerableT.FullName))? + .GenericArguments[0]; } } } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index 60a7d953ab..63683da0db 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -33,6 +33,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlType InheritDataTypeFromItemsAttribute { get; } public IXamlType MarkupExtensionOptionAttribute { get; } public IXamlType MarkupExtensionDefaultOptionAttribute { get; } + public IXamlType AvaloniaList { get; } public IXamlType OnExtensionType { get; } public IXamlType UnsetValueType { get; } public IXamlType StyledElement { get; } @@ -141,6 +142,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers InheritDataTypeFromItemsAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.InheritDataTypeFromItemsAttribute"); MarkupExtensionOptionAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.MarkupExtensionOptionAttribute"); MarkupExtensionDefaultOptionAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.MarkupExtensionDefaultOptionAttribute"); + AvaloniaList = cfg.TypeSystem.GetType("Avalonia.Collections.AvaloniaList`1"); OnExtensionType = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.On"); AvaloniaObjectBindMethod = AvaloniaObjectExtensions.FindMethod("Bind", IDisposable, false, AvaloniaObject, AvaloniaProperty, diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github b/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github index 5dd0b042e1..5d1025f30d 160000 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github @@ -1 +1 @@ -Subproject commit 5dd0b042e144e677638224c49fec16dab66143e8 +Subproject commit 5d1025f30d0ed6d8f419d82959c148276301f393 diff --git a/tests/Avalonia.RenderTests/Shapes/PolygonTests.cs b/tests/Avalonia.RenderTests/Shapes/PolygonTests.cs index 3ac884df7d..b918f7180a 100644 --- a/tests/Avalonia.RenderTests/Shapes/PolygonTests.cs +++ b/tests/Avalonia.RenderTests/Shapes/PolygonTests.cs @@ -30,7 +30,7 @@ namespace Avalonia.Direct2D1.RenderTests.Shapes Stroke = Brushes.DarkBlue, Stretch = Stretch.Uniform, Fill = Brushes.Violet, - Points = new [] { new Point(5, 0), new Point(8, 8), new Point(0, 3), new Point(10, 3), new Point(2, 8) }, + Points = new Points { new Point(5, 0), new Point(8, 8), new Point(0, 3), new Point(10, 3), new Point(2, 8) }, StrokeThickness = 1 } }; @@ -52,7 +52,7 @@ namespace Avalonia.Direct2D1.RenderTests.Shapes Stroke = Brushes.DarkBlue, Stretch = Stretch.Fill, Fill = Brushes.Violet, - Points = new[] { new Point(5, 0), new Point(8, 8), new Point(0, 3), new Point(10, 3), new Point(2, 8) }, + Points = new Points { new Point(5, 0), new Point(8, 8), new Point(0, 3), new Point(10, 3), new Point(2, 8) }, StrokeThickness = 5, } }; diff --git a/tests/Avalonia.RenderTests/Shapes/PolylineTests.cs b/tests/Avalonia.RenderTests/Shapes/PolylineTests.cs index d02d494ff2..12420b524a 100644 --- a/tests/Avalonia.RenderTests/Shapes/PolylineTests.cs +++ b/tests/Avalonia.RenderTests/Shapes/PolylineTests.cs @@ -20,7 +20,7 @@ namespace Avalonia.Direct2D1.RenderTests.Shapes [Fact] public async Task Polyline_1px_Stroke() { - var polylinePoints = new Point[] { new Point(0, 0), new Point(5, 0), new Point(6, -2), new Point(7, 3), new Point(8, -3), + var polylinePoints = new Points { new Point(0, 0), new Point(5, 0), new Point(6, -2), new Point(7, 3), new Point(8, -3), new Point(9, 1), new Point(10, 0), new Point(15, 0) }; Decorator target = new Decorator @@ -44,7 +44,7 @@ namespace Avalonia.Direct2D1.RenderTests.Shapes [Fact] public async Task Polyline_10px_Stroke_PenLineJoin() { - var polylinePoints = new Point[] { new Point(0, 0), new Point(5, 0), new Point(6, -2), new Point(7, 3), new Point(8, -3), + var polylinePoints = new Points { new Point(0, 0), new Point(5, 0), new Point(6, -2), new Point(7, 3), new Point(8, -3), new Point(9, 1), new Point(10, 0), new Point(15, 0) }; Decorator target = new Decorator