فهرست منبع

Merge branch 'refs/heads/avalonia-rewrite' into structure-member-outputs

# Conflicts:
#	src/PixiEditor.AvaloniaUI/Helpers/ServiceCollectionHelpers.cs
#	src/PixiEditor.Numerics/ColorMatrix.cs
CPKreuz 1 سال پیش
والد
کامیت
2e076eb700
82فایلهای تغییر یافته به همراه1267 افزوده شده و 505 حذف شده
  1. 5 0
      samples/Sample7_FlyUI/WindowContentElement.cs
  2. 7 0
      src/ChunkyImageLib/ChunkyImage.cs
  3. 23 0
      src/PixiEditor.AvaloniaUI/Helpers/Converters/FrameToTimeConverter.cs
  4. 13 6
      src/PixiEditor.AvaloniaUI/Helpers/Converters/TimelineSliderValueToMarginConverter.cs
  5. 11 2
      src/PixiEditor.AvaloniaUI/Helpers/DocumentViewModelBuilder.cs
  6. 2 2
      src/PixiEditor.AvaloniaUI/Helpers/Extensions/PixiParserDocumentEx.cs
  7. 4 3
      src/PixiEditor.AvaloniaUI/Helpers/Extensions/PixiParserPixiV4DocumentEx.cs
  8. 1 1
      src/PixiEditor.AvaloniaUI/Helpers/ServiceCollectionHelpers.cs
  9. 1 0
      src/PixiEditor.AvaloniaUI/Initialization/ClassicDesktopEntry.cs
  10. 1 18
      src/PixiEditor.AvaloniaUI/Models/Controllers/ClipboardController.cs
  11. 1 1
      src/PixiEditor.AvaloniaUI/Models/Dialogs/SizeUnit.cs
  12. 1 1
      src/PixiEditor.AvaloniaUI/Models/ExceptionHandling/CrashReport.cs
  13. 1 1
      src/PixiEditor.AvaloniaUI/Models/Files/PixiFileType.cs
  14. 47 34
      src/PixiEditor.AvaloniaUI/Models/IO/Importer.cs
  15. 13 3
      src/PixiEditor.AvaloniaUI/Models/IO/PaletteParsers/PixiPaletteParser.cs
  16. 41 17
      src/PixiEditor.AvaloniaUI/Models/Rendering/MemberPreviewUpdater.cs
  17. 0 36
      src/PixiEditor.AvaloniaUI/Models/Serialization/Factories/ColorMatrixFactory.cs
  18. 51 0
      src/PixiEditor.AvaloniaUI/Models/Serialization/Factories/ColorMatrixSerializationFactory.cs
  19. 3 27
      src/PixiEditor.AvaloniaUI/Models/UserData/RecentlyOpenedDocument.cs
  20. 27 3
      src/PixiEditor.AvaloniaUI/Styles/PortingWipStyles.axaml
  21. 16 3
      src/PixiEditor.AvaloniaUI/Styles/Templates/KeyFrame.axaml
  22. 115 65
      src/PixiEditor.AvaloniaUI/Styles/Templates/Timeline.axaml
  23. 31 5
      src/PixiEditor.AvaloniaUI/Styles/Templates/TimelineGroupHeader.axaml
  24. 4 2
      src/PixiEditor.AvaloniaUI/Styles/Templates/TimelineSlider.axaml
  25. 2 2
      src/PixiEditor.AvaloniaUI/ViewModels/Dock/LayoutManager.cs
  26. 1 1
      src/PixiEditor.AvaloniaUI/ViewModels/Document/AnimationDataViewModel.cs
  27. 22 29
      src/PixiEditor.AvaloniaUI/ViewModels/Document/DocumentViewModel.Serialization.cs
  28. 0 1
      src/PixiEditor.AvaloniaUI/ViewModels/Document/DocumentViewModel.cs
  29. 38 0
      src/PixiEditor.AvaloniaUI/ViewModels/Document/KeyFrameGroupViewModel.cs
  30. 10 3
      src/PixiEditor.AvaloniaUI/ViewModels/Document/KeyFrameViewModel.cs
  31. 1 0
      src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/ClipboardViewModel.cs
  32. 2 2
      src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/FileViewModel.cs
  33. 2 2
      src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/WindowViewModel.cs
  34. 20 0
      src/PixiEditor.AvaloniaUI/Views/Animations/KeyFrame.cs
  35. 23 1
      src/PixiEditor.AvaloniaUI/Views/Animations/Timeline.cs
  36. 22 1
      src/PixiEditor.AvaloniaUI/Views/Animations/TimelineGroupHeader.cs
  37. 14 5
      src/PixiEditor.AvaloniaUI/Views/Animations/TimelineTickBar.cs
  38. 67 52
      src/PixiEditor.AvaloniaUI/Views/Input/NumberInput.cs
  39. 4 3
      src/PixiEditor.AvaloniaUI/Views/Input/SizeInput.axaml
  40. 5 45
      src/PixiEditor.AvaloniaUI/Views/Input/SizeInput.axaml.cs
  41. 1 1
      src/PixiEditor.AvaloniaUI/Views/Input/SizePicker.axaml
  42. 2 2
      src/PixiEditor.AvaloniaUI/Views/Main/ViewportControls/Viewport.axaml
  43. 2 2
      src/PixiEditor.AvaloniaUI/Views/MainView.axaml
  44. 1 1
      src/PixiEditor.AvaloniaUI/Views/Rendering/Scene.cs
  45. 1 0
      src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IReadOnlyKeyFrameData.cs
  46. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Animation/CreateRasterKeyFrame_Change.cs
  47. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Drawing/ApplyLayerMask_Change.cs
  48. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Drawing/DrawingChangeHelper.cs
  49. 4 4
      src/PixiEditor.ChangeableDocument/Changes/Properties/LayerStructure/DeleteStructureMemberMask_Change.cs
  50. 2 2
      src/PixiEditor.ChangeableDocument/Changes/Root/ClipCanvas_Change.cs
  51. 2 2
      src/PixiEditor.ChangeableDocument/Changes/Root/Crop_Change.cs
  52. 4 4
      src/PixiEditor.ChangeableDocument/Changes/Root/FlipImage_Change.cs
  53. 4 4
      src/PixiEditor.ChangeableDocument/Changes/Root/ResizeBasedChangeBase.cs
  54. 25 12
      src/PixiEditor.ChangeableDocument/Changes/Root/RotateImage_Change.cs
  55. 29 0
      src/PixiEditor.ClosedBeta/ClosedBetaExtension.cs
  56. 30 0
      src/PixiEditor.ClosedBeta/PixiEditor.ClosedBeta.csproj
  57. 9 0
      src/PixiEditor.ClosedBeta/Program.cs
  58. 15 0
      src/PixiEditor.ClosedBeta/WelcomeMessage.cs
  59. 77 0
      src/PixiEditor.ClosedBeta/WelcomeMessageState.cs
  60. 20 0
      src/PixiEditor.ClosedBeta/extension.json
  61. 1 0
      src/PixiEditor.Extensions.CommonApi/FlyUI/Events/ElementEventArgs.cs
  62. 11 0
      src/PixiEditor.Extensions.CommonApi/FlyUI/Events/ToggleEventArgs.cs
  63. 41 1
      src/PixiEditor.Extensions.CommonApi/Palettes/PaletteColor.Impl.cs
  64. 44 0
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/CheckBox.cs
  65. 31 0
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/LayoutElement.cs
  66. 1 1
      src/PixiEditor.Extensions.Sdk/Bridge/Native.cs
  67. 4 2
      src/PixiEditor.Extensions/FlyUI/Converters/EnumToEnumConverter.cs
  68. 14 3
      src/PixiEditor.Extensions/FlyUI/Elements/Align.cs
  69. 13 1
      src/PixiEditor.Extensions/FlyUI/Elements/Border.cs
  70. 15 4
      src/PixiEditor.Extensions/FlyUI/Elements/Button.cs
  71. 10 18
      src/PixiEditor.Extensions/FlyUI/Elements/Center.cs
  72. 39 0
      src/PixiEditor.Extensions/FlyUI/Elements/CheckBox.cs
  73. 22 17
      src/PixiEditor.Extensions/FlyUI/Elements/Container.cs
  74. 12 1
      src/PixiEditor.Extensions/FlyUI/Elements/Layout.cs
  75. 30 0
      src/PixiEditor.Extensions/FlyUI/Elements/LayoutElement.cs
  76. 5 0
      src/PixiEditor.Extensions/FlyUI/Elements/MultiChildLayoutElement.cs
  77. 15 4
      src/PixiEditor.Extensions/FlyUI/Elements/Padding.cs
  78. 8 1
      src/PixiEditor.Extensions/FlyUI/Elements/SingleChildLayoutElement.cs
  79. 27 30
      src/PixiEditor.Numerics/ColorMatrix.cs
  80. 4 4
      src/PixiEditor.UI.Common/Fonts/PixiPerfectIcons.axaml
  81. 4 4
      src/PixiEditor.UI.Common/Fonts/PixiPerfectIcons.axaml.cs
  82. 33 0
      src/PixiEditor.sln

+ 5 - 0
samples/Sample7_FlyUI/WindowContentElement.cs

@@ -1,4 +1,5 @@
 using PixiEditor.Extensions.CommonApi.FlyUI.Properties;
+using PixiEditor.Extensions.Sdk;
 using PixiEditor.Extensions.Sdk.Api.FlyUI;
 using PixiEditor.Extensions.Sdk.Api.Window;
 
@@ -31,6 +32,10 @@ public class WindowContentElement : StatelessElement
                                 filterQuality: FilterQuality.None,
                                 width: 256, height: 256))
                     ),
+                    new CheckBox(new Text("heloo"), onCheckedChanged: args =>
+                    {
+                        PixiEditorExtension.Api.Logger.Log(((CheckBox)args.Sender).IsChecked ? "Checked" : "Unchecked");
+                    }),
                     new Center(
                         new Button(
                             child: new Text("Close"), onClick: _ =>

+ 7 - 0
src/ChunkyImageLib/ChunkyImage.cs

@@ -9,6 +9,7 @@ using PixiEditor.DrawingApi.Core;
 using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Surfaces;
+using PixiEditor.DrawingApi.Core.Surfaces.ImageData;
 using PixiEditor.DrawingApi.Core.Surfaces.PaintImpl;
 using PixiEditor.DrawingApi.Core.Surfaces.Vector;
 using PixiEditor.Numerics;
@@ -125,6 +126,12 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
         };
     }
 
+    public ChunkyImage(Surface image) : this(image.Size)
+    {
+        EnqueueDrawImage(VecI.Zero, image);
+        CommitChanges();
+    }
+
     /// <exception cref="ObjectDisposedException">This image is disposed</exception>
     public RectI? FindChunkAlignedMostUpToDateBounds()
     {

+ 23 - 0
src/PixiEditor.AvaloniaUI/Helpers/Converters/FrameToTimeConverter.cs

@@ -0,0 +1,23 @@
+using System.Globalization;
+
+namespace PixiEditor.AvaloniaUI.Helpers.Converters;
+
+internal class FrameToTimeConverter : SingleInstanceMultiValueConverter<FrameToTimeConverter>
+{
+    public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+    {
+        return Convert(new[] { value }, targetType, parameter, culture); 
+    }
+
+    public override object? Convert(IList<object?> values, Type targetType, object? parameter, CultureInfo culture)
+    {
+        if(values.Count < 2) return null;
+        
+        if (values[0] is int frame && values[1] is int fps)
+        {
+            return TimeSpan.FromSeconds(frame / (double)fps).ToString("mm\\:ss\\.ff");
+        }
+        
+        return null;
+    }
+}

+ 13 - 6
src/PixiEditor.AvaloniaUI/Helpers/Converters/TimelineSliderValueToMarginConverter.cs

@@ -3,7 +3,8 @@ using Avalonia;
 
 namespace PixiEditor.AvaloniaUI.Helpers.Converters;
 
-internal class TimelineSliderValueToMarginConverter : SingleInstanceMultiValueConverter<TimelineSliderValueToMarginConverter>
+internal class
+    TimelineSliderValueToMarginConverter : SingleInstanceMultiValueConverter<TimelineSliderValueToMarginConverter>
 {
     public override object Convert(object value, Type targetType, object parameter, CultureInfo culture)
     {
@@ -12,14 +13,20 @@ internal class TimelineSliderValueToMarginConverter : SingleInstanceMultiValueCo
 
     public override object? Convert(IList<object?> values, Type targetType, object? parameter, CultureInfo culture)
     {
-        if (values.Count != 4)
+        if (values.Count == 3)
         {
-            throw new ArgumentException("TimelineSliderValueToMarginConverter requires 3 values");
+            if (values[0] is double minimum && values[1] is double scale && values[2] is Vector offset)
+            {
+                return new Thickness((-minimum) * scale - offset.X, 0, 0, 0);
+            }
         }
-
-        if (values[0] is int frame && values[1] is double minimum && values[2] is double scale && values[3] is Vector offset)
+        else if (values.Count == 4)
         {
-            return new Thickness((frame - minimum) * scale - offset.X, 0, 0, 0);
+            if (values[0] is int frame && values[1] is double minimum && values[2] is double scale &&
+                values[3] is Vector offset)
+            {
+                return new Thickness((frame - minimum) * scale - offset.X, 0, 0, 0);
+            }
         }
 
         return new Thickness();

+ 11 - 2
src/PixiEditor.AvaloniaUI/Helpers/DocumentViewModelBuilder.cs

@@ -285,8 +285,17 @@ internal class NodeGraphBuilder
             this.WithNodeOfType(typeof(ImageLayerNode))
                 .WithName(name)
                 .WithId(AllNodes.Count + 1)
-                .WithAdditionalData(
-                    new Dictionary<string, object> { { ImageLayerNode.ImageFramesKey, new List<Surface> { image } } }));
+                .WithKeyFrames(
+                [
+                    new KeyFrameData
+                        {
+                            AffectedElement = ImageLayerNode.ImageLayerKey,
+                            Data = new ChunkyImage(image),
+                            Duration = 0,
+                            StartFrame = 0,
+                            IsVisible = true
+                        }
+                ]));
 
         id = AllNodes.Count;
         return this;

+ 2 - 2
src/PixiEditor.AvaloniaUI/Helpers/Extensions/PixiParserDocumentEx.cs

@@ -77,13 +77,13 @@ internal static class PixiParserDocumentEx
         DocumentViewModelBuilder.ReferenceLayerBuilder layerBuilder,
         ImageEncoder encoder)
     {
-        DecodeSurface(referenceLayer.ImageBytes, (int)referenceLayer.Width, (int)referenceLayer.Height, encoder);
+        var surface = DecodeSurface(referenceLayer.ImageBytes, referenceLayer.ImageWidth, referenceLayer.ImageHeight, encoder);
 
         layerBuilder
             .WithIsVisible(referenceLayer.Enabled)
             .WithShape(referenceLayer.Corners)
             .WithIsTopmost(referenceLayer.Topmost)
-            .WithSurface(Surface.Load(referenceLayer.ImageBytes));
+            .WithSurface(surface);
     }
 
     private static Surface DecodeSurface(byte[] imgBytes, int width, int height, ImageEncoder encoder)

+ 4 - 3
src/PixiEditor.AvaloniaUI/Helpers/Extensions/PixiParserV3DocumentEx.cs → src/PixiEditor.AvaloniaUI/Helpers/Extensions/PixiParserPixiV4DocumentEx.cs

@@ -6,15 +6,16 @@ using PixiEditor.DrawingApi.Core.Surfaces;
 using PixiEditor.Extensions.CommonApi.Palettes;
 using PixiEditor.Numerics;
 using PixiEditor.Parser;
-using PixiEditor.Parser.Deprecated;
+using PixiEditor.Parser.Old.PixiV4;
 using BlendMode = PixiEditor.Parser.BlendMode;
 
 namespace PixiEditor.AvaloniaUI.Helpers.Extensions;
 
-internal static class PixiParserV3DocumentEx
+internal static class PixiParserPixiV4DocumentEx
 {
-    public static DocumentViewModel ToDocument(this DeprecatedDocument document)
+    public static DocumentViewModel ToDocument(this DocumentV4 document)
     {
+        // TODO: Implement?
         return DocumentViewModel.Build(b =>
         {
             /*b.WithSize(document.Width, document.Height)

+ 1 - 1
src/PixiEditor.AvaloniaUI/Helpers/ServiceCollectionHelpers.cs

@@ -119,7 +119,7 @@ internal static class ServiceCollectionHelpers
             .AddSingleton<SerializationFactory, VecDSerializationFactory>()
             .AddSingleton<SerializationFactory, VecISerializationFactory>()
             .AddSingleton<SerializationFactory, ColorSerializationFactory>()
-            .AddSingleton<SerializationFactory, ColorMatrixFactory>()
+            .AddSingleton<SerializationFactory, ColorMatrixSerializationFactory>()
             // Palette Parsers
             .AddSingleton<IPalettesProvider, PaletteProvider>()
             .AddSingleton<PaletteFileParser, JascFileParser>()

+ 1 - 0
src/PixiEditor.AvaloniaUI/Initialization/ClassicDesktopEntry.cs

@@ -95,6 +95,7 @@ internal class ClassicDesktopEntry
         //TODO: fetch from extension store
         extensionLoader.AddOfficialExtension("pixieditor.supporterpack",
             new OfficialExtensionData("supporter-pack.snk", AdditionalContentProduct.SupporterPack));
+        extensionLoader.AddOfficialExtension("pixieditor.closedbeta1", new OfficialExtensionData());
         extensionLoader.LoadExtensions();
 
         return extensionLoader;

+ 1 - 18
src/PixiEditor.AvaloniaUI/Models/Controllers/ClipboardController.cs

@@ -24,7 +24,6 @@ using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Surfaces.ImageData;
 using PixiEditor.Numerics;
 using PixiEditor.Parser;
-using PixiEditor.Parser.Deprecated;
 using Bitmap = Avalonia.Media.Imaging.Bitmap;
 
 namespace PixiEditor.AvaloniaUI.Models.Controllers;
@@ -208,23 +207,7 @@ internal static class ClipboardController
                 {
                     using var stream = new FileStream(path, FileMode.Open, FileAccess.Read);
 
-                    try
-                    {
-                        imported = Surface.Load(PixiParser.Deserialize(path).PreviewImage);
-                    }
-                    catch (InvalidFileException e)
-                    {
-                        // Check if it could be a old file
-                        if (!e.Message.StartsWith("Header"))
-                        {
-                            throw;
-                        }
-
-                        stream.Position = 0;
-                        var document = DeprecatedPixiParser.Deserialize(stream);
-
-                        imported = Surface.Load(document.PreviewImage);
-                    }
+                    imported = Surface.Load(PixiParser.ReadPreview(stream));
                 }
                 else
                 {

+ 1 - 1
src/PixiEditor.AvaloniaUI/Models/Dialogs/SizeUnit.cs

@@ -1,3 +1,3 @@
 namespace PixiEditor.AvaloniaUI.Models.Dialogs;
 
-public enum SizeUnit { Pixel, Percentage }
+public enum SizeUnit { Pixel, Percentage }

+ 1 - 1
src/PixiEditor.AvaloniaUI/Models/ExceptionHandling/CrashReport.cs

@@ -384,7 +384,7 @@ internal class CrashReport : IDisposable
                     .Replace(':', '_')
                     .Replace('/', '_');
 
-                byte[] serialized = PixiParser.Serialize(document.ToSerializable());
+                byte[] serialized = PixiParser.V5.Serialize(document.ToSerializable());
 
                 using Stream documentStream = archive.CreateEntry($"Documents/{nameInZip}").Open();
                 documentStream.Write(serialized);

+ 1 - 1
src/PixiEditor.AvaloniaUI/Models/Files/PixiFileType.cs

@@ -20,7 +20,7 @@ internal class PixiFileType : IoFileType
     {
         try
         {
-            await Parser.PixiParser.SerializeAsync(document.ToSerializable(), pathWithExtension);
+            await Parser.PixiParser.V5.SerializeAsync(document.ToSerializable(), pathWithExtension);
         }
         catch (UnauthorizedAccessException e)
         {

+ 47 - 34
src/PixiEditor.AvaloniaUI/Models/IO/Importer.cs

@@ -17,7 +17,7 @@ using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Extensions.Exceptions;
 using PixiEditor.Numerics;
 using PixiEditor.Parser;
-using PixiEditor.Parser.Deprecated;
+using PixiEditor.Parser.Old.PixiV4;
 using Bitmap = Avalonia.Media.Imaging.Bitmap;
 using BlendMode = PixiEditor.DrawingApi.Core.Surfaces.BlendMode;
 
@@ -85,37 +85,35 @@ internal class Importer : ObservableObject
     {
         try
         {
-            var doc = PixiParser.Deserialize(path).ToDocument();
+            using var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read);
+            var pixiDocument = PixiParser.DeserializeUsingCompatible(fileStream);
+
+            var document = pixiDocument switch
+            {
+                Document v5 => v5.ToDocument(),
+                DocumentV4 v4 => v4.ToDocument()
+                // TODO: Default handling
+            };
 
             if (associatePath)
             {
-                doc.FullFilePath = path;
+                document.FullFilePath = path;
             }
 
-            return doc;
+            return document;
         }
         catch (DirectoryNotFoundException)
         {
             //TODO: Handle
             throw new RecoverableException();
         }
-        catch (InvalidFileException)
+        catch (InvalidFileException e)
         {
-            try
-            {
-                var doc = DeprecatedPixiParser.Deserialize(path).ToDocument();
-                
-                if (associatePath)
-                {
-                    doc.FullFilePath = path;
-                }
-
-                return doc;
-            }
-            catch (Exception e)
-            {
-                throw new CorruptedFileException("FAILED_TO_OPEN_FILE", e);
-            }
+            throw new CorruptedFileException("FAILED_TO_OPEN_FILE", e);
+        }
+        catch (OldFileFormatException e)
+        {
+            throw new CorruptedFileException("FAILED_TO_OPEN_FILE", e);
         }
     }
 
@@ -123,22 +121,32 @@ internal class Importer : ObservableObject
     {
         try
         {
-            var doc = PixiParser.Deserialize(file).ToDocument();
-            doc.FullFilePath = originalFilePath;
-            return doc;
-        }
-        catch (InvalidFileException)
-        {
-            try
+            if (!PixiParser.TryGetCompatibleVersion(file, out var parser))
             {
-                var doc = DeprecatedPixiParser.Deserialize(file).ToDocument();
-                doc.FullFilePath = originalFilePath;
-                return doc;
+                // TODO: Handle
+                throw new RecoverableException();
             }
-            catch (InvalidFileException e)
+            
+            var pixiDocument = parser.Deserialize(file);
+
+            var document = pixiDocument switch
             {
-                throw new CorruptedFileException("FAILED_TO_OPEN_FILE", e);
-            }
+                Document v5 => v5.ToDocument(),
+                DocumentV4 v4 => v4.ToDocument()
+                // TODO: Default handling
+            };
+
+            document.FullFilePath = originalFilePath;
+
+            return document;
+        }
+        catch (InvalidFileException e)
+        {
+            throw new CorruptedFileException("FAILED_TO_OPEN_FILE", e);
+        }
+        catch (OldFileFormatException e)
+        {
+            throw new CorruptedFileException("FAILED_TO_OPEN_FILE", e);
         }
     }
 
@@ -148,8 +156,13 @@ internal class Importer : ObservableObject
         {
             throw new InvalidFileTypeException(new LocalizedString("FILE_EXTENSION_NOT_SUPPORTED", Path.GetExtension(path)));
         }
+        
+        if (Path.GetExtension(path) != ".pixi")
+            return Surface.Load(path);
+
+        using var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read);
 
-        return Path.GetExtension(path) != ".pixi" ? Surface.Load(path) : PixiParser.Deserialize(path).ToDocument().PreviewSurface;
+        return Surface.Load(PixiParser.ReadPreview(fileStream));
     }
 
     public static bool IsSupportedFile(string path)

+ 13 - 3
src/PixiEditor.AvaloniaUI/Models/IO/PaletteParsers/PixiPaletteParser.cs

@@ -4,6 +4,7 @@ using System.Threading.Tasks;
 using PixiEditor.Extensions.CommonApi.Palettes;
 using PixiEditor.Extensions.CommonApi.Palettes.Parsers;
 using PixiEditor.Parser;
+using PixiEditor.Parser.Old.PixiV4;
 
 namespace PixiEditor.AvaloniaUI.Models.IO.PaletteParsers;
 
@@ -25,12 +26,21 @@ internal class PixiPaletteParser : PaletteFileParser
 
     private async Task<PaletteFileData> ParseFile(string path)
     {
-        var file = await PixiParser.DeserializeAsync(path);
-        if(file.Palette == null) return PaletteFileData.Corrupted;
+        using var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read);
+        var file = await PixiParser.DeserializeUsingCompatibleAsync(fileStream);
+
+        var palette = file switch
+        {
+            Document v5 => v5.Palette,
+            DocumentV4 v4 => v4.Palette,
+            _ => null
+        };
+        
+        if(palette == null) return PaletteFileData.Corrupted;
 
         string name = Path.GetFileNameWithoutExtension(path);
 
-        return new PaletteFileData(name, file.Palette.Select(x => new PaletteColor(x.R, x.G, x.B)).ToArray());
+        return new PaletteFileData(name, palette.Select(x => new PaletteColor(x.R, x.G, x.B)).ToArray());
     }
 
     public override bool CanSave => false;

+ 41 - 17
src/PixiEditor.AvaloniaUI/Models/Rendering/MemberPreviewUpdater.cs

@@ -1,6 +1,7 @@
 #nullable enable
 
 using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using System.Threading.Tasks;
 using ChunkyImageLib;
@@ -505,12 +506,15 @@ internal class MemberPreviewUpdater
 
             if (memberVM is ILayerHandler)
             {
-                RenderLayerMainPreview((IReadOnlyLayerNode)member, memberVM, affArea.Value, position, scaling);
+                RenderLayerMainPreview((IReadOnlyLayerNode)member,
+                    memberVM.PreviewSurface, affArea.Value, position, scaling,
+                    doc.AnimationHandler.ActiveFrameBindable);
 
                 if (doc.AnimationHandler.FindKeyFrame(guid, out IKeyFrameHandler? keyFrame))
                 {
                     if (keyFrame is IKeyFrameGroupHandler group)
                     {
+                        RenderGroupPreview(keyFrame, memberVM, member, affArea, position, scaling);
                         foreach (var child in group.Children)
                         {
                             if (member is IReadOnlyImageNode rasterLayer)
@@ -535,6 +539,24 @@ internal class MemberPreviewUpdater
         }
     }
 
+    private void RenderGroupPreview(IKeyFrameHandler keyFrame, IStructureMemberHandler memberVM,
+        IReadOnlyStructureNode member, [DisallowNull] AffectedArea? affArea, VecI position, float scaling)
+    {
+        bool isEditingRootImage = !member.KeyFrames.Any(x => x.IsInFrame(doc.AnimationHandler.ActiveFrameBindable));
+        if(!isEditingRootImage)
+            return;
+        
+        if (keyFrame.PreviewSurface == null ||
+            keyFrame.PreviewSurface.Size != memberVM.PreviewSurface.Size)
+        {
+            keyFrame.PreviewSurface?.Dispose();
+            keyFrame.PreviewSurface = new Surface(memberVM.PreviewSurface.Size);
+        }
+
+        RenderLayerMainPreview((IReadOnlyLayerNode)member, keyFrame.PreviewSurface, affArea.Value,
+            position, scaling, 0);
+    }
+
     /// <summary>
     /// Re-render the <paramref name="area"/> of the main preview of the <paramref name="memberVM"/> folder
     /// </summary>
@@ -561,9 +583,10 @@ internal class MemberPreviewUpdater
             }
             else
             {
-                rendered = doc.Renderer.RenderChunk(chunk, ChunkResolution.Full, contentNode, doc.AnimationHandler.ActiveFrameBindable);
+                rendered = doc.Renderer.RenderChunk(chunk, ChunkResolution.Full, contentNode,
+                    doc.AnimationHandler.ActiveFrameBindable);
             }
-            
+
             if (rendered.IsT0)
             {
                 memberVM.PreviewSurface.DrawingSurface.Canvas.DrawSurface(rendered.AsT0.Surface.DrawingSurface, pos,
@@ -583,31 +606,31 @@ internal class MemberPreviewUpdater
     /// <summary>
     /// Re-render the <paramref name="area"/> of the main preview of the <paramref name="memberVM"/> layer
     /// </summary>
-    private void RenderLayerMainPreview(IReadOnlyLayerNode layer, IStructureMemberHandler memberVM, AffectedArea area,
-        VecI position, float scaling)
+    private void RenderLayerMainPreview(IReadOnlyLayerNode layer, Surface surface, AffectedArea area,
+        VecI position, float scaling, int frame)
     {
-        memberVM.PreviewSurface.DrawingSurface.Canvas.Save();
-        memberVM.PreviewSurface.DrawingSurface.Canvas.Scale(scaling);
-        memberVM.PreviewSurface.DrawingSurface.Canvas.Translate(-position);
-        memberVM.PreviewSurface.DrawingSurface.Canvas.ClipRect((RectD)area.GlobalArea);
+        surface.DrawingSurface.Canvas.Save();
+        surface.DrawingSurface.Canvas.Scale(scaling);
+        surface.DrawingSurface.Canvas.Translate(-position);
+        surface.DrawingSurface.Canvas.ClipRect((RectD)area.GlobalArea);
 
         foreach (var chunk in area.Chunks)
         {
             var pos = chunk * ChunkResolution.Full.PixelSize();
             if (layer is not IReadOnlyImageNode raster) return;
-            IReadOnlyChunkyImage? result = raster.GetLayerImageAtFrame(doc.AnimationHandler.ActiveFrameBindable);
+            IReadOnlyChunkyImage? result = raster.GetLayerImageAtFrame(frame);
 
             if (!result.DrawCommittedChunkOn(
                     chunk,
-                    ChunkResolution.Full, memberVM.PreviewSurface.DrawingSurface, pos,
+                    ChunkResolution.Full, surface.DrawingSurface, pos,
                     scaling < smoothingThreshold ? SmoothReplacingPaint : ReplacingPaint))
             {
-                memberVM.PreviewSurface.DrawingSurface.Canvas.DrawRect(pos.X, pos.Y, ChunkyImage.FullChunkSize,
+                surface.DrawingSurface.Canvas.DrawRect(pos.X, pos.Y, ChunkyImage.FullChunkSize,
                     ChunkyImage.FullChunkSize, ClearPaint);
             }
         }
 
-        memberVM.PreviewSurface.DrawingSurface.Canvas.Restore();
+        surface.DrawingSurface.Canvas.Restore();
     }
 
     private void RenderAnimationFramePreview(IReadOnlyImageNode node, IKeyFrameHandler keyFrameVM, AffectedArea area)
@@ -697,7 +720,7 @@ internal class MemberPreviewUpdater
 
     private void RenderNodePreviews(List<IRenderInfo> infos)
     {
-        foreach(var node in internals.Tracker.Document.NodeGraph.AllNodes)
+        foreach (var node in internals.Tracker.Document.NodeGraph.AllNodes)
         {
             if (node is null)
                 return;
@@ -712,7 +735,7 @@ internal class MemberPreviewUpdater
             {
                 return;
             }
-            
+
             if (nodeVm.ResultPreview == null)
             {
                 nodeVm.ResultPreview =
@@ -726,8 +749,9 @@ internal class MemberPreviewUpdater
             nodeVm.ResultPreview.DrawingSurface.Canvas.Scale(scalingX, scalingY);
 
             RectI region = new RectI(0, 0, node.CachedResult.Size.X, node.CachedResult.Size.Y);
-           
-            nodeVm.ResultPreview.DrawingSurface.Canvas.DrawSurface(node.CachedResult.DrawingSurface, 0, 0, ReplacingPaint);
+
+            nodeVm.ResultPreview.DrawingSurface.Canvas.DrawSurface(node.CachedResult.DrawingSurface, 0, 0,
+                ReplacingPaint);
 
             nodeVm.ResultPreview.DrawingSurface.Canvas.Restore();
             infos.Add(new NodePreviewDirty_RenderInfo(node.Id));

+ 0 - 36
src/PixiEditor.AvaloniaUI/Models/Serialization/Factories/ColorMatrixFactory.cs

@@ -1,36 +0,0 @@
-using System.Runtime.CompilerServices;
-using PixiEditor.Numerics;
-
-namespace PixiEditor.AvaloniaUI.Models.Serialization.Factories;
-
-// TODO: Might wanna write that for the 4x5 matrix too
-public class ColorMatrixFactory : SerializationFactory<byte[], ColorMatrix>
-{
-    public override string DeserializationId { get; } = "PixiEditor.ColorMatrix";
-    
-    public override byte[] Serialize(ColorMatrix original)
-    {
-        var members = original.ToArray();
-        var bytes = new byte[ColorMatrix.Width * ColorMatrix.Height * sizeof(float)];
-
-        Buffer.BlockCopy(members, 0, bytes, 0, bytes.Length);
-
-        return bytes;
-    }
-
-    public override bool TryDeserialize(object serialized, out ColorMatrix original)
-    {
-        if (serialized is not byte[] bytes)
-        {
-            original = default;
-            return false;
-        }
-
-        var members = new float[ColorMatrix.Width * ColorMatrix.Height];
-        
-        Buffer.BlockCopy(bytes, 0, members, 0, bytes.Length);
-        original = ColorMatrix.CreateFromMembers(members);
-
-        return true;
-    }
-}

+ 51 - 0
src/PixiEditor.AvaloniaUI/Models/Serialization/Factories/ColorMatrixSerializationFactory.cs

@@ -0,0 +1,51 @@
+using MessagePack;
+using PixiEditor.DrawingApi.Core.Numerics;
+using PixiEditor.Numerics;
+
+namespace PixiEditor.AvaloniaUI.Models.Serialization.Factories;
+
+internal class ColorMatrixSerializationFactory : SerializationFactory<SerializableMatrix, ColorMatrix>
+{
+    public override SerializableMatrix Serialize(ColorMatrix original)
+    {
+        return new SerializableMatrix
+        {
+            Width = 4,
+            Height = 5,
+            Values = original.ToArray()
+        };    
+    }
+
+    public override bool TryDeserialize(object raw, out ColorMatrix original)
+    {
+        if (raw is not Dictionary<string, object> serialized)
+        {
+            original = default;
+            return false;
+        }
+
+        if (serialized.Count == 3)
+        {
+            float[] values = ExtractArray<float>(serialized["Values"]);
+            original = new ColorMatrix(values);
+            
+            return true;
+        }
+
+        original = default;
+        return false; 
+    }
+
+    public override string DeserializationId { get; } = "PixiEditor.Matrix";
+}
+
+[MessagePackObject]
+class SerializableMatrix
+{
+    [Key("Width")]
+    public int Width { get; set; }
+    [Key("Height")]
+    public int Height { get; set; }
+    [Key("Values")]
+    public float[] Values { get; set; }
+}

+ 3 - 27
src/PixiEditor.AvaloniaUI/Models/UserData/RecentlyOpenedDocument.cs

@@ -4,12 +4,12 @@ using System.Linq;
 using ChunkyImageLib;
 using CommunityToolkit.Mvvm.ComponentModel;
 using PixiEditor.AvaloniaUI.Helpers;
+using PixiEditor.AvaloniaUI.Models.IO;
 using PixiEditor.DrawingApi.Core;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.Extensions.Exceptions;
 using PixiEditor.Numerics;
 using PixiEditor.Parser;
-using PixiEditor.Parser.Deprecated;
 
 namespace PixiEditor.AvaloniaUI.Models.UserData;
 
@@ -91,36 +91,12 @@ internal class RecentlyOpenedDocument : ObservableObject
         {
             try
             {
-                var document = PixiParser.Deserialize(filePath);
-
-                if (document.PreviewImage == null || document.PreviewImage.Length == 0)
-                {
-                    return null;
-                }
-
-                return Surface.Load(document.PreviewImage);
+                return Importer.GetPreviewBitmap(FilePath);
             }
             catch
             {
-
-                try
-                {
-                    var deprecatedDocument = DeprecatedPixiParser.Deserialize(filePath);
-                    
-                    if (deprecatedDocument.PreviewImage == null || deprecatedDocument.PreviewImage.Length == 0)
-                    {
-                        return null;
-                    }
-                    
-                    return Surface.Load(deprecatedDocument.PreviewImage);
-                }
-                catch
-                {
-                    corrupt = true;
-                    return null;
-                }
+                return null;
             }
-
         }
 
         if (SupportedFilesHelper.IsExtensionSupported(FileExtension))

+ 27 - 3
src/PixiEditor.AvaloniaUI/Styles/PortingWipStyles.axaml

@@ -45,14 +45,15 @@
     <Style Selector="CheckBox.ImageCheckBox:checked">
         <Setter Property="Content" Value="{DynamicResource icon-eye}" />
     </Style>
-    
+
     <Style Selector="ToggleButton.PlayButton">
         <Setter Property="Content" Value="{DynamicResource icon-play}" />
         <Setter Property="Template">
             <Setter.Value>
                 <ControlTemplate>
                     <Border Cursor="Hand" Background="{TemplateBinding Background}">
-                        <TextBlock Text="{TemplateBinding Content}" FontSize="{TemplateBinding Width}" Classes="pixi-icon" />
+                        <TextBlock Text="{TemplateBinding Content}" FontSize="{TemplateBinding Width}"
+                                   Classes="pixi-icon" />
                     </Border>
                 </ControlTemplate>
             </Setter.Value>
@@ -61,9 +62,32 @@
 
     <Style Selector="ToggleButton.PlayButton:checked">
         <Setter Property="Content" Value="{DynamicResource icon-pause}" />
+        <Setter Property="Background" Value="Transparent" />
     </Style>
+    
+    <Style Selector="ToggleButton.PlayButton:pressed">
+        <Setter Property="Background" Value="Transparent" />
+   </Style> 
     <Style Selector="ToggleButton.ExpandCollapseToggleStyle">
-
+        <Setter Property="Content" Value="{DynamicResource icon-chevron-down}" />
+        <Setter Property="Template">
+            <Setter.Value>
+                <ControlTemplate>
+                    <Border Cursor="Hand" Background="{TemplateBinding Background}">
+                        <TextBlock Text="{TemplateBinding Content}" FontSize="16" Classes="pixi-icon" />
+                    </Border>
+                </ControlTemplate>
+            </Setter.Value>
+        </Setter>
+    </Style>
+    
+    <Style Selector="ToggleButton.ExpandCollapseToggleStyle:pressed">
+        <Setter Property="Background" Value="Transparent" />
+    </Style>
+    
+    <Style Selector="ToggleButton.ExpandCollapseToggleStyle:checked">
+        <Setter Property="Content" Value="{DynamicResource icon-chevron-left}" />
+        <Setter Property="Background" Value="Transparent"/>
     </Style>
 
     <Style Selector="Button.SocialMediaButton">

+ 16 - 3
src/PixiEditor.AvaloniaUI/Styles/Templates/KeyFrame.axaml

@@ -6,19 +6,23 @@
                     xmlns:converters="clr-namespace:PixiEditor.AvaloniaUI.Helpers.Converters">
     <ControlTheme TargetType="animations:KeyFrame" x:Key="{x:Type animations:KeyFrame}">
         <Setter Property="ClipToBounds" Value="False"/>
+        <Setter Property="Height" Value="70"/>
+        <Setter Property="MinWidth" Value="35"/>
         <Setter Property="Template">
             <ControlTemplate>
                 <Grid>
                     <Border CornerRadius="{DynamicResource ControlCornerRadius}" Name="MainBorder"
-                            Background="{DynamicResource ThemeBackgroundBrush1}" Height="20"
+                            Background="{DynamicResource ThemeBackgroundBrush1}" Margin="0 5"
                             BorderBrush="{DynamicResource ThemeBorderMidBrush}" BorderThickness="1">
                         <Grid>
                             <Panel HorizontalAlignment="Right" Name="PART_ResizePanelRight" Width="5" Cursor="SizeWestEast" Background="Transparent" ZIndex="1"/>
-                            <Panel Margin="-35, 0, 0, 0" HorizontalAlignment="Left" Name="PART_ResizePanelLeft" Width="5" Cursor="SizeWestEast" Background="Transparent" ZIndex="1"/>
+                            <Panel Margin="-35, 0, 0, 0" HorizontalAlignment="Left" Name="PART_ResizePanelLeft"
+                                   Width="5" Cursor="SizeWestEast" Background="Transparent" ZIndex="1"/>
                         </Grid>
                     </Border>
                     
-                    <Border CornerRadius="{DynamicResource ControlCornerRadius}" Width="60" Height="60" Margin="-30, 0, 0, 0"
+                    <Border IsVisible="{Binding !IsCollapsed, RelativeSource={RelativeSource TemplatedParent}}"
+                        CornerRadius="{DynamicResource ControlCornerRadius}" Width="60" Height="60" Margin="-30, 0, 0, 0"
                             BorderThickness="1" VerticalAlignment="Center" IsHitTestVisible="True" Name="PreviewBorder"
                             HorizontalAlignment="Left" BorderBrush="{DynamicResource ThemeBorderMidBrush}"
                             RenderOptions.BitmapInterpolationMode="None">
@@ -45,6 +49,15 @@
             </ControlTemplate>
         </Setter>
         
+        <Style Selector="^:collapsed">
+            <Setter Property="Height" Value="30"/>
+            <Setter Property="MinWidth" Value="5"/>
+        </Style>
+        
+        <Style Selector="^:collapsed /template/ Panel#PART_ResizePanelLeft">
+            <Setter Property="Margin" Value="0"/>
+        </Style>
+        
         <Style Selector="^:selected /template/ Border#MainBorder">
             <Setter Property="BorderBrush" Value="{DynamicResource ThemeAccent2Color}" />
         </Style>

+ 115 - 65
src/PixiEditor.AvaloniaUI/Styles/Templates/Timeline.axaml

@@ -19,28 +19,68 @@
                         <RowDefinition Height="Auto" />
                         <RowDefinition Height="*" />
                     </Grid.RowDefinitions>
+                    <Grid.ColumnDefinitions>
+                        <ColumnDefinition Width="200" />
+                        <ColumnDefinition Width="*" />
+                    </Grid.ColumnDefinitions>
 
-                    <DockPanel Grid.Column="0" Grid.Row="0" LastChildFill="True">
-                        <Button DockPanel.Dock="Left" Classes="pixi-icon"
-                                Content="{DynamicResource icon-plus-square}"
-                                Command="{TemplateBinding NewKeyFrameCommand}" />
+                    <Border Grid.Row="0" Grid.Column="0" BorderThickness="0 0 1 0"
+                            BorderBrush="{DynamicResource ThemeBorderMidBrush}">
+                        <input:SizeInput Unit="FPS"
+                                         Width="80" Height="25" HorizontalAlignment="Left"
+                                         Size="{Binding Fps, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" />
+                    </Border>
+                    <Border Grid.Row="0" Grid.Column="1">
+                        <StackPanel Orientation="Horizontal" HorizontalAlignment="Left" Margin="5">
+                            <ToggleButton Margin="0, 5" Width="24" HorizontalAlignment="Center" Classes="PlayButton"
+                                          Name="PART_PlayToggle" />
+                            <TextBlock VerticalAlignment="Center" FontSize="14">
+                                <Run>
+                                    <Run.Text>
+                                        <MultiBinding>
+                                            <MultiBinding.Converter>
+                                                <converters:FrameToTimeConverter />
+                                            </MultiBinding.Converter>
+                                            <Binding Path="ActiveFrame"
+                                                     RelativeSource="{RelativeSource TemplatedParent}" />
+                                            <Binding Path="Fps" RelativeSource="{RelativeSource TemplatedParent}" />
+                                        </MultiBinding>
+                                    </Run.Text>
+                                </Run>
+                                <Run Text="/" />
+                                <Run>
+                                    <Run.Text>
+                                        <MultiBinding>
+                                            <MultiBinding.Converter>
+                                                <converters:FrameToTimeConverter />
+                                            </MultiBinding.Converter>
+                                            <Binding Path="EndFrame" RelativeSource="{RelativeSource TemplatedParent}" />
+                                            <Binding Path="Fps" RelativeSource="{RelativeSource TemplatedParent}" />
+                                        </MultiBinding>
+                                    </Run.Text>
+                                </Run>
+                            </TextBlock>
+                        </StackPanel>
+                    </Border>
 
-                        <Button DockPanel.Dock="Left" Classes="pixi-icon"
-                                Content="{DynamicResource icon-duplicate}"
-                                Command="{TemplateBinding DuplicateKeyFrameCommand}" />
-                        <Button DockPanel.Dock="Left" Classes="pixi-icon"
-                                Content="{DynamicResource icon-trash}"
-                                Command="{TemplateBinding DeleteKeyFrameCommand}"
-                                IsEnabled="{Binding SelectedKeyFrames.Count, RelativeSource={RelativeSource TemplatedParent}}"
-                                CommandParameter="{Binding SelectedKeyFrames, RelativeSource={RelativeSource TemplatedParent}}" />
-                        <input:NumberInput Min="1"
-                                           Value="{Binding Fps, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" />
-                        <Panel>
-                            <ToggleButton Margin="0, 5" Width="24" HorizontalAlignment="Center" Classes="PlayButton" Name="PART_PlayToggle" />
-                        </Panel>
-                    </DockPanel>
+                    <Border Grid.Column="0" Grid.Row="1" BorderThickness="0 1 1 1"
+                            BorderBrush="{DynamicResource ThemeBorderMidBrush}">
+                        <StackPanel Orientation="Horizontal" Height="40">
+                            <Button Classes="pixi-icon"
+                                    Content="{DynamicResource icon-plus-square}"
+                                    Command="{TemplateBinding NewKeyFrameCommand}" />
+                            <Button Classes="pixi-icon"
+                                    Content="{DynamicResource icon-duplicate}"
+                                    Command="{TemplateBinding DuplicateKeyFrameCommand}" />
+                            <Button Classes="pixi-icon"
+                                    Content="{DynamicResource icon-trash}"
+                                    Command="{TemplateBinding DeleteKeyFrameCommand}"
+                                    IsEnabled="{Binding SelectedKeyFrames.Count, RelativeSource={RelativeSource TemplatedParent}}"
+                                    CommandParameter="{Binding SelectedKeyFrames, RelativeSource={RelativeSource TemplatedParent}}" />
+                        </StackPanel>
+                    </Border>
 
-                    <Grid Grid.Row="2">
+                    <Grid Grid.Row="1" Grid.RowSpan="2" Grid.Column="0" Grid.ColumnSpan="2">
                         <Grid.RowDefinitions>
                             <RowDefinition Height="Auto" /> <!-- For the timeline slider -->
                             <RowDefinition Height="*" />    <!-- For the keyframes and headers -->
@@ -49,50 +89,53 @@
                             <ColumnDefinition Width="200" /> <!-- For the headers -->
                             <ColumnDefinition Width="*" />    <!-- For the timeline slider and keyframes -->
                         </Grid.ColumnDefinitions>
-                        <animations:TimelineSlider
-                            Grid.Row="0" Grid.Column="1"
-                            TickFrequency="1" Height="35" ClipToBounds="False"
-                            TickPlacement="TopLeft" VerticalAlignment="Top"
-                            SmallChange="1" ZIndex="10"
-                            LargeChange="10"
-                            Scale="{Binding Scale, RelativeSource={RelativeSource TemplatedParent}}"
-                            Offset="{Binding ScrollOffset, RelativeSource={RelativeSource TemplatedParent}}"
-                            MinLeftOffset="{Binding MinLeftOffset, RelativeSource={RelativeSource TemplatedParent}}"
-                            IsSnapToTickEnabled="True"
-                            Name="PART_TimelineSlider"
-                            Minimum="1">
-                            <animations:TimelineSlider.Maximum>
-                                <MultiBinding>
-                                    <MultiBinding.Converter>
-                                        <converters:TimelineSliderWidthToMaximumConverter />
-                                    </MultiBinding.Converter>
-                                    <Binding Path="Bounds"
-                                             RelativeSource="{RelativeSource Self}" />
-                                    <Binding RelativeSource="{RelativeSource TemplatedParent}"
-                                             Path="Scale" />
-                                    <Binding RelativeSource="{RelativeSource TemplatedParent}"
-                                             Path="ScrollOffset" />
-                                </MultiBinding>
-                            </animations:TimelineSlider.Maximum>
-                            <Interaction.Behaviors>
-                                <behaviours:SliderUpdateBehavior
-                                    Binding="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=ActiveFrame, Mode=OneWay}"
-                                    DragStarted="{xaml:Command PixiEditor.Document.StartChangeActiveFrame}"
-                                    DragValueChanged="{xaml:Command PixiEditor.Document.ChangeActiveFrame, UseProvided=True}"
-                                    DragEnded="{xaml:Command PixiEditor.Document.EndChangeActiveFrame}"
-                                    SetValueCommand="{xaml:Command PixiEditor.Animation.ActiveFrameSet, UseProvided=True}"
-                                    ValueFromSlider="{Binding ElementName=PART_TimelineSlider, Path=Value, Mode=TwoWay}" />
-                            </Interaction.Behaviors>
-                        </animations:TimelineSlider>
+                        <Border Grid.Row="0" Grid.Column="1" BorderBrush="{DynamicResource ThemeBorderMidBrush}"
+                                BorderThickness="0 1">
+                            <animations:TimelineSlider
+                                TickFrequency="1" Height="40" ClipToBounds="False"
+                                TickPlacement="TopLeft" VerticalAlignment="Top"
+                                SmallChange="1" ZIndex="10"
+                                LargeChange="10"
+                                Scale="{Binding Scale, RelativeSource={RelativeSource TemplatedParent}}"
+                                Offset="{Binding ScrollOffset, RelativeSource={RelativeSource TemplatedParent}}"
+                                MinLeftOffset="{Binding MinLeftOffset, RelativeSource={RelativeSource TemplatedParent}}"
+                                IsSnapToTickEnabled="True"
+                                Name="PART_TimelineSlider"
+                                Minimum="1">
+                                <animations:TimelineSlider.Maximum>
+                                    <MultiBinding>
+                                        <MultiBinding.Converter>
+                                            <converters:TimelineSliderWidthToMaximumConverter />
+                                        </MultiBinding.Converter>
+                                        <Binding Path="Bounds"
+                                                 RelativeSource="{RelativeSource Self}" />
+                                        <Binding RelativeSource="{RelativeSource TemplatedParent}"
+                                                 Path="Scale" />
+                                        <Binding RelativeSource="{RelativeSource TemplatedParent}"
+                                                 Path="ScrollOffset" />
+                                    </MultiBinding>
+                                </animations:TimelineSlider.Maximum>
+                                <Interaction.Behaviors>
+                                    <behaviours:SliderUpdateBehavior
+                                        Binding="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=ActiveFrame, Mode=OneWay}"
+                                        DragStarted="{xaml:Command PixiEditor.Document.StartChangeActiveFrame}"
+                                        DragValueChanged="{xaml:Command PixiEditor.Document.ChangeActiveFrame, UseProvided=True}"
+                                        DragEnded="{xaml:Command PixiEditor.Document.EndChangeActiveFrame}"
+                                        SetValueCommand="{xaml:Command PixiEditor.Animation.ActiveFrameSet, UseProvided=True}"
+                                        ValueFromSlider="{Binding ElementName=PART_TimelineSlider, Path=Value, Mode=TwoWay}" />
+                                </Interaction.Behaviors>
+                            </animations:TimelineSlider>
+                        </Border>
 
-                        <Panel ClipToBounds="True" Grid.Row="1" Grid.Column="1" Margin="29, -16, 0, 0" VerticalAlignment="Stretch"
+                        <Panel ClipToBounds="True" Grid.Row="1" Grid.Column="1" Margin="29, -22, 0, 0"
+                               VerticalAlignment="Stretch"
                                ZIndex="11" HorizontalAlignment="Left" IsHitTestVisible="False">
                             <Border Width="2" Background="{DynamicResource ThemeAccentBrush}">
                                 <Border.Margin>
                                     <MultiBinding Converter="{converters:TimelineSliderValueToMarginConverter}">
                                         <Binding Path="ActiveFrame"
                                                  RelativeSource="{RelativeSource TemplatedParent}" />
-                                        <Binding Path="Minimum" ElementName="PART_TimelineSlider"/>
+                                        <Binding Path="Minimum" ElementName="PART_TimelineSlider" />
                                         <Binding Path="Scale"
                                                  RelativeSource="{RelativeSource TemplatedParent}" />
                                         <Binding Path="ScrollOffset"
@@ -106,19 +149,25 @@
                                       Name="PART_TimelineHeaderScroll"
                                       Grid.Row="1" Grid.Column="0">
                             <StackPanel Orientation="Vertical" Background="{DynamicResource ThemeBackgroundBrush1}">
-                                <ItemsControl Margin="0, 35"
-                                              ItemsSource="{Binding KeyFrames, RelativeSource={RelativeSource TemplatedParent}}">
+                                <ItemsControl
+                                    ItemsSource="{Binding KeyFrames, RelativeSource={RelativeSource TemplatedParent}}">
                                     <ItemsControl.DataTemplates>
                                         <DataTemplate DataType="document:KeyFrameGroupViewModel">
                                             <animations:TimelineGroupHeader Height="70"
                                                                             Item="{Binding}" />
                                         </DataTemplate>
                                     </ItemsControl.DataTemplates>
+                                    <ItemsControl.Styles>
+                                        <Style Selector="animations|TimelineGroupHeader:collapsed">
+                                            <Setter Property="Height" Value="30" />
+                                        </Style>
+                                    </ItemsControl.Styles>
                                 </ItemsControl>
                             </StackPanel>
                         </ScrollViewer>
                         <ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"
-                                      Grid.Row="1" Offset="{Binding ScrollOffset, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
+                                      Grid.Row="1"
+                                      Offset="{Binding ScrollOffset, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
                                       Name="PART_TimelineKeyFramesScroll" Grid.Column="1">
                             <Grid Background="{DynamicResource ThemeBackgroundBrush}" Name="PART_ContentGrid">
                                 <Interaction.Behaviors>
@@ -130,12 +179,11 @@
                                     </EventTriggerBehavior>
                                 </Interaction.Behaviors>
                                 <ItemsControl ClipToBounds="False" Name="PART_KeyFramesHost"
-                                              Margin="0, 35, 0, 0"
                                               ItemsSource="{Binding KeyFrames, RelativeSource={RelativeSource TemplatedParent}}">
                                     <ItemsControl.DataTemplates>
                                         <DataTemplate DataType="document:KeyFrameGroupViewModel">
-                                            <ItemsControl Padding="0 5" ClipToBounds="False" Height="70"
-                                                          BorderThickness="0, 0, 0, 1"
+                                            <ItemsControl ClipToBounds="False"
+                                                          BorderThickness="0, 0, 0, 1" 
                                                           BorderBrush="{DynamicResource ThemeBorderMidBrush}"
                                                           ItemsSource="{Binding Children}">
                                                 <ItemsControl.ItemContainerTheme>
@@ -160,6 +208,7 @@
                                                 Scale="{Binding Scale, RelativeSource={RelativeSource FindAncestor, AncestorType=animations:Timeline}}"
                                                 IsEnabled="{Binding IsVisible}"
                                                 IsSelected="{Binding IsSelected, Mode=TwoWay}"
+                                                IsCollapsed="{Binding IsCollapsed}"
                                                 Min="{Binding ElementName=PART_TimelineSlider, Path=Minimum}"
                                                 Item="{Binding}">
                                                 <animations:KeyFrame.Width>
@@ -194,10 +243,11 @@
                                         </DataTemplate>
                                     </ItemsControl.DataTemplates>
                                 </ItemsControl>
-                                
-                                <Rectangle Name="PART_SelectionRectangle" HorizontalAlignment="Left" VerticalAlignment="Top"
+
+                                <Rectangle Name="PART_SelectionRectangle" HorizontalAlignment="Left"
+                                           VerticalAlignment="Top"
                                            IsVisible="False" ZIndex="100"
-                                           Fill="{DynamicResource SelectionFillBrush}" Opacity="1"/>
+                                           Fill="{DynamicResource SelectionFillBrush}" Opacity="1" />
                             </Grid>
                         </ScrollViewer>
                     </Grid>

+ 31 - 5
src/PixiEditor.AvaloniaUI/Styles/Templates/TimelineGroupHeader.axaml

@@ -7,11 +7,37 @@
     <ControlTheme TargetType="animations:TimelineGroupHeader" x:Key="{x:Type animations:TimelineGroupHeader}">
         <Setter Property="Template">
             <ControlTemplate>
-                <Border BorderBrush="{DynamicResource ThemeBorderMidBrush}" BorderThickness="1">
-                    <StackPanel Orientation="Horizontal" VerticalAlignment="Center">
-                        <CheckBox Classes="ImageCheckBox" IsChecked="{Binding Item.IsVisible, RelativeSource={RelativeSource TemplatedParent}}"/>
-                        <TextBlock Text="{Binding Item.LayerName, RelativeSource={RelativeSource TemplatedParent}}" />
-                    </StackPanel>
+                <Border BorderBrush="{DynamicResource ThemeBorderMidBrush}" BorderThickness="0 0 1 1">
+                    <DockPanel LastChildFill="False" VerticalAlignment="Center">
+                        <CheckBox VerticalAlignment="Center" Classes="ImageCheckBox" DockPanel.Dock="Left"
+                                  IsChecked="{Binding Item.IsVisible, RelativeSource={RelativeSource TemplatedParent}}" />
+                        <Border IsVisible="{Binding ElementName=PART_CollapseButton, Path=!IsChecked}" CornerRadius="{DynamicResource ControlCornerRadius}" Width="60" Height="60"
+                                BorderThickness="1" VerticalAlignment="Center" IsHitTestVisible="True"
+                                Name="PreviewBorder" Margin="5 0"
+                                HorizontalAlignment="Left" BorderBrush="{DynamicResource ThemeBorderMidBrush}"
+                                RenderOptions.BitmapInterpolationMode="None">
+                            <Border.Background>
+                                <ImageBrush Source="/Images/CheckerTile.png" TileMode="Tile">
+                                    <ImageBrush.Transform>
+                                        <ScaleTransform ScaleX="0.4" ScaleY="0.4" />
+                                    </ImageBrush.Transform>
+                                </ImageBrush>
+                            </Border.Background>
+                            <visuals:SurfaceControl
+                                Surface="{Binding Item.PreviewSurface, RelativeSource={RelativeSource TemplatedParent}}"
+                                Stretch="Uniform" Width="60" Height="60">
+                                <ui:RenderOptionsBindable.BitmapInterpolationMode>
+                                    <MultiBinding Converter="{converters:WidthToBitmapScalingModeConverter}">
+                                        <Binding Path="Item.PreviewSurface.Size.X"
+                                                 RelativeSource="{RelativeSource TemplatedParent}" />
+                                        <Binding RelativeSource="{RelativeSource Mode=Self}" Path="Bounds.Width" />
+                                    </MultiBinding>
+                                </ui:RenderOptionsBindable.BitmapInterpolationMode>
+                            </visuals:SurfaceControl>
+                        </Border>
+                        <TextBlock Margin="5 0 0 0" VerticalAlignment="Center" Text="{Binding Item.LayerName, RelativeSource={RelativeSource TemplatedParent}}" />
+                        <ToggleButton Name="PART_CollapseButton" Margin="0 0 5 0" DockPanel.Dock="Right" Classes="ExpandCollapseToggleStyle" HorizontalAlignment="Right" VerticalAlignment="Center" />
+                    </DockPanel>
                 </Border>
             </ControlTemplate>
         </Setter>

+ 4 - 2
src/PixiEditor.AvaloniaUI/Styles/Templates/TimelineSlider.axaml

@@ -24,7 +24,7 @@
                 <ControlTemplate>
                     <Grid Name="grid">
                         <Border Background="{DynamicResource ThemeControlLowBrush}"
-                                Height="35"
+                                Height="40"
                                 VerticalAlignment="Center">
                         </Border>
                         <Canvas Margin="-6,-1">
@@ -38,7 +38,9 @@
                             Offset="{TemplateBinding Offset}"
                             MinLeftOffset="{TemplateBinding MinLeftOffset}"
                             MinValue="{TemplateBinding Minimum}"
-                            Fill="{DynamicResource ThemeForegroundBrush}" />
+                            Fill="{DynamicResource ThemeControlHighBrush}" 
+                            Foreground="{DynamicResource ThemeForegroundBrush}"
+                            Margin="0 0 0 5"/>
                         <animations:TimelineSliderTrack Name="PART_Track"
                                IsDirectionReversed="{TemplateBinding IsDirectionReversed}"
                                Margin="15, 0, 0, 0"

+ 2 - 2
src/PixiEditor.AvaloniaUI/ViewModels/Dock/LayoutManager.cs

@@ -63,7 +63,7 @@ internal class LayoutManager
                         Id = "DocumentArea", FallbackContent = new CreateDocumentFallbackView(),
                         Dockables = [ DockContext.CreateDockable(nodeGraphDockViewModel) ]
                     },
-                    FirstSize = 0.75,
+                    FirstSize = 0.85,
                     SplitDirection = DockingDirection.Bottom,
                     Second = new DockableArea
                     {
@@ -71,7 +71,7 @@ internal class LayoutManager
                         ActiveDockable = DockContext.CreateDockable(timelineDockViewModel)
                     }
                 },
-                FirstSize = 0.8,
+                FirstSize = 0.85,
                 SplitDirection = DockingDirection.Right,
                 Second = new DockableTree
                 {

+ 1 - 1
src/PixiEditor.AvaloniaUI/ViewModels/Document/AnimationDataViewModel.cs

@@ -71,7 +71,7 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
     {
         if (!Document.UpdateableChangeActive)
         {
-            Internals.ActionAccumulator.AddFinishedActions(new CreateRasterKeyFrame_Action(targetLayerGuid, Guid.NewGuid(), frame,
+            Internals.ActionAccumulator.AddFinishedActions(new CreateRasterKeyFrame_Action(targetLayerGuid, Guid.NewGuid(), Math.Max(1, frame),
                 frameToCopyFrom ?? -1, toCloneFrom ?? Guid.Empty));
         }
     }

+ 22 - 29
src/PixiEditor.AvaloniaUI/ViewModels/Document/DocumentViewModel.Serialization.cs

@@ -5,11 +5,9 @@ using ChunkyImageLib.DataHolders;
 using Microsoft.Extensions.DependencyInjection;
 using PixiEditor.AvaloniaUI.Helpers;
 using PixiEditor.AvaloniaUI.Helpers.Extensions;
-using PixiEditor.AvaloniaUI.Models.IO;
 using PixiEditor.AvaloniaUI.Models.IO.FileEncoders;
 using PixiEditor.AvaloniaUI.Models.Serialization;
 using PixiEditor.AvaloniaUI.Models.Serialization.Factories;
-using PixiEditor.ChangeableDocument.Changeables;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Interfaces;
 using PixiEditor.DrawingApi.Core;
@@ -35,6 +33,7 @@ internal partial class DocumentViewModel
     {
         NodeGraph graph = new();
         ImageEncoder encoder = new QoiEncoder();
+        var serializationConfig = new SerializationConfig(encoder);
         var doc = Internals.Tracker.Document;
         
         Dictionary<Guid, int> nodeIdMap = new();
@@ -43,7 +42,7 @@ internal partial class DocumentViewModel
         List<SerializationFactory> factories =
             ViewModelMain.Current.Services.GetServices<SerializationFactory>().ToList(); // a bit ugly, sorry
 
-        AddNodes(doc.NodeGraph, graph, nodeIdMap, keyFrameIdMap, new SerializationConfig(encoder), factories);
+        AddNodes(doc.NodeGraph, graph, nodeIdMap, keyFrameIdMap, serializationConfig, factories);
 
         var document = new PixiDocument
         {
@@ -54,7 +53,7 @@ internal partial class DocumentViewModel
             Graph = graph,
             PreviewImage =
                 (TryRenderWholeImage(0).Value as Surface)?.DrawingSurface.Snapshot().Encode().AsSpan().ToArray(),
-            ReferenceLayer = GetReferenceLayer(doc),
+            ReferenceLayer = GetReferenceLayer(doc, serializationConfig),
             AnimationData = ToAnimationData(doc.AnimationData, nodeIdMap, keyFrameIdMap),
             ImageEncoderUsed = encoder.EncodedFormatName
         };
@@ -70,6 +69,7 @@ internal partial class DocumentViewModel
         targetGraph.AllNodes = new List<Node>();
 
         int id = 0;
+        int keyFrameId = 0;
         foreach (var node in graph.AllNodes)
         {
             nodeIdMap[node.Id] = id + 1;
@@ -96,16 +96,18 @@ internal partial class DocumentViewModel
             
             for (int i = 0; i < node.KeyFrames.Count; i++)
             {
-                keyFrameIdMap[node.KeyFrames[i].KeyFrameGuid] = i + 1;
+                keyFrameIdMap[node.KeyFrames[i].KeyFrameGuid] = keyFrameId + 1;
                 keyFrames[i] = new KeyFrameData
                 {
-                    Id = i + 1, 
+                    Id = keyFrameId + 1,
                     Data = SerializationUtil.SerializeObject(node.KeyFrames[i].Data, config, allFactories), 
                     AffectedElement = node.KeyFrames[i].AffectedElement,
                     StartFrame = node.KeyFrames[i].StartFrame, 
                     Duration = node.KeyFrames[i].Duration,
                     IsVisible = node.KeyFrames[i].IsVisible
                 };
+                
+                keyFrameId++;
             }
                 
             Dictionary<string, object> converted = ConvertToSerializable(additionalData, config, allFactories);
@@ -168,43 +170,34 @@ internal partial class DocumentViewModel
         return converted;
     }
 
-    private static ReferenceLayer? GetReferenceLayer(IReadOnlyDocument document)
+    private static ReferenceLayer? GetReferenceLayer(IReadOnlyDocument document, SerializationConfig config)
     {
         if (document.ReferenceLayer == null)
         {
             return null;
         }
 
-        var layer = document.ReferenceLayer!;
-
-        var surface = new Surface(new VecI(layer.ImageSize.X, layer.ImageSize.Y));
-
-        surface.DrawBytes(surface.Size, layer.ImageBgra8888Bytes.ToArray(), ColorType.Bgra8888, AlphaType.Premul);
-
-        var encoder = new UniversalFileEncoder(EncodedImageFormat.Png);
+        var layer = document.ReferenceLayer;
 
-        using var stream = new MemoryStream();
-
-        encoder.Save(stream, surface);
-
-        stream.Position = 0;
+        var shape = layer.Shape;
+        var imageSize = layer.ImageSize;
+        
+        var imageBytes = config.Encoder.Encode(layer.ImageBgra8888Bytes.ToArray(), imageSize.X, imageSize.Y);
 
         return new ReferenceLayer
         {
             Enabled = layer.IsVisible,
-            Width = (float)layer.Shape.RectSize.X,
-            Height = (float)layer.Shape.RectSize.Y,
-            OffsetX = (float)layer.Shape.TopLeft.X,
-            OffsetY = (float)layer.Shape.TopLeft.Y,
+            Topmost = layer.IsTopMost,
+            ImageWidth = imageSize.X,
+            ImageHeight = imageSize.Y,
             Corners = new Corners
             {
-                TopLeft = layer.Shape.TopLeft.ToVector2(),
-                TopRight = layer.Shape.TopRight.ToVector2(),
-                BottomLeft = layer.Shape.BottomLeft.ToVector2(),
-                BottomRight = layer.Shape.BottomRight.ToVector2()
+                TopLeft = shape.TopLeft.ToVector2(),
+                TopRight = shape.TopRight.ToVector2(),
+                BottomLeft = shape.BottomLeft.ToVector2(),
+                BottomRight = shape.BottomRight.ToVector2()
             },
-            Opacity = 1,
-            ImageBytes = stream.ToArray()
+            ImageBytes = imageBytes
         };
     }
 

+ 0 - 1
src/PixiEditor.AvaloniaUI/ViewModels/Document/DocumentViewModel.cs

@@ -45,7 +45,6 @@ using PixiEditor.DrawingApi.Core.Surfaces.Vector;
 using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Extensions.CommonApi.Palettes;
 using PixiEditor.Numerics;
-using PixiEditor.Parser;
 using PixiEditor.Parser.Skia;
 using Color = PixiEditor.DrawingApi.Core.ColorsImpl.Color;
 using Colors = PixiEditor.DrawingApi.Core.ColorsImpl.Colors;

+ 38 - 0
src/PixiEditor.AvaloniaUI/ViewModels/Document/KeyFrameGroupViewModel.cs

@@ -1,4 +1,5 @@
 using System.Collections.ObjectModel;
+using System.Collections.Specialized;
 using System.Reactive.Linq;
 using PixiEditor.AvaloniaUI.Models.DocumentModels;
 using PixiEditor.AvaloniaUI.Models.Handlers;
@@ -14,6 +15,24 @@ internal class KeyFrameGroupViewModel : KeyFrameViewModel, IKeyFrameGroupHandler
 
     public string LayerName => Document.StructureHelper.Find(LayerGuid).NodeNameBindable;
 
+    public bool IsCollapsed
+    {
+        get => _isCollapsed;
+        set
+        {
+            SetProperty(ref _isCollapsed, value);
+            foreach (var child in Children)
+            {
+                if (child is KeyFrameViewModel keyFrame)
+                {
+                    keyFrame.IsCollapsed = value;
+                }
+            }
+        }
+    }
+
+    private bool _isCollapsed;
+
     public override void SetVisibility(bool isVisible)
     {
         foreach (var child in Children)
@@ -30,6 +49,7 @@ internal class KeyFrameGroupViewModel : KeyFrameViewModel, IKeyFrameGroupHandler
     public KeyFrameGroupViewModel(int startFrame, int duration, Guid layerGuid, Guid id, DocumentViewModel doc, DocumentInternalParts internalParts) 
         : base(startFrame, duration, layerGuid, id, doc, internalParts)
     {
+        Children.CollectionChanged += ChildrenOnCollectionChanged;
         Document.StructureHelper.Find(LayerGuid).PropertyChanged += (sender, args) =>
         {
             if (args.PropertyName == nameof(StructureMemberViewModel.NodeNameBindable))
@@ -38,4 +58,22 @@ internal class KeyFrameGroupViewModel : KeyFrameViewModel, IKeyFrameGroupHandler
             }
         };
     }
+
+    private void ChildrenOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
+    {
+        OnPropertyChanged(nameof(StartFrameBindable));
+        OnPropertyChanged(nameof(DurationBindable));
+        
+        if (e.Action == NotifyCollectionChangedAction.Add)
+        {
+            foreach (var item in e.NewItems)
+            {
+                if (item is KeyFrameViewModel keyFrame)
+                {
+                    keyFrame.IsCollapsed = IsCollapsed;
+                    keyFrame.SetVisibility(IsVisible);
+                }
+            }
+        }
+    }
 }

+ 10 - 3
src/PixiEditor.AvaloniaUI/ViewModels/Document/KeyFrameViewModel.cs

@@ -14,6 +14,13 @@ internal abstract class KeyFrameViewModel : ObservableObject, IKeyFrameHandler
     private int durationBindable;
     private bool isVisibleBindable = true;
     private bool isSelected;
+    private bool isCollapsed;
+    
+    public bool IsCollapsed
+    {
+        get => isCollapsed;
+        set => SetProperty(ref isCollapsed, value);
+    }
 
     public DocumentViewModel Document { get; }
     protected DocumentInternalParts Internals { get; }
@@ -69,7 +76,7 @@ internal abstract class KeyFrameViewModel : ObservableObject, IKeyFrameHandler
         get => isVisibleBindable;
         set
         {
-            if(!Document.UpdateableChangeActive)
+            if (!Document.UpdateableChangeActive)
             {
                 Internals.ActionAccumulator.AddFinishedActions(new KeyFrameVisibility_Action(Id, value));
             }
@@ -107,7 +114,7 @@ internal abstract class KeyFrameViewModel : ObservableObject, IKeyFrameHandler
         durationBindable = newDuration;
         OnPropertyChanged(nameof(DurationBindable));
     }
-    
+
     public void ChangeFrameLength(int newStartFrame, int newDuration)
     {
         newStartFrame = Math.Max(0, newStartFrame);
@@ -115,7 +122,7 @@ internal abstract class KeyFrameViewModel : ObservableObject, IKeyFrameHandler
         Internals.ActionAccumulator.AddActions(
             new KeyFrameLength_Action(Id, newStartFrame, newDuration));
     }
-    
+
     public void EndChangeFrameLength()
     {
         Internals.ActionAccumulator.AddFinishedActions(new EndKeyFrameLength_Action());

+ 1 - 0
src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/ClipboardViewModel.cs

@@ -81,6 +81,7 @@ internal class ClipboardViewModel : SubViewModel<ViewModelMain>
     {
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
 
+        // TODO: Exception handling would probably be good
         var bitmap = Importer.GetPreviewBitmap(path);
         byte[] pixels = bitmap.ToWriteableBitmap().ExtractPixels();
 

+ 2 - 2
src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/FileViewModel.cs

@@ -250,7 +250,7 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
             .WithSize(image.Size)
             .WithGraph(x => x
                 .WithImageLayerNode(
-                    new LocalizedString("PASTED_IMAGE_NAME"),
+                    new LocalizedString("IMAGE"),
                     image, out int id)
                 .WithOutputNode(id, "Output")
             ));
@@ -273,7 +273,7 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
             .WithSize(surface.Size)
             .WithGraph(x => x
                 .WithImageLayerNode(
-                    new LocalizedString("PASTED_IMAGE_NAME"),
+                    new LocalizedString("IMAGE"),
                     surface, out int id)
                 .WithOutputNode(id, "Output")
             ));

+ 2 - 2
src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/WindowViewModel.cs

@@ -71,7 +71,7 @@ internal class WindowViewModel : SubViewModel<ViewModelMain>
     }
     
     [Command.Basic("PixiEditor.Window.FlipHorizontally", "FLIP_VIEWPORT_HORIZONTALLY", "FLIP_VIEWPORT_HORIZONTALLY", CanExecute = "PixiEditor.HasDocument",
-        Icon = PixiPerfectIcons.XFlip)]
+        Icon = PixiPerfectIcons.YFlip)]
     public void FlipViewportHorizontally()
     {
         if (ActiveWindow is ViewportWindowViewModel viewport)
@@ -81,7 +81,7 @@ internal class WindowViewModel : SubViewModel<ViewModelMain>
     }
     
     [Command.Basic("PixiEditor.Window.FlipVertically", "FLIP_VIEWPORT_VERTICALLY", "FLIP_VIEWPORT_VERTICALLY", CanExecute = "PixiEditor.HasDocument",
-        Icon = PixiPerfectIcons.YFlip)]
+        Icon = PixiPerfectIcons.XFlip)]
     public void FlipViewportVertically()
     {
         if (ActiveWindow is ViewportWindowViewModel viewport)

+ 20 - 0
src/PixiEditor.AvaloniaUI/Views/Animations/KeyFrame.cs

@@ -28,6 +28,15 @@ internal class KeyFrame : TemplatedControl
     public static readonly StyledProperty<double> MinProperty = AvaloniaProperty.Register<KeyFrame, double>(
         nameof(Min), 1);
 
+    public static readonly StyledProperty<bool> IsCollapsedProperty = AvaloniaProperty.Register<KeyFrame, bool>(
+        nameof(IsCollapsed)); 
+
+    public bool IsCollapsed
+    {
+        get => GetValue(IsCollapsedProperty);
+        set => SetValue(IsCollapsedProperty, value);
+    }
+    
     public double Min
     {
         get => GetValue(MinProperty);
@@ -58,6 +67,7 @@ internal class KeyFrame : TemplatedControl
     static KeyFrame()
     {
         IsSelectedProperty.Changed.Subscribe(IsSelectedChanged);
+        IsCollapsedProperty.Changed.Subscribe(IsCollapsedChanged);
     }
 
     protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
@@ -178,4 +188,14 @@ internal class KeyFrame : TemplatedControl
 
         keyFrame.PseudoClasses.Set(":selected", keyFrame.IsSelected);
     }
+    
+    private static void IsCollapsedChanged(AvaloniaPropertyChangedEventArgs e)
+    {
+        if (e.Sender is not KeyFrame keyFrame)
+        {
+            return;
+        }
+
+        keyFrame.PseudoClasses.Set(":collapsed", keyFrame.IsCollapsed);
+    }
 }

+ 23 - 1
src/PixiEditor.AvaloniaUI/Views/Animations/Timeline.cs

@@ -139,6 +139,8 @@ internal class Timeline : TemplatedControl, INotifyPropertyChanged
         set { SetValue(FpsProperty, value); }
     }
 
+    public int EndFrame => KeyFrames?.FrameCount > 0 ? KeyFrames.FrameCount : DefaultEndFrame;
+
     public ICommand DraggedKeyFrameCommand { get; }
     public ICommand ReleasedKeyFrameCommand { get; }
     public ICommand ClearSelectedKeyFramesCommand { get; }
@@ -172,6 +174,7 @@ internal class Timeline : TemplatedControl, INotifyPropertyChanged
         IsPlayingProperty.Changed.Subscribe(IsPlayingChanged);
         FpsProperty.Changed.Subscribe(FpsChanged);
         KeyFramesProperty.Changed.Subscribe(OnKeyFramesChanged);
+        DefaultEndFrameProperty.Changed.Subscribe(OnDefaultEndFrameChanged);
     }
 
     public Timeline()
@@ -332,7 +335,7 @@ internal class Timeline : TemplatedControl, INotifyPropertyChanged
 
     private void PlayTimerOnTick(object? sender, EventArgs e)
     {
-        if (ActiveFrame >= (KeyFrames.Count > 0 ? KeyFrames.FrameCount : DefaultEndFrame))
+        if (ActiveFrame >= EndFrame) 
         {
             ActiveFrame = 1;
         }
@@ -570,12 +573,16 @@ internal class Timeline : TemplatedControl, INotifyPropertyChanged
             newCollection.KeyFrameAdded += timeline.KeyFrames_KeyFrameAdded;
             newCollection.KeyFrameRemoved += timeline.KeyFrames_KeyFrameRemoved;
         }
+        
+        timeline.PropertyChanged(timeline, new PropertyChangedEventArgs(nameof(SelectedKeyFrames)));
+        timeline.PropertyChanged(timeline, new PropertyChangedEventArgs(nameof(EndFrame)));
     }
 
     private void KeyFrames_KeyFrameAdded(KeyFrameViewModel keyFrame)
     {
         keyFrame.PropertyChanged += KeyFrameOnPropertyChanged;
         PropertyChanged(this, new PropertyChangedEventArgs(nameof(SelectedKeyFrames)));
+        PropertyChanged(this, new PropertyChangedEventArgs(nameof(EndFrame)));
     }
 
     private void KeyFrames_KeyFrameRemoved(KeyFrameViewModel keyFrame)
@@ -587,6 +594,17 @@ internal class Timeline : TemplatedControl, INotifyPropertyChanged
         }
         
         PropertyChanged(this, new PropertyChangedEventArgs(nameof(SelectedKeyFrames)));
+        PropertyChanged(this, new PropertyChangedEventArgs(nameof(EndFrame)));
+    }
+    
+    private static void OnDefaultEndFrameChanged(AvaloniaPropertyChangedEventArgs e)
+    {
+        if (e.Sender is not Timeline timeline)
+        {
+            return;
+        }
+
+        timeline.PropertyChanged(timeline, new PropertyChangedEventArgs(nameof(EndFrame)));
     }
     
     private void KeyFrameOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
@@ -597,6 +615,10 @@ internal class Timeline : TemplatedControl, INotifyPropertyChanged
             {
                 PropertyChanged(this, new PropertyChangedEventArgs(nameof(SelectedKeyFrames)));
             }
+            else if (e.PropertyName == nameof(KeyFrameViewModel.StartFrameBindable) || e.PropertyName == nameof(KeyFrameViewModel.DurationBindable))
+            {
+                PropertyChanged(this, new PropertyChangedEventArgs(nameof(EndFrame)));
+            }
         }
     }
 }

+ 22 - 1
src/PixiEditor.AvaloniaUI/Views/Animations/TimelineGroupHeader.cs

@@ -1,18 +1,39 @@
 using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Metadata;
 using Avalonia.Controls.Primitives;
+using Avalonia.Interactivity;
 using Avalonia.Markup.Xaml.Templates;
 using PixiEditor.AvaloniaUI.ViewModels.Document;
 
 namespace PixiEditor.AvaloniaUI.Views.Animations;
 
+[TemplatePart("PART_CollapseButton", typeof(ToggleButton))]
+[PseudoClasses(":collapsed")]
 internal class TimelineGroupHeader : TemplatedControl
 {
     public static readonly StyledProperty<KeyFrameGroupViewModel> ItemProperty = AvaloniaProperty.Register<TimelineGroupHeader, KeyFrameGroupViewModel>(
-        "Item");
+        nameof(Item));
 
     public KeyFrameGroupViewModel Item
     {
         get => GetValue(ItemProperty);
         set => SetValue(ItemProperty, value);
     }
+
+    protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
+    {
+        base.OnApplyTemplate(e);
+        if (e.NameScope.Find("PART_CollapseButton") is { } collapseButton)
+        {
+            (collapseButton as ToggleButton).IsCheckedChanged += CollapseButtonOnIsCheckedChanged;
+        }
+    }
+
+    private void CollapseButtonOnIsCheckedChanged(object? sender, RoutedEventArgs e)
+    {
+        bool isCollapsed = (sender as ToggleButton).IsChecked == true;
+        PseudoClasses.Set(":collapsed", isCollapsed);
+        Item.IsCollapsed = isCollapsed;
+    }
 }

+ 14 - 5
src/PixiEditor.AvaloniaUI/Views/Animations/TimelineTickBar.cs

@@ -20,6 +20,15 @@ public class TimelineTickBar : Control
     public static readonly StyledProperty<int> MinValueProperty = AvaloniaProperty.Register<TimelineTickBar, int>(
         nameof(MinValue), 1);
 
+    public static readonly StyledProperty<IBrush> ForegroundProperty = AvaloniaProperty.Register<TimelineTickBar, IBrush>(
+        nameof(Foreground), Brushes.White);
+
+    public IBrush Foreground
+    {
+        get => GetValue(ForegroundProperty);
+        set => SetValue(ForegroundProperty, value);
+    }
+    
     public int MinValue
     {
         get => GetValue(MinValueProperty);
@@ -52,7 +61,7 @@ public class TimelineTickBar : Control
 
     static TimelineTickBar()
     {
-        AffectsRender<TimelineTickBar>(ScaleProperty, FillProperty, OffsetProperty);
+        AffectsRender<TimelineTickBar>(ScaleProperty, FillProperty, OffsetProperty, MinValueProperty, ForegroundProperty, MinLeftOffsetProperty);
     }
     
     private readonly int[] possibleLargeTickIntervals = { 1, 5, 10, 50, 100 };
@@ -83,10 +92,10 @@ public class TimelineTickBar : Control
             smallTickInterval = 1;
         }
 
-        Pen largeTickPen = new Pen(Fill);
-        Pen smallTickPen = new Pen(Fill, 0.5);
+        Pen largeTickPen = new Pen(Fill, thickness: 2);
+        Pen smallTickPen = new Pen(Fill, 1.5);
         
-        int largeStart = visibleMin - (visibleMin % largeTickInterval);
+        int largeStart = visibleMin - (visibleMin % largeTickInterval) - MinValue;
         
         RenderBigTicks(context, largeStart, visibleMax, largeTickInterval, frameWidth, largeTickPen, height);
         
@@ -117,7 +126,7 @@ public class TimelineTickBar : Control
             context.DrawLine(largeTickPen, new Point(x, height), new Point(x, height * 0.55f));
             
             var text = new FormattedText((i + MinValue).ToString(), CultureInfo.CurrentCulture, FlowDirection.LeftToRight,
-                Typeface.Default, 12, Fill);
+                Typeface.Default, 12, Foreground);
             
             double textCenter = text.WidthIncludingTrailingWhitespace / 2;
             Point textPosition = new Point(x - textCenter, height * 0.05);

+ 67 - 52
src/PixiEditor.AvaloniaUI/Views/Input/NumberInput.cs

@@ -19,29 +19,33 @@ internal partial class NumberInput : TextBox
 {
     public static readonly StyledProperty<double> ValueProperty =
         AvaloniaProperty.Register<NumberInput, double>(
-            nameof(Value), 0);
+            nameof(Value), 0, coerce: CoerceValue);
 
     public static readonly StyledProperty<double> MinProperty =
         AvaloniaProperty.Register<NumberInput, double>(
-            nameof(Min), float.NegativeInfinity);
+            nameof(Min), float.NegativeInfinity, coerce: CoerceValue);
 
     public static readonly StyledProperty<double> MaxProperty =
         AvaloniaProperty.Register<NumberInput, double>(
-            nameof(Max), double.PositiveInfinity);
+            nameof(Max), double.PositiveInfinity, coerce: CoerceValue);
 
-    public static readonly StyledProperty<string> FormattedValueProperty = AvaloniaProperty.Register<NumberInput, string>(
-        nameof(FormattedValue), "0");
+    public static readonly StyledProperty<string> FormattedValueProperty =
+        AvaloniaProperty.Register<NumberInput, string>(
+            nameof(FormattedValue), "0");
+
+    public static readonly StyledProperty<bool> EnableScrollChangeProperty =
+        AvaloniaProperty.Register<NumberInput, bool>(
+            "EnableScrollChange", true);
 
-    public static readonly StyledProperty<bool> EnableScrollChangeProperty = AvaloniaProperty.Register<NumberInput, bool>(
-        "EnableScrollChange", true);
     public string FormattedValue
     {
         get => GetValue(FormattedValueProperty);
         set => SetValue(FormattedValueProperty, value);
     }
 
-    public static readonly StyledProperty<bool> SelectOnMouseClickProperty = AvaloniaProperty.Register<NumberInput, bool>(
-        nameof(SelectOnMouseClick), true);
+    public static readonly StyledProperty<bool> SelectOnMouseClickProperty =
+        AvaloniaProperty.Register<NumberInput, bool>(
+            nameof(SelectOnMouseClick), true);
 
     public static readonly StyledProperty<bool> ConfirmOnEnterProperty = AvaloniaProperty.Register<NumberInput, bool>(
         nameof(ConfirmOnEnter), true);
@@ -60,14 +64,8 @@ internal partial class NumberInput : TextBox
 
     private static Regex regex;
 
-    public int Decimals
-    {
-        get { return (int)GetValue(DecimalsProperty); }
-        set { SetValue(DecimalsProperty, value); }
-    }
-
     public static readonly StyledProperty<int> DecimalsProperty =
-        AvaloniaProperty.Register<NumberInput, int>(nameof(Decimals), 2);
+        AvaloniaProperty.Register<NumberInput, int>(nameof(Decimals), 2, coerce: CoerceDecimals);
 
     public Action OnScrollAction
     {
@@ -96,6 +94,13 @@ internal partial class NumberInput : TextBox
         set => SetValue(MaxProperty, value);
     }
 
+
+    public int Decimals
+    {
+        get { return (int)GetValue(DecimalsProperty); }
+        set { SetValue(DecimalsProperty, value); }
+    }
+
     public static readonly StyledProperty<bool> FocusNextProperty =
         AvaloniaProperty.Register<NumberInput, bool>(
             nameof(FocusNext));
@@ -107,10 +112,11 @@ internal partial class NumberInput : TextBox
     }
 
     private static readonly DataTable DataTable = new DataTable();
+
     private static char[] allowedChars = new char[]
     {
-        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '-', '*', '/', '(', ')', '.', ',', ' ',
-        'i', 'n', 'f', 't', 'y', 'e', 'I', 'N', 'F', 'T', 'Y', 'E'
+        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '-', '*', '/', '(', ')', '.', ',', ' ', 'i', 'n',
+        'f', 't', 'y', 'e', 'I', 'N', 'F', 'T', 'Y', 'E'
     };
 
 
@@ -124,10 +130,10 @@ internal partial class NumberInput : TextBox
 
     private Control? leftGrabber;
     private Control? rightGrabber;
-    
+
     private double _pressedValue;
     private double _pressedRelativeX;
-    
+
     static NumberInput()
     {
         ValueProperty.Changed.Subscribe(OnValueChanged);
@@ -143,11 +149,7 @@ internal partial class NumberInput : TextBox
         behaviors.Add(behavior);
         Interaction.SetBehaviors(this, behaviors);
 
-        Binding binding = new Binding(nameof(FormattedValue))
-        {
-            Source = this,
-            Mode = BindingMode.TwoWay
-        };
+        Binding binding = new Binding(nameof(FormattedValue)) { Source = this, Mode = BindingMode.TwoWay };
 
         this.Bind(TextProperty, binding);
 
@@ -159,10 +161,10 @@ internal partial class NumberInput : TextBox
     protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
     {
         base.OnApplyTemplate(e);
-        
-        InnerLeftContent = leftGrabber = CreateMouseGrabber(); 
+
+        InnerLeftContent = leftGrabber = CreateMouseGrabber();
         leftGrabber.HorizontalAlignment = HorizontalAlignment.Left;
-        InnerRightContent = rightGrabber = CreateMouseGrabber(); 
+        InnerRightContent = rightGrabber = CreateMouseGrabber();
         rightGrabber.HorizontalAlignment = HorizontalAlignment.Right;
     }
 
@@ -172,10 +174,10 @@ internal partial class NumberInput : TextBox
         {
             rightGrabber.IsVisible = false;
         }
-        
+
         leftGrabber.Height = e.NewSize.Height - 10;
         leftGrabber.Width = e.NewSize.Width / 4f;
-        
+
         rightGrabber.Height = e.NewSize.Height - 10;
         rightGrabber.Width = e.NewSize.Width / 4f;
     }
@@ -184,16 +186,15 @@ internal partial class NumberInput : TextBox
     {
         var grabber = new Grid()
         {
-            Cursor = new Cursor(StandardCursorType.SizeWestEast),
-            Background = Brushes.Transparent,
+            Cursor = new Cursor(StandardCursorType.SizeWestEast), Background = Brushes.Transparent,
         };
 
         grabber.PointerPressed += GrabberPressed;
         grabber.PointerMoved += GrabberMoved;
-        
+
         return grabber;
     }
-    
+
     private void GrabberPressed(object sender, PointerPressedEventArgs e)
     {
         e.Pointer.Capture(leftGrabber);
@@ -201,53 +202,67 @@ internal partial class NumberInput : TextBox
         _pressedRelativeX = e.GetPosition(this).X;
         e.Handled = true;
     }
-    
+
     private void GrabberMoved(object sender, PointerEventArgs e)
     {
-        if(e.Pointer.Captured != null && (e.Pointer.Captured.Equals(leftGrabber) || e.Pointer.Captured.Equals(rightGrabber)))
+        if (e.Pointer.Captured != null &&
+            (e.Pointer.Captured.Equals(leftGrabber) || e.Pointer.Captured.Equals(rightGrabber)))
         {
             double relativeX = e.GetPosition(this).X;
             double diff = relativeX - _pressedRelativeX;
 
             double pixelsPerUnit = 5;
-            
+
             double newValue = _pressedValue + diff / pixelsPerUnit;
-            Value = (float)Math.Round(Math.Clamp(newValue, Min, Max), Decimals);
-            e.Handled = true; 
+            Value = newValue;
+            e.Handled = true;
         }
     }
 
     private void BindTextBoxBehavior(TextBoxFocusBehavior behavior)
     {
-        Binding focusNextBinding = new Binding(nameof(FocusNext))
-        {
-            Source = this,
-            Mode = BindingMode.OneWay
-        };
+        Binding focusNextBinding = new Binding(nameof(FocusNext)) { Source = this, Mode = BindingMode.OneWay };
 
         behavior.Bind(TextBoxFocusBehavior.FocusNextProperty, focusNextBinding);
 
         Binding selectOnMouseClickBinding = new Binding(nameof(SelectOnMouseClick))
         {
-            Source = this,
-            Mode = BindingMode.OneWay
+            Source = this, Mode = BindingMode.OneWay
         };
 
         behavior.Bind(TextBoxFocusBehavior.SelectOnMouseClickProperty, selectOnMouseClickBinding);
 
         Binding confirmOnEnterBinding = new Binding(nameof(ConfirmOnEnter))
         {
-            Source = this,
-            Mode = BindingMode.OneWay
+            Source = this, Mode = BindingMode.OneWay
         };
 
         behavior.Bind(TextBoxFocusBehavior.ConfirmOnEnterProperty, confirmOnEnterBinding);
     }
 
+    private static double CoerceValue(AvaloniaObject o, double value)
+    {
+        double min = (double)o.GetValue(MinProperty);
+        double max = (double)o.GetValue(MaxProperty);
+        int decimals = (int)o.GetValue(DecimalsProperty);
+
+        return Math.Round(Math.Clamp(value, min, max), decimals);
+    }
+    
+    private static int CoerceDecimals(AvaloniaObject o, int value)
+    {
+        if (value < 0)
+        {
+            value = 0;
+        }
+        
+        return value;
+    }
+
     private static void OnValueChanged(AvaloniaPropertyChangedEventArgs<double> e)
     {
         NumberInput input = (NumberInput)e.Sender;
-        input.Value = (float)Math.Round(Math.Clamp(e.NewValue.Value, input.Min, input.Max), input.Decimals);
+        //input.Value = (float)Math.Round(Math.Clamp(e.NewValue.Value, input.Min, input.Max), input.Decimals);
 
         var preFormatted = FormatValue(input.Value, input.Decimals);
         input.FormattedValue = preFormatted;
@@ -317,7 +332,7 @@ internal partial class NumberInput : TextBox
     private static void FormattedValueChanged(AvaloniaPropertyChangedEventArgs<string> e)
     {
         NumberInput input = (NumberInput)e.Sender;
-        if(ContainsInvalidCharacter(e.NewValue.Value))
+        if (ContainsInvalidCharacter(e.NewValue.Value))
         {
             input.FormattedValue = e.OldValue.Value;
         }
@@ -325,7 +340,7 @@ internal partial class NumberInput : TextBox
 
     private static bool ContainsInvalidCharacter(string text)
     {
-        if(text == null)
+        if (text == null)
         {
             return false;
         }
@@ -339,7 +354,7 @@ internal partial class NumberInput : TextBox
         {
             return;
         }
-        
+
         int step = (int)e.Delta.Y;
 
         double newValue = Value;

+ 4 - 3
src/PixiEditor.AvaloniaUI/Views/Input/SizeInput.axaml

@@ -5,8 +5,6 @@
              xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
              xmlns:ui="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
-             xmlns:converters="clr-namespace:PixiEditor.AvaloniaUI.Helpers.Converters"
-             xmlns:behaviours="clr-namespace:PixiEditor.AvaloniaUI.Helpers.Behaviours"
              xmlns:input="clr-namespace:PixiEditor.AvaloniaUI.Views.Input"
              mc:Ignorable="d" Focusable="True"
              d:DesignHeight="30" Name="uc"
@@ -37,6 +35,8 @@
                      Decimals="0"
                      x:Name="input"
                      Value="{Binding Size, ElementName=uc, Mode=TwoWay}"
+                     Min="1"
+                     Max="{Binding MaxSize, ElementName=uc}"
                      d:Value="22"
                      FocusNext="{Binding FocusNext, ElementName=uc}"
                      SelectOnMouseClick="{Binding BehaveLikeSmallEmbeddedField, ElementName=uc}"
@@ -44,7 +44,8 @@
                      Width="43"/>
             <Grid Grid.Column="1" Background="{Binding BorderBrush, ElementName=border}"
                   d:Background="{DynamicResource ThemeAccentBrush}"/>
-            <TextBlock ui:Translator.Key="{Binding Unit, ElementName=uc, Converter={converters:EnumToStringConverter}}" TextAlignment="Right"
+            <TextBlock ui:Translator.Key="{Binding Unit, ElementName=uc}" 
+                       TextAlignment="Right"
                        Grid.Column="2" Margin="5,0" VerticalAlignment="Center"
             />
         </Grid>

+ 5 - 45
src/PixiEditor.AvaloniaUI/Views/Input/SizeInput.axaml.cs

@@ -11,7 +11,7 @@ namespace PixiEditor.AvaloniaUI.Views.Input;
 internal partial class SizeInput : UserControl
 {
     public static readonly StyledProperty<int> SizeProperty =
-        AvaloniaProperty.Register<SizeInput, int>(nameof(Size), defaultValue: 1, coerce: Coerce);
+        AvaloniaProperty.Register<SizeInput, int>(nameof(Size), defaultValue: 1);
 
     public static readonly StyledProperty<int> MaxSizeProperty =
         AvaloniaProperty.Register<SizeInput, int>(nameof(MaxSize), defaultValue: int.MaxValue);
@@ -19,8 +19,8 @@ internal partial class SizeInput : UserControl
     public static readonly StyledProperty<bool> BehaveLikeSmallEmbeddedFieldProperty =
         AvaloniaProperty.Register<SizeInput, bool>(nameof(BehaveLikeSmallEmbeddedField), defaultValue: true);
 
-    public static readonly StyledProperty<SizeUnit> UnitProperty =
-        AvaloniaProperty.Register<SizeInput, SizeUnit>(nameof(Unit), defaultValue: SizeUnit.Pixel);
+    public static readonly StyledProperty<string> UnitProperty =
+        AvaloniaProperty.Register<SizeInput, string>(nameof(Unit), defaultValue: "PIXEL_UNIT");
 
     public static readonly StyledProperty<bool> FocusNextProperty = AvaloniaProperty.Register<SizeInput, bool>(
         nameof(FocusNext), defaultValue: true);
@@ -58,11 +58,6 @@ internal partial class SizeInput : UserControl
         set => SetValue(BehaveLikeSmallEmbeddedFieldProperty, value);
     }
 
-    static SizeInput()
-    {
-        SizeProperty.Changed.Subscribe(InputSizeChanged);
-    }
-
     public SizeInput()
     {
         InitializeComponent();
@@ -97,47 +92,12 @@ internal partial class SizeInput : UserControl
             input.Focus();
     }
 
-    public SizeUnit Unit
+    public string Unit
     {
-        get => (SizeUnit)GetValue(UnitProperty);
+        get => (string)GetValue(UnitProperty);
         set => SetValue(UnitProperty, value);
     }
 
-
-    private static int Coerce(AvaloniaObject sender, int value)
-    {
-        if (value <= 0)
-        {
-            return 1;
-        }
-
-        int maxSize = sender.GetValue(MaxSizeProperty);
-        
-        if (value > maxSize)
-        {
-            return maxSize;
-        }
-        
-        return value;
-    }
-
-    private static void InputSizeChanged(AvaloniaPropertyChangedEventArgs<int> e)
-    {
-        int newValue = e.NewValue.Value;
-        int maxSize = (int)e.Sender.GetValue(MaxSizeProperty);
-
-        if (newValue > maxSize)
-        {
-            e.Sender.SetValue(SizeProperty, maxSize);
-
-            return;
-        }
-        else if (newValue <= 0)
-        {
-            e.Sender.SetValue(SizeProperty, 1);
-        }
-    }
-
     private void Border_MouseWheel(object? sender, PointerWheelEventArgs e)
     {
         int step = (int)e.Delta.Y / 100;

+ 1 - 1
src/PixiEditor.AvaloniaUI/Views/Input/SizePicker.axaml

@@ -55,7 +55,7 @@
                                      x:Name="PercentageSizePicker"
                                      IsEnabled="{Binding EditingEnabled, ElementName=uc}"
                                      Size="{Binding Path=ChosenPercentageSize, ElementName=uc, Mode=TwoWay}"
-                                     Unit="Percentage"
+                                     Unit="%"
                                      Margin="-10,0,0,0"
                                      MaxSize="9999"
                                      Width="{Binding Bounds.Width, ElementName=WidthPicker}">

+ 2 - 2
src/PixiEditor.AvaloniaUI/Views/Main/ViewportControls/Viewport.axaml

@@ -85,13 +85,13 @@
                             <ToggleButton Width="32" Height="32" ui:Translator.TooltipKey="FLIP_VIEWPORT_HORIZONTALLY"
                                           Classes="OverlayToggleButton pixi-icon"
                                           IsChecked="{Binding FlipX, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=viewportControls:Viewport}, Mode=TwoWay}"
-                                          Content="{DynamicResource icon-x-flip}"
+                                          Content="{DynamicResource icon-y-flip}"
                                           Cursor="Hand"/>
                             <ToggleButton Margin="10 0 0 0" Width="32" Height="32"
                                           ui:Translator.TooltipKey="FLIP_VIEWPORT_VERTICALLY"
                                           Classes="OverlayToggleButton pixi-icon"
                                           IsChecked="{Binding FlipY, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=viewportControls:Viewport}, Mode=TwoWay}"
-                                            Content="{DynamicResource icon-y-flip}"
+                                            Content="{DynamicResource icon-x-flip}"
                                           Cursor="Hand"/>
                         </StackPanel>
                     </StackPanel>

+ 2 - 2
src/PixiEditor.AvaloniaUI/Views/MainView.axaml

@@ -32,9 +32,9 @@
 
                 <tools:Toolbar Grid.Row="0" DataContext="{Binding .}" />
                 <tools:ToolsPicker ZIndex="2" Grid.Row="1"
-                                   Margin="10 0 0 0"
+                                   Margin="10 50 0 0"
                                    HorizontalAlignment="Left"
-                                   VerticalAlignment="Center"
+                                   VerticalAlignment="Top"
                                    Tools="{Binding Path=ToolsSubViewModel.ToolSet}" />
                 <controls:DockableAreaRegion Grid.Row="1"
                                              Root="{Binding LayoutSubViewModel.LayoutManager.ActiveLayout.Root}"

+ 1 - 1
src/PixiEditor.AvaloniaUI/Views/Rendering/Scene.cs

@@ -496,7 +496,7 @@ internal class DrawSceneOperation : SkiaDrawOperation
 
     public override void Render(ISkiaSharpApiLease lease)
     {
-        if (Surface == null || Document == null) return;
+        if (Surface == null || Surface.IsDisposed || Document == null) return;
 
         SKCanvas canvas = lease.SkCanvas;
 

+ 1 - 0
src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IReadOnlyKeyFrameData.cs

@@ -8,4 +8,5 @@ public interface IReadOnlyKeyFrameData
     object Data { get; }
     string AffectedElement { get; }
     bool IsVisible { get; }
+    bool IsInFrame(int frame);
 }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/Animation/CreateRasterKeyFrame_Change.cs

@@ -27,7 +27,7 @@ internal class CreateRasterKeyFrame_Change : Change
 
     public override bool InitializeAndValidate(Document target)
     {
-        return target.TryFindMember(_targetLayerGuid, out _layer);
+        return _frame != 0 && target.TryFindMember(_targetLayerGuid, out _layer);
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/Drawing/ApplyLayerMask_Change.cs

@@ -63,7 +63,7 @@ internal class ApplyLayerMask_Change : Change
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
         var layer = target.FindMemberOrThrow<ImageLayerNode>(layerGuid);
-        if (layer.Mask is not null)
+        if (layer.Mask.Value is not null)
             throw new InvalidOperationException("Cannot restore layer mask, it already has one");
         if (savedLayer is null || savedMask is null)
             throw new InvalidOperationException("Cannot restore layer mask, no saved data");

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/Drawing/DrawingChangeHelper.cs

@@ -109,7 +109,7 @@ internal static class DrawingChangeHelper
         return drawOnMask switch
         {
             // If it should draw on the mask, the mask can't be null
-            true when member.Mask.Value is null => false,
+            true when member.Mask.NonOverridenValue is null => false,
             // If it should not draw on the mask, the member can't be a folder
             false when member is FolderNode => false,
             _ => true

+ 4 - 4
src/PixiEditor.ChangeableDocument/Changes/Properties/LayerStructure/DeleteStructureMemberMask_Change.cs

@@ -15,17 +15,17 @@ internal class DeleteStructureMemberMask_Change : Change
 
     public override bool InitializeAndValidate(Document target)
     {
-        if (!target.TryFindMember(memberGuid, out var member) || member.Mask is null)
+        if (!target.TryFindMember(memberGuid, out var member) || member.Mask.NonOverridenValue is null)
             return false;
         
-        storedMask = member.Mask.Value.CloneFromCommitted();
+        storedMask = member.Mask.NonOverridenValue.CloneFromCommitted();
         return true;
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
     {
         var member = target.FindMemberOrThrow(memberGuid);
-        if (member.Mask is null)
+        if (member.Mask.NonOverridenValue is null)
             throw new InvalidOperationException("Cannot delete the mask; Target member has no mask");
         member.Mask.NonOverridenValue.Dispose();
         member.Mask.NonOverridenValue = null;
@@ -37,7 +37,7 @@ internal class DeleteStructureMemberMask_Change : Change
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
         var member = target.FindMemberOrThrow(memberGuid);
-        if (member.Mask is not null)
+        if (member.Mask.NonOverridenValue is not null)
             throw new InvalidOperationException("Cannot revert mask deletion; The target member already has a mask");
         member.Mask.NonOverridenValue = storedMask!.CloneFromCommitted();
 

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changes/Root/ClipCanvas_Change.cs

@@ -52,10 +52,10 @@ internal class ClipCanvas_Change : ResizeBasedChangeBase
                 });
             }
             
-            if (member.Mask is null)
+            if (member.Mask.NonOverridenValue is null)
                 return;
             
-            Resize(member.Mask.Value, member.Id, newBounds.Size, -newBounds.Pos, deletedMaskChunks);
+            Resize(member.Mask.NonOverridenValue, member.Id, newBounds.Size, -newBounds.Pos, deletedMaskChunks);
         });
 
         ignoreInUndo = false;

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changes/Root/Crop_Change.cs

@@ -42,10 +42,10 @@ internal class Crop_Change : ResizeBasedChangeBase
                     Resize(frame, layer.Id, rect.Size, rect.Pos * -1, deletedChunks);
                 });
             }
-            if (member.Mask.Value is null)
+            if (member.Mask.NonOverridenValue is null)
                 return;
 
-            Resize(member.Mask.Value, member.Id, rect.Size, rect.Pos * -1, deletedMaskChunks);
+            Resize(member.Mask.NonOverridenValue, member.Id, rect.Size, rect.Pos * -1, deletedMaskChunks);
         });
         
         ignoreInUndo = false;

+ 4 - 4
src/PixiEditor.ChangeableDocument/Changes/Root/FlipImage_Change.cs

@@ -113,12 +113,12 @@ internal sealed class FlipImage_Change : Change
                 }
                 // TODO: Add support for non-raster layers
 
-                if (member.Mask.Value is not null)
+                if (member.Mask.NonOverridenValue is not null)
                 {
-                    FlipImage(member.Mask.Value);
+                    FlipImage(member.Mask.NonOverridenValue);
                     changes.Add(
-                        new MaskArea_ChangeInfo(member.Id, member.Mask.Value.FindAffectedArea()));
-                    member.Mask.Value.CommitChanges();
+                        new MaskArea_ChangeInfo(member.Id, member.Mask.NonOverridenValue.FindAffectedArea()));
+                    member.Mask.NonOverridenValue.CommitChanges();
                 }
             }
         });

+ 4 - 4
src/PixiEditor.ChangeableDocument/Changes/Root/ResizeBasedChangeBase.cs

@@ -60,11 +60,11 @@ internal abstract class ResizeBasedChangeBase : Change
 
             // TODO: Add support for different Layer types?
 
-            if (member.Mask.Value is null)
+            if (member.Mask.NonOverridenValue is null)
                 return;
-            member.Mask.Value.EnqueueResize(_originalSize);
-            deletedMaskChunks[member.Id][0].ApplyChunksToImage(member.Mask.Value);
-            member.Mask.Value.CommitChanges();
+            member.Mask.NonOverridenValue.EnqueueResize(_originalSize);
+            deletedMaskChunks[member.Id][0].ApplyChunksToImage(member.Mask.NonOverridenValue);
+            member.Mask.NonOverridenValue.CommitChanges();
         });
 
         target.HorizontalSymmetryAxisY = _originalHorAxisY;

+ 25 - 12
src/PixiEditor.ChangeableDocument/Changes/Root/RotateImage_Change.cs

@@ -158,10 +158,10 @@ internal sealed class RotateImage_Change : Change
 
                 // TODO: Add support for different Layer types
 
-                if (member.Mask.Value is null)
+                if (member.Mask.NonOverridenValue is null)
                     return;
 
-                Resize(member.Mask.Value, member.Id, deletedMaskChunks, null);
+                Resize(member.Mask.NonOverridenValue, member.Id, deletedMaskChunks, null);
             }
         });
 
@@ -199,10 +199,10 @@ internal sealed class RotateImage_Change : Change
                 }
             }
 
-            if (member.Mask.Value is null)
+            if (member.Mask.NonOverridenValue is null)
                 return;
 
-            Resize(member.Mask.Value, member.Id, deletedMaskChunks, null);
+            Resize(member.Mask.NonOverridenValue, member.Id, deletedMaskChunks, null);
         });
 
         return new Size_ChangeInfo(newSize, target.VerticalSymmetryAxisX, target.HorizontalSymmetryAxisY);
@@ -237,19 +237,32 @@ internal sealed class RotateImage_Change : Change
             if (membersToRotate.Count > 0 && !membersToRotate.Contains(member.Id)) return;
             if (member is ImageLayerNode layer)
             {
-                var layerImage = layer.GetLayerImageAtFrame(frame.Value);
-                layerImage.EnqueueResize(originalSize);
-                deletedChunks[layer.Id].ApplyChunksToImage(layerImage);
-                revertChanges.Add(new LayerImageArea_ChangeInfo(layer.Id, layerImage.FindAffectedArea()));
-                layerImage.CommitChanges();
+                if (frame != null)
+                {
+                    var layerImage = layer.GetLayerImageAtFrame(frame.Value);
+                    layerImage.EnqueueResize(originalSize);
+                    deletedChunks[layer.Id].ApplyChunksToImage(layerImage);
+                    revertChanges.Add(new LayerImageArea_ChangeInfo(layer.Id, layerImage.FindAffectedArea()));
+                    layerImage.CommitChanges();
+                }
+                else
+                {
+                    layer.ForEveryFrame(img =>
+                    {
+                        img.EnqueueResize(originalSize);
+                        deletedChunks[layer.Id].ApplyChunksToImage(img);
+                        revertChanges.Add(new LayerImageArea_ChangeInfo(layer.Id, img.FindAffectedArea()));
+                        img.CommitChanges();
+                    });
+                }
             }
 
-            if (member.Mask.Value is null)
+            if (member.Mask.NonOverridenValue is null)
                 return;
-            member.Mask.Value.EnqueueResize(originalSize);
+            member.Mask.NonOverridenValue.EnqueueResize(originalSize);
             deletedMaskChunks[member.Id].ApplyChunksToImage(member.Mask.Value);
             revertChanges.Add(new LayerImageArea_ChangeInfo(member.Id, member.Mask.Value.FindAffectedArea()));
-            member.Mask.Value.CommitChanges();
+            member.Mask.NonOverridenValue.CommitChanges();
         });
 
         DisposeDeletedChunks();

+ 29 - 0
src/PixiEditor.ClosedBeta/ClosedBetaExtension.cs

@@ -0,0 +1,29 @@
+using PixiEditor.Extensions.Sdk;
+
+namespace PixiEditor.ClosedBeta;
+
+public class ClosedBetaExtension : PixiEditorExtension
+{
+    public override void OnInitialized()
+    {
+        if (Api.Preferences.GetPreference<bool>("ClosedBetaWelcomeShown"))
+        {
+            return;   
+        }
+        
+        WelcomeMessage welcomeMessage = new();
+        var window = Api.WindowProvider.CreatePopupWindow("Welcome to the closed beta!", welcomeMessage);
+        welcomeMessage.OnContinue += () =>
+        {
+            Api.Preferences.UpdatePreference("ClosedBetaWelcomeShown", true);
+            window.Close();
+        };
+
+        window.Width = 800;
+        window.Height = 600;
+        
+        window.CanResize = false;
+        window.CanMinimize = false;
+        window.ShowDialog();
+    }
+}

+ 30 - 0
src/PixiEditor.ClosedBeta/PixiEditor.ClosedBeta.csproj

@@ -0,0 +1,30 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>net8.0</TargetFramework>
+    <RuntimeIdentifier>wasi-wasm</RuntimeIdentifier>
+    <OutputType>Exe</OutputType>
+    <ImplicitUsings>enable</ImplicitUsings>
+    <Nullable>disable</Nullable>
+    <PublishTrimmed>true</PublishTrimmed>
+    <WasmSingleFileBundle>true</WasmSingleFileBundle>
+    <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
+    <PixiExtOutputPath>..\PixiEditor.AvaloniaUI.Desktop\bin\Debug\net8.0\win-x64\Extensions</PixiExtOutputPath>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\PixiEditor.Extensions.Sdk\PixiEditor.Extensions.Sdk.csproj"/>
+  </ItemGroup>
+
+  <ItemGroup>
+    <None Update="extension.json">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </None>
+  </ItemGroup>
+
+
+  <!--Below is not required if you use Nuget package, this sample references project directly, so it must be here-->
+  <Import Project="..\PixiEditor.Extensions.Sdk\build\PixiEditor.Extensions.Sdk.props"/>
+  <Import Project="..\PixiEditor.Extensions.Sdk\build\PixiEditor.Extensions.Sdk.targets"/>
+
+</Project>

+ 9 - 0
src/PixiEditor.ClosedBeta/Program.cs

@@ -0,0 +1,9 @@
+namespace PixiEditor.ClosedBeta;
+
+public static class Program
+{
+    public static void Main(string[] args)
+    {
+
+    }
+}

+ 15 - 0
src/PixiEditor.ClosedBeta/WelcomeMessage.cs

@@ -0,0 +1,15 @@
+using PixiEditor.Extensions.Sdk.Api.FlyUI;
+
+namespace PixiEditor.ClosedBeta;
+
+public class WelcomeMessage : StatefulElement<WelcomeMessageState>
+{
+    public event Action OnContinue;
+    
+    public override WelcomeMessageState CreateState()
+    { 
+        WelcomeMessageState state = new WelcomeMessageState();
+        state.OnContinue += () => OnContinue?.Invoke();
+        return state;
+    }
+}

+ 77 - 0
src/PixiEditor.ClosedBeta/WelcomeMessageState.cs

@@ -0,0 +1,77 @@
+using PixiEditor.Extensions.CommonApi.FlyUI.Properties;
+using PixiEditor.Extensions.Sdk;
+using PixiEditor.Extensions.Sdk.Api.FlyUI;
+
+namespace PixiEditor.ClosedBeta;
+
+public class WelcomeMessageState : State
+{
+    private const string Body = @"
+We are extremely exicted to share this version to you, early testers. Before you jump in and test all the new things,
+we have a few things to note:
+
+- This is a very first publicly available version of PixiEditor 2.0. Not every feature promised in the roadmap is
+  implemented yet. 
+- App is not production ready! Expect bugs, crashes, unfinished features, placeholders and other signs of development.
+- Your feedback is the most important thing of this beta, please take a moment to report any issues and suggestions on the Discord channel.
+- Promised features available in this beta are: Animations, Procedural Art (Nodes)
+
+Click on below checkboxes that you understand what you are getting into and you are ready to test the app.
+
+I understand that:
+";
+
+    private bool[] _checkboxes = new bool[4];
+
+    public event Action OnContinue;
+
+    public override LayoutElement BuildElement()
+    {
+        return new Layout(body:
+            new Align(
+                Alignment.TopCenter,
+                new Column(
+                    new Center(new Text("Welcome to the closed beta of PixiEditor 2.0!", TextWrap.Wrap,
+                        FontStyle.Normal,
+                        fontSize: 24)),
+                    new Text(Body, TextWrap.Wrap, fontSize: 16),
+                    new CheckBox(
+                        new Text("The app is unstable and may crash and freeze", fontSize: 16,
+                            fontStyle: FontStyle.Italic),
+                        onCheckedChanged: (args) => CheckboxChanged(args.Sender as CheckBox, 0)),
+                    new CheckBox(
+                        new Text("I may encounter unfinished features and placeholders", fontSize: 16,
+                            fontStyle: FontStyle.Italic),
+                        onCheckedChanged: (args) => CheckboxChanged(args.Sender as CheckBox, 1)),
+                    new CheckBox(new Text("I may lose my work due to bugs", fontSize: 16, fontStyle: FontStyle.Italic),
+                        onCheckedChanged: (args) => CheckboxChanged(args.Sender as CheckBox, 2)),
+                    new CheckBox(
+                        new Text("I will have a lot of fun testing the app", fontSize: 16,
+                            fontStyle: FontStyle.Italic),
+                        onCheckedChanged: (args) => CheckboxChanged(args.Sender as CheckBox, 3)),
+                    new Container(
+                        margin: new Edges(0, 5, 0, 0),
+                        width: AllCheckBoxesChecked() ? 100 : 200,
+                        child:
+                        AllCheckBoxesChecked()
+                            ? new Button(new Text("Continue"), onClick: (args) => { OnContinue?.Invoke(); })
+                            : new Text("Select All Checkboxes to continue")
+                    )
+                )
+            )
+        );
+    }
+
+    void CheckboxChanged(CheckBox checkBox, int index)
+    {
+        SetState(() =>
+        {
+            _checkboxes[index] = checkBox.IsChecked;
+        });
+    }
+
+    private bool AllCheckBoxesChecked()
+    {
+        return _checkboxes.All(x => x);
+    }
+}

+ 20 - 0
src/PixiEditor.ClosedBeta/extension.json

@@ -0,0 +1,20 @@
+{
+  "displayName": "PixiEditor Closed Beta 1",
+  "uniqueName": "PixiEditor.ClosedBeta1",
+  "description": "First Closed Beta of PixiEditor 2.0",
+  "version": "1.0.0",
+  "author": {
+    "name": "PixiEditor",
+    "email": "[email protected]",
+    "website": "https://pixieditor.net"
+  },
+  "publisher": {
+    "name": "PixiEditor",
+    "email": "[email protected]",
+    "website": "https://pixieditor.net"
+  },
+  "license": "Copyright PixiEditor Organization",
+  "categories": [
+    "Beta"
+  ]
+}

+ 1 - 0
src/PixiEditor.Extensions.CommonApi/FlyUI/Events/ElementEventArgs.cs

@@ -2,6 +2,7 @@
 
 public class ElementEventArgs
 {
+    public object Sender { get; set; } 
     public static ElementEventArgs Empty { get; } = new ElementEventArgs();
 }
 

+ 11 - 0
src/PixiEditor.Extensions.CommonApi/FlyUI/Events/ToggleEventArgs.cs

@@ -0,0 +1,11 @@
+namespace PixiEditor.Extensions.CommonApi.FlyUI.Events;
+
+public class ToggleEventArgs : ElementEventArgs<ToggleEventArgs>
+{
+    public bool IsToggled { get; }
+
+    public ToggleEventArgs(bool isToggled)
+    {
+        IsToggled = isToggled;
+    }
+}

+ 41 - 1
src/PixiEditor.Extensions.CommonApi/Palettes/PaletteColor.Impl.cs

@@ -1,6 +1,6 @@
 namespace PixiEditor.Extensions.CommonApi.Palettes;
 
-public partial class PaletteColor
+public partial class PaletteColor : IEquatable<PaletteColor>
 {
     public static PaletteColor Empty => new PaletteColor(0, 0, 0);
     public static PaletteColor Black => new PaletteColor(0, 0, 0);
@@ -79,4 +79,44 @@ public partial class PaletteColor
 
         return new PaletteColor(r, g, b);
     }
+
+    public bool Equals(PaletteColor other)
+    {
+        if (ReferenceEquals(null, other))
+        {
+            return false;
+        }
+
+        if (ReferenceEquals(this, other))
+        {
+            return true;
+        }
+
+        return RValue == other.RValue && GValue == other.GValue && BValue == other.BValue;
+    }
+
+    public override bool Equals(object obj)
+    {
+        if (ReferenceEquals(null, obj))
+        {
+            return false;
+        }
+
+        if (ReferenceEquals(this, obj))
+        {
+            return true;
+        }
+
+        if (obj.GetType() != this.GetType())
+        {
+            return false;
+        }
+
+        return Equals((PaletteColor)obj);
+    }
+
+    public override int GetHashCode()
+    {
+        return HashCode.Combine(RValue, GValue, BValue);
+    }
 }

+ 44 - 0
src/PixiEditor.Extensions.Sdk/Api/FlyUI/CheckBox.cs

@@ -0,0 +1,44 @@
+using PixiEditor.Extensions.CommonApi.FlyUI;
+using PixiEditor.Extensions.CommonApi.FlyUI.Events;
+
+namespace PixiEditor.Extensions.Sdk.Api.FlyUI;
+
+public class CheckBox : SingleChildLayoutElement
+{
+    public event ElementEventHandler CheckedChanged
+    {
+        add => AddEvent(nameof(CheckedChanged), value);
+        remove => RemoveEvent(nameof(CheckedChanged), value);
+    }
+
+    public bool IsChecked { get; set; }
+
+    public CheckBox(ILayoutElement<CompiledControl> child = null, ElementEventHandler onCheckedChanged = null)
+    {
+        Child = child;
+        
+        if (onCheckedChanged != null)
+        {
+            CheckedChanged += (args) =>
+            {
+                IsChecked = !IsChecked;
+                onCheckedChanged(args);
+            };
+        }
+        else
+        {
+            CheckedChanged += args => IsChecked = !IsChecked;
+        }
+    }
+
+
+    public override CompiledControl BuildNative()
+    {
+        CompiledControl checkbox = new CompiledControl(UniqueId, "CheckBox");
+        if (Child != null)
+            checkbox.AddChild(Child.BuildNative());
+
+        BuildPendingEvents(checkbox);
+        return checkbox;
+    }
+}

+ 31 - 0
src/PixiEditor.Extensions.Sdk/Api/FlyUI/LayoutElement.cs

@@ -37,6 +37,22 @@ public abstract class LayoutElement : ILayoutElement<CompiledControl>
         _events[eventName].Add(eventHandler);
         BuildQueuedEvents.Add(eventName);
     }
+    
+    /*public void AddEvent<TEventArgs>(string eventName, ElementEventHandler<TEventArgs> eventHandler) where TEventArgs : ElementEventArgs<TEventArgs>
+    {
+        if (_events == null)
+        {
+            _events = new Dictionary<string, List<ElementEventHandler>>();
+        }
+
+        if (!_events.ContainsKey(eventName))
+        {
+            _events.Add(eventName, new List<ElementEventHandler>());
+        }
+
+        _events[eventName].Add((args => eventHandler((TEventArgs)args))); 
+        BuildQueuedEvents.Add(eventName);
+    }*/
 
     public void RemoveEvent(string eventName, ElementEventHandler eventHandler)
     {
@@ -52,6 +68,21 @@ public abstract class LayoutElement : ILayoutElement<CompiledControl>
 
         _events[eventName].Remove(eventHandler);
     }
+    
+    /*public void RemoveEvent<TEventArgs>(string eventName, ElementEventHandler<TEventArgs> eventHandler) where TEventArgs : ElementEventArgs<TEventArgs>
+    {
+        if (_events == null)
+        {
+            return;
+        }
+
+        if (!_events.ContainsKey(eventName))
+        {
+            return;
+        }
+
+        _events[eventName].Remove((args => eventHandler((TEventArgs)args)));
+    }*/
 
     public void RaiseEvent(string eventName, ElementEventArgs args)
     {

+ 1 - 1
src/PixiEditor.Extensions.Sdk/Bridge/Native.cs

@@ -54,7 +54,7 @@ internal static partial class Native
     {
         if (LayoutElementsStore.LayoutElements.TryGetValue((int)internalControlId, out ILayoutElement<CompiledControl> element))
         {
-            element.RaiseEvent(eventName ?? "", new ElementEventArgs());
+            element.RaiseEvent(eventName ?? "", new ElementEventArgs { Sender = element });
         }
     }
 

+ 4 - 2
src/PixiEditor.Extensions/FlyUI/Converters/EnumToEnumConverter.cs

@@ -9,7 +9,8 @@ public class EnumToEnumConverter<T1, T2> : IValueConverter
     {
         if (value is T1 enumValue)
         {
-            return (T2)(object)enumValue;
+            int enumInt = (int)(object)enumValue;
+            return Enum.ToObject(typeof(T2), enumInt);
         }
 
         return null;
@@ -19,7 +20,8 @@ public class EnumToEnumConverter<T1, T2> : IValueConverter
     {
         if (value is T2 enumValue)
         {
-            return (T1)(object)enumValue;
+            int enumInt = (int)(object)enumValue;
+            return Enum.ToObject(typeof(T1), enumInt);
         }
 
         return null;

+ 14 - 3
src/PixiEditor.Extensions/FlyUI/Elements/Align.cs

@@ -8,6 +8,7 @@ namespace PixiEditor.Extensions.FlyUI.Elements;
 
 public class Align : SingleChildLayoutElement, IPropertyDeserializable
 {
+    private Panel _panel; 
     public Alignment Alignment { get; set; }
 
     public Align(LayoutElement child = null, Alignment alignment = Alignment.Center)
@@ -18,17 +19,27 @@ public class Align : SingleChildLayoutElement, IPropertyDeserializable
 
     public override Control BuildNative()
     {
-        Panel panel = new Panel
+        _panel = new Panel
         {
             HorizontalAlignment = DecomposeHorizontalAlignment(Alignment), VerticalAlignment = DecomposeVerticalAlignment(Alignment)
         };
 
         if (Child != null)
         {
-            panel.Children.Add(Child.BuildNative());
+            _panel.Children.Add(Child.BuildNative());
         }
 
-        return panel;
+        return _panel;
+    }
+
+    protected override void AddChild(Control child)
+    {
+        _panel.Children.Add(child);
+    }
+
+    protected override void RemoveChild()
+    {
+        _panel.Children.Clear(); 
     }
 
     private HorizontalAlignment DecomposeHorizontalAlignment(Alignment alignment)

+ 13 - 1
src/PixiEditor.Extensions/FlyUI/Elements/Border.cs

@@ -11,6 +11,8 @@ namespace PixiEditor.Extensions.FlyUI.Elements;
 
 public class Border : SingleChildLayoutElement, IPropertyDeserializable
 {
+    private Avalonia.Controls.Border border;
+    
     private Edges _thickness;
     private Color _color;
     private Edges cornerRadius;
@@ -25,7 +27,7 @@ public class Border : SingleChildLayoutElement, IPropertyDeserializable
     
     public override Control BuildNative()
     {
-        Avalonia.Controls.Border border = new Avalonia.Controls.Border();
+        border = new Avalonia.Controls.Border();
         
         border.ClipToBounds = true;
         
@@ -78,6 +80,16 @@ public class Border : SingleChildLayoutElement, IPropertyDeserializable
         return border;
     }
 
+    protected override void AddChild(Control child)
+    {
+        border.Child = child;
+    }
+
+    protected override void RemoveChild()
+    {
+        border.Child = null;
+    }
+
     public IEnumerable<object> GetProperties()
     {
         yield return Color;

+ 15 - 4
src/PixiEditor.Extensions/FlyUI/Elements/Button.cs

@@ -7,6 +7,7 @@ namespace PixiEditor.Extensions.FlyUI.Elements;
 
 public class Button : SingleChildLayoutElement
 {
+    private Avalonia.Controls.Button _button;
     public event ElementEventHandler Click
     {
         add => AddEvent(nameof(Click), value);
@@ -29,12 +30,22 @@ public class Button : SingleChildLayoutElement
 
     public override Control BuildNative()
     {
-        Avalonia.Controls.Button btn = new Avalonia.Controls.Button();
+        _button = new Avalonia.Controls.Button();
         Binding binding = new Binding(nameof(Child)) { Source = this, Converter = LayoutElementToNativeControlConverter.Instance };
-        btn.Bind(Avalonia.Controls.Button.ContentProperty, binding);
+        _button.Bind(Avalonia.Controls.Button.ContentProperty, binding);
 
-        btn.Click += (sender, args) => RaiseEvent(nameof(Click), new ElementEventArgs());
+        _button.Click += (sender, args) => RaiseEvent(nameof(Click), new ElementEventArgs() { Sender = this });
 
-        return btn;
+        return _button;
+    }
+
+    protected override void AddChild(Control child)
+    {
+        _button.Content = child;
+    }
+
+    protected override void RemoveChild()
+    {
+        _button.Content = null;
     }
 }

+ 10 - 18
src/PixiEditor.Extensions/FlyUI/Elements/Center.cs

@@ -14,24 +14,6 @@ public class Center : SingleChildLayoutElement
     public Center(LayoutElement child = null)
     {
         Child = child;
-        PropertyChanged += OnPropertyChanged;
-    }
-
-    private void OnPropertyChanged(object? sender, PropertyChangedEventArgs e)
-    {
-        if(panel == null)
-        {
-            return;
-        }
-
-        if (e.PropertyName == nameof(Child))
-        {
-            panel.Children.Clear();
-            if (Child != null)
-            {
-                panel.Children.Add(Child.BuildNative());
-            }
-        }
     }
 
     public override Control BuildNative()
@@ -50,4 +32,14 @@ public class Center : SingleChildLayoutElement
 
         return panel;
     }
+
+    protected override void AddChild(Control child)
+    {
+        panel.Children.Add(child);
+    }
+
+    protected override void RemoveChild()
+    {
+        panel.Children.Clear();
+    }
 }

+ 39 - 0
src/PixiEditor.Extensions/FlyUI/Elements/CheckBox.cs

@@ -0,0 +1,39 @@
+using Avalonia.Controls;
+using Avalonia.Data;
+using PixiEditor.Extensions.CommonApi.FlyUI.Events;
+
+namespace PixiEditor.Extensions.FlyUI.Elements;
+
+public class CheckBox : SingleChildLayoutElement
+{
+    private Avalonia.Controls.CheckBox checkbox;    
+    public event ElementEventHandler<ToggleEventArgs> CheckedChanged
+    {
+        add => AddEvent(nameof(CheckedChanged), value);
+        remove => RemoveEvent(nameof(CheckedChanged), value);
+    }
+
+    public override Control BuildNative()
+    {
+        checkbox = new Avalonia.Controls.CheckBox();
+        Binding binding =
+            new Binding(nameof(Child)) { Source = this, Converter = LayoutElementToNativeControlConverter.Instance };
+        checkbox.Bind(ContentControl.ContentProperty, binding);
+
+        checkbox.IsCheckedChanged += (sender, args) => RaiseEvent(
+            nameof(CheckedChanged),
+            new ToggleEventArgs((sender as Avalonia.Controls.CheckBox).IsChecked.Value) { Sender = this });
+
+        return checkbox;
+    }
+
+    protected override void AddChild(Control child)
+    {
+        checkbox.Content = child;
+    }
+
+    protected override void RemoveChild()
+    {
+        checkbox.Content = null;
+    }
+}

+ 22 - 17
src/PixiEditor.Extensions/FlyUI/Elements/Container.cs

@@ -11,6 +11,7 @@ namespace PixiEditor.Extensions.FlyUI.Elements;
 
 public class Container : SingleChildLayoutElement, IPropertyDeserializable
 {
+    private Panel _panel; 
     private Edges _margin = Edges.All(0);
     private Color _backgroundColor = Colors.Transparent;
     private double _width = double.NaN;
@@ -23,13 +24,13 @@ public class Container : SingleChildLayoutElement, IPropertyDeserializable
     
     public override Control BuildNative()
     {
-        Panel panel = new Panel();
+        _panel = new Panel();
         
-        panel.ClipToBounds = true;
+        _panel.ClipToBounds = true;
         
         if(Child != null)
         {
-            panel.Children.Add(Child.BuildNative());
+            _panel.Children.Add(Child.BuildNative());
         }
         
         Binding marginBinding = new()
@@ -58,25 +59,29 @@ public class Container : SingleChildLayoutElement, IPropertyDeserializable
             Path = nameof(Height),
         };
         
-        panel.Bind(Layoutable.MarginProperty, marginBinding);
-        panel.Bind(Panel.BackgroundProperty, backgroundColorBinding);
-        panel.Bind(Layoutable.WidthProperty, widthBinding);
-        panel.Bind(Layoutable.HeightProperty, heightBinding);
+        _panel.Bind(Layoutable.MarginProperty, marginBinding);
+        _panel.Bind(Panel.BackgroundProperty, backgroundColorBinding);
+        _panel.Bind(Layoutable.WidthProperty, widthBinding);
+        _panel.Bind(Layoutable.HeightProperty, heightBinding);
         
-        return panel;
+        return _panel;
+    }
+
+    protected override void AddChild(Control child)
+    {
+        _panel.Children.Add(child);
+    }
+
+    protected override void RemoveChild()
+    {
+        _panel.Children.Clear();
     }
 
     public IEnumerable<object> GetProperties()
     {
-        yield return Margin.Left;
-        yield return Margin.Top;
-        yield return Margin.Right;
-        yield return Margin.Bottom;
-        
-        yield return BackgroundColor.R;
-        yield return BackgroundColor.G;
-        yield return BackgroundColor.B;
-        yield return BackgroundColor.A;
+        yield return Margin;
+
+        yield return BackgroundColor;
         
         yield return Width;
         yield return Height;

+ 12 - 1
src/PixiEditor.Extensions/FlyUI/Elements/Layout.cs

@@ -4,6 +4,7 @@ namespace PixiEditor.Extensions.FlyUI.Elements;
 
 public class Layout : SingleChildLayoutElement
 {
+    private Panel panel;
     public Layout()
     {
 
@@ -16,7 +17,7 @@ public class Layout : SingleChildLayoutElement
 
     public override Control BuildNative()
     {
-        Panel panel = new Panel();
+        panel = new Panel();
         if (Child != null)
         {
             panel.Children.Add(Child.BuildNative());
@@ -24,4 +25,14 @@ public class Layout : SingleChildLayoutElement
 
         return panel;
     }
+
+    protected override void AddChild(Control child)
+    {
+        panel.Children.Add(child);
+    }
+
+    protected override void RemoveChild()
+    {
+        panel.Children.Clear();
+    }
 }

+ 30 - 0
src/PixiEditor.Extensions/FlyUI/Elements/LayoutElement.cs

@@ -27,6 +27,21 @@ public abstract class LayoutElement : ILayoutElement<Control>, INotifyPropertyCh
 
         _events[eventName].Add(eventHandler);
     }
+    
+    public void AddEvent<T>(string eventName, ElementEventHandler<T> eventHandler) where T : ElementEventArgs<T>
+    {
+        if (_events == null)
+        {
+            _events = new Dictionary<string, List<ElementEventHandler>>();
+        }
+
+        if (!_events.ContainsKey(eventName))
+        {
+            _events.Add(eventName, new List<ElementEventHandler>());
+        }
+
+        _events[eventName].Add((args => eventHandler((T)args)));
+    }
 
     public void RemoveEvent(string eventName, ElementEventHandler eventHandler)
     {
@@ -42,6 +57,21 @@ public abstract class LayoutElement : ILayoutElement<Control>, INotifyPropertyCh
 
         _events[eventName].Remove(eventHandler);
     }
+    
+    public void RemoveEvent<T>(string eventName, ElementEventHandler<T> eventHandler) where T : ElementEventArgs<T>
+    {
+        if (_events == null)
+        {
+            return;
+        }
+
+        if (!_events.ContainsKey(eventName))
+        {
+            return;
+        }
+
+        _events[eventName].Remove((args => eventHandler((T)args)));
+    }
 
     public void RaiseEvent(string eventName, ElementEventArgs args)
     {

+ 5 - 0
src/PixiEditor.Extensions/FlyUI/Elements/MultiChildLayoutElement.cs

@@ -25,6 +25,8 @@ public abstract class MultiChildLayoutElement : LayoutElement, IMultiChildLayout
     }
 
     public abstract override Control BuildNative();
+    /*public abstract void AddChild(Control child);
+    public abstract void RemoveChild(int atIndex);*/
 
     public IEnumerator<ILayoutElement<Control>> GetEnumerator()
     {
@@ -44,10 +46,13 @@ public abstract class MultiChildLayoutElement : LayoutElement, IMultiChildLayout
     public void AddChild(ILayoutElement<Control> child)
     {
         Children.Add((LayoutElement)child);
+        //AddChild(child.BuildNative());
     }
 
     public void RemoveChild(ILayoutElement<Control> child)
     {
+        int index = Children.IndexOf((LayoutElement)child);
         Children.Remove((LayoutElement)child);
+        //RemoveChild(index);   
     }
 }

+ 15 - 4
src/PixiEditor.Extensions/FlyUI/Elements/Padding.cs

@@ -10,16 +10,17 @@ namespace PixiEditor.Extensions.FlyUI.Elements;
 
 public class Padding : SingleChildLayoutElement, IPropertyDeserializable
 {
+    private Decorator _decorator;
     private Edges _edges = Edges.All(0);
     
     public Edges Edges { get => _edges; set => SetField(ref _edges, value); }
     public override Control BuildNative()
     {
-        Decorator decorator = new();
+        _decorator = new();
         
         if(Child != null)
         {
-            decorator.Child = Child.BuildNative();
+            _decorator.Child = Child.BuildNative();
         }
         
         Binding edgesBinding = new()
@@ -29,9 +30,19 @@ public class Padding : SingleChildLayoutElement, IPropertyDeserializable
             Converter = new EdgesToThicknessConverter(),
         };
         
-        decorator.Bind(Decorator.PaddingProperty, edgesBinding);
+        _decorator.Bind(Decorator.PaddingProperty, edgesBinding);
         
-        return decorator;
+        return _decorator;
+    }
+
+    protected override void AddChild(Control child)
+    {
+        _decorator.Child = child;
+    }
+
+    protected override void RemoveChild()
+    {
+        _decorator.Child = null;
     }
 
     public IEnumerable<object> GetProperties()

+ 8 - 1
src/PixiEditor.Extensions/FlyUI/Elements/SingleChildLayoutElement.cs

@@ -17,10 +17,15 @@ public abstract class SingleChildLayoutElement : LayoutElement, ISingleChildLayo
     public LayoutElement Child
     {
         get => _child;
-        set => SetField(ref _child, value);
+        set
+        {
+            SetField(ref _child, value);
+        }
     }
 
     public abstract override Control BuildNative();
+    protected abstract void AddChild(Control child);
+    protected abstract void RemoveChild();
 
     void IChildHost.DeserializeChildren(List<ILayoutElement<Control>> children)
     {
@@ -30,11 +35,13 @@ public abstract class SingleChildLayoutElement : LayoutElement, ISingleChildLayo
     public void AddChild(ILayoutElement<Control> child)
     {
         Child = (LayoutElement)child;
+        AddChild(child.BuildNative());
     }
 
     public void RemoveChild(ILayoutElement<Control> child)
     {
         Child = null;
+        RemoveChild();
     }
 
     public IEnumerator<ILayoutElement<Control>> GetEnumerator()

+ 27 - 30
src/PixiEditor.Numerics/ColorMatrix.cs

@@ -242,6 +242,33 @@ public record struct ColorMatrix
         return buffer;
     }
 
+    public ColorMatrix(float[] values)
+    {
+        if (values.Length != 20)
+            throw new ArgumentException("Array must have 20 elements", nameof(values));
+        M11 = values[0];
+        M12 = values[1];
+        M13 = values[2];
+        M14 = values[3];
+        M15 = values[4];
+        M21 = values[5];
+        M22 = values[6];
+        M23 = values[7];
+        M24 = values[8];
+        M25 = values[9];
+        M31 = values[10];
+        M32 = values[11];
+        M33 = values[12];
+        M34 = values[13];
+        M35 = values[14];
+        M41 = values[15];
+        M42 = values[16];
+        M43 = values[17];
+        M44 = values[18];
+        M45 = values[19];
+    }
+
+    [System.Runtime.CompilerServices.CompilerGenerated]
     public bool TryGetMembers(Span<float> members)
     {
         if (members.Length < 20)
@@ -269,36 +296,6 @@ public record struct ColorMatrix
         return true;
     }
 
-    public static ColorMatrix CreateFromMembers(ReadOnlySpan<float> members)
-    {
-        if (members.Length < 20)
-            throw new IndexOutOfRangeException($"{nameof(members)} must have at least 20 elements. Actual length was {members.Length}");
-
-        var m11 = members[0];
-        var m12 = members[1];
-        var m13 = members[2];
-        var m14 = members[3];
-        var m15 = members[4];
-        var m21 = members[5];
-        var m22 = members[6];
-        var m23 = members[7];
-        var m24 = members[8];
-        var m25 = members[9];
-        var m31 = members[10];
-        var m32 = members[11];
-        var m33 = members[12];
-        var m34 = members[13];
-        var m35 = members[14];
-        var m41 = members[15];
-        var m42 = members[16];
-        var m43 = members[17];
-        var m44 = members[18];
-        var m45 = members[19];
-
-        return new ColorMatrix(m11, m12, m13, m14, m15, m21, m22, m23, m24, m25, m31, m32, m33, m34, m35, m41, m42, m43,
-            m44, m45);
-    }
-
     public bool TryGetRow(int row, Span<float> members)
     {
         if (row < 0 || row >= 4)

+ 4 - 4
src/PixiEditor.UI.Common/Fonts/PixiPerfectIcons.axaml

@@ -99,11 +99,11 @@
             <system:String x:Key="icon-trash">&#xE958;</system:String>
             <system:String x:Key="icon-undo">&#xE959;</system:String>
             <system:String x:Key="icon-unlock">&#xE95A;</system:String>
-            <system:String x:Key="icon-y-flip">&#xE95B;</system:String>
-            <system:String x:Key="icon-x-selected-flip">&#xE95C;</system:String>
+            <system:String x:Key="icon-x-flip">&#xE95B;</system:String>
+            <system:String x:Key="icon-x-selected-flip">&#xE95F;</system:String>
+            <system:String x:Key="icon-y-flip">&#xE95E;</system:String>
+            <system:String x:Key="icon-y-selected-flip">&#xE95C;</system:String>
             <system:String x:Key="icon-x-symmetry">&#xE95D;</system:String>
-            <system:String x:Key="icon-x-flip">&#xE95E;</system:String>
-            <system:String x:Key="icon-y-selected-flip">&#xE95F;</system:String>
             <system:String x:Key="icon-y-symmetry">&#xE960;</system:String>
             <system:String x:Key="icon-zoom-in">&#xE961;</system:String>
             <system:String x:Key="icon-zoom-out">&#xE962;</system:String>

+ 4 - 4
src/PixiEditor.UI.Common/Fonts/PixiPerfectIcons.axaml.cs

@@ -102,11 +102,11 @@ public static class PixiPerfectIcons
     public const string Trash = "\uE958";
     public const string Undo = "\uE959";
     public const string Unlock = "\uE95A";
-    public const string XFlip = "\uE95B";
-    public const string XSelectedFlip = "\uE95C";
+    public const string XFlip = "\uE95E";
+    public const string XSelectedFlip = "\uE95F";
     public const string XSymmetry = "\uE95D";
-    public const string YFlip = "\uE95E";
-    public const string YSelectedFlip = "\uE95F";
+    public const string YFlip = "\uE95B";
+    public const string YSelectedFlip = "\uE95C";
     public const string YSymmetry = "\uE960";
     public const string ZoomIn = "\uE961";
     public const string ZoomOut = "\uE962";

+ 33 - 0
src/PixiEditor.sln

@@ -108,6 +108,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiParser.Skia", "..\..\Pi
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiEditor.Common", "PixiEditor.Common\PixiEditor.Common.csproj", "{E92E7B0E-C24B-4087-9DD9-AD10DA3BE80A}"
 EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BuiltInExtensions", "BuiltInExtensions", "{99903753-40A6-434E-8B8F-CE55417CF7FC}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiEditor.ClosedBeta", "PixiEditor.ClosedBeta\PixiEditor.ClosedBeta.csproj", "{059904E2-9ACC-41E6-B78D-F0BC03CA9D78}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|x64 = Debug|x64
@@ -1590,6 +1594,34 @@ Global
 		{E92E7B0E-C24B-4087-9DD9-AD10DA3BE80A}.Steam|x64.Build.0 = Debug|Any CPU
 		{E92E7B0E-C24B-4087-9DD9-AD10DA3BE80A}.Steam|ARM64.ActiveCfg = Debug|Any CPU
 		{E92E7B0E-C24B-4087-9DD9-AD10DA3BE80A}.Steam|ARM64.Build.0 = Debug|Any CPU
+		{059904E2-9ACC-41E6-B78D-F0BC03CA9D78}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{059904E2-9ACC-41E6-B78D-F0BC03CA9D78}.Debug|x64.Build.0 = Debug|Any CPU
+		{059904E2-9ACC-41E6-B78D-F0BC03CA9D78}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+		{059904E2-9ACC-41E6-B78D-F0BC03CA9D78}.Debug|ARM64.Build.0 = Debug|Any CPU
+		{059904E2-9ACC-41E6-B78D-F0BC03CA9D78}.DevRelease|x64.ActiveCfg = Debug|Any CPU
+		{059904E2-9ACC-41E6-B78D-F0BC03CA9D78}.DevRelease|x64.Build.0 = Debug|Any CPU
+		{059904E2-9ACC-41E6-B78D-F0BC03CA9D78}.DevRelease|ARM64.ActiveCfg = Debug|Any CPU
+		{059904E2-9ACC-41E6-B78D-F0BC03CA9D78}.DevRelease|ARM64.Build.0 = Debug|Any CPU
+		{059904E2-9ACC-41E6-B78D-F0BC03CA9D78}.DevSteam|x64.ActiveCfg = Debug|Any CPU
+		{059904E2-9ACC-41E6-B78D-F0BC03CA9D78}.DevSteam|x64.Build.0 = Debug|Any CPU
+		{059904E2-9ACC-41E6-B78D-F0BC03CA9D78}.DevSteam|ARM64.ActiveCfg = Debug|Any CPU
+		{059904E2-9ACC-41E6-B78D-F0BC03CA9D78}.DevSteam|ARM64.Build.0 = Debug|Any CPU
+		{059904E2-9ACC-41E6-B78D-F0BC03CA9D78}.MSIX Debug|x64.ActiveCfg = Debug|Any CPU
+		{059904E2-9ACC-41E6-B78D-F0BC03CA9D78}.MSIX Debug|x64.Build.0 = Debug|Any CPU
+		{059904E2-9ACC-41E6-B78D-F0BC03CA9D78}.MSIX Debug|ARM64.ActiveCfg = Debug|Any CPU
+		{059904E2-9ACC-41E6-B78D-F0BC03CA9D78}.MSIX Debug|ARM64.Build.0 = Debug|Any CPU
+		{059904E2-9ACC-41E6-B78D-F0BC03CA9D78}.MSIX|x64.ActiveCfg = Debug|Any CPU
+		{059904E2-9ACC-41E6-B78D-F0BC03CA9D78}.MSIX|x64.Build.0 = Debug|Any CPU
+		{059904E2-9ACC-41E6-B78D-F0BC03CA9D78}.MSIX|ARM64.ActiveCfg = Debug|Any CPU
+		{059904E2-9ACC-41E6-B78D-F0BC03CA9D78}.MSIX|ARM64.Build.0 = Debug|Any CPU
+		{059904E2-9ACC-41E6-B78D-F0BC03CA9D78}.Release|x64.ActiveCfg = Release|Any CPU
+		{059904E2-9ACC-41E6-B78D-F0BC03CA9D78}.Release|x64.Build.0 = Release|Any CPU
+		{059904E2-9ACC-41E6-B78D-F0BC03CA9D78}.Release|ARM64.ActiveCfg = Release|Any CPU
+		{059904E2-9ACC-41E6-B78D-F0BC03CA9D78}.Release|ARM64.Build.0 = Release|Any CPU
+		{059904E2-9ACC-41E6-B78D-F0BC03CA9D78}.Steam|x64.ActiveCfg = Debug|Any CPU
+		{059904E2-9ACC-41E6-B78D-F0BC03CA9D78}.Steam|x64.Build.0 = Debug|Any CPU
+		{059904E2-9ACC-41E6-B78D-F0BC03CA9D78}.Steam|ARM64.ActiveCfg = Debug|Any CPU
+		{059904E2-9ACC-41E6-B78D-F0BC03CA9D78}.Steam|ARM64.Build.0 = Debug|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -1637,6 +1669,7 @@ Global
 		{9B552A44-9587-4410-8673-254B31E2E4F7} = {2BA72059-FFD7-4887-AE88-269017198933}
 		{CD863C88-72E3-40F4-9AAE-5696BBB4460C} = {2BA72059-FFD7-4887-AE88-269017198933}
 		{E92E7B0E-C24B-4087-9DD9-AD10DA3BE80A} = {1E816135-76C1-4255-BE3C-BF17895A65AA}
+		{059904E2-9ACC-41E6-B78D-F0BC03CA9D78} = {99903753-40A6-434E-8B8F-CE55417CF7FC}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {D04B4AB0-CA33-42FD-A909-79966F9255C5}