Browse Source

Merge pull request #698 from PixiEditor/fixes/12.12.2024

Fixes/12.12.2024
Krzysztof Krysiński 8 months ago
parent
commit
5103ccebe1
34 changed files with 265 additions and 120 deletions
  1. 6 4
      src/ChunkyImageLib/ChunkyImage.cs
  2. 1 1
      src/Drawie
  3. 16 10
      src/PixiEditor.ChangeableDocument/Changeables/Document.cs
  4. 1 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Factories/ImageLayerNodeFactory.cs
  5. 5 3
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageLayerNode.cs
  6. 1 0
      src/PixiEditor.ChangeableDocument/Changeables/Interfaces/IReadOnlyDocument.cs
  7. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Animation/CreateRasterKeyFrame_Change.cs
  8. 2 2
      src/PixiEditor.ChangeableDocument/Changes/Drawing/ApplyLayerMask_Change.cs
  9. 14 5
      src/PixiEditor.ChangeableDocument/Changes/Drawing/LineBasedPen_UpdateableChange.cs
  10. 33 5
      src/PixiEditor.ChangeableDocument/Changes/Properties/ChangeProcessingColorSpace_Change.cs
  11. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Properties/LayerStructure/CreateStructureMemberMask_Change.cs
  12. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Structure/RasterizeMember_Change.cs
  13. BIN
      src/PixiEditor.Extensions.Sdk/build/PixiEditor.Api.CGlueMSBuild.dll
  14. 1 0
      src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller/ViewModels/UpdateController.cs
  15. 4 1
      src/PixiEditor.UpdateModule/UpdateInstaller.cs
  16. 9 3
      src/PixiEditor.Windows/WindowsProcessUtility.cs
  17. 12 4
      src/PixiEditor/Helpers/DocumentViewModelBuilder.cs
  18. 1 0
      src/PixiEditor/Helpers/Extensions/PixiParserDocumentEx.cs
  19. 5 3
      src/PixiEditor/Helpers/Extensions/PixiParserPixiV4DocumentEx.cs
  20. 42 30
      src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs
  21. 1 1
      src/PixiEditor/Models/Serialization/Factories/ChunkyImageSerializationFactory.cs
  22. 5 2
      src/PixiEditor/Models/Serialization/SerializationConfig.cs
  23. 1 1
      src/PixiEditor/ViewModels/Document/DocumentViewModel.Serialization.cs
  24. 18 7
      src/PixiEditor/ViewModels/Document/DocumentViewModel.cs
  25. 10 3
      src/PixiEditor/ViewModels/SubViewModels/FileViewModel.cs
  26. 0 1
      src/PixiEditor/Views/Main/Tools/Toolbar.axaml
  27. 19 9
      src/PixiEditor/Views/Main/Tools/ToolsPicker.axaml
  28. 12 3
      src/PixiEditor/Views/Main/Tools/ToolsPicker.axaml.cs
  29. 28 9
      src/PixiEditor/Views/Main/ViewportControls/Viewport.axaml
  30. 2 2
      src/PixiEditor/Views/Rendering/Scene.cs
  31. 1 0
      src/PixiEditor/Views/Windows/BetaExampleButton.axaml.cs
  32. 5 5
      tests/ChunkyImageLibTest/ChunkyImageTests.cs
  33. 4 0
      tests/PixiEditor.Backend.Tests/MockDocument.cs
  34. 3 2
      tests/PixiEditor.Backend.Tests/NodeSystemTests.cs

+ 6 - 4
src/ChunkyImageLib/ChunkyImage.cs

@@ -75,7 +75,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
     private static Paint AddingPaint { get; } = new Paint() { BlendMode = BlendMode.Plus };
     private readonly Paint blendModePaint = new Paint() { BlendMode = BlendMode.Src };
 
-    public ColorSpace ProcessingColorSpace { get; set; } = ColorSpace.CreateSrgbLinear();
+    public ColorSpace ProcessingColorSpace { get; set; }
     
     public int CommitCounter => commitCounter;
 
@@ -105,7 +105,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
     private readonly Dictionary<ChunkResolution, Dictionary<VecI, Chunk>> latestChunks;
     private readonly Dictionary<ChunkResolution, Dictionary<VecI, LatestChunkData>> latestChunksData;
 
-    public ChunkyImage(VecI size)
+    public ChunkyImage(VecI size, ColorSpace colorSpace)
     {
         CommittedSize = size;
         LatestSize = size;
@@ -130,9 +130,11 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
             [ChunkResolution.Quarter] = new(),
             [ChunkResolution.Eighth] = new(),
         };
+        
+        ProcessingColorSpace = colorSpace;
     }
 
-    public ChunkyImage(Surface image) : this(image.Size)
+    public ChunkyImage(Surface image, ColorSpace colorSpace) : this(image.Size, colorSpace)
     {
         EnqueueDrawImage(VecI.Zero, image);
         CommitChanges();
@@ -252,7 +254,7 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable, ICloneable, ICache
         lock (lockObject)
         {
             ThrowIfDisposed();
-            ChunkyImage output = new(LatestSize);
+            ChunkyImage output = new(LatestSize, ProcessingColorSpace);
             var chunks = FindCommittedChunks();
             foreach (var chunk in chunks)
             {

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit bfb22ae2b5f17e0f750c8106ec4677614c697428
+Subproject commit 2ba04a8018897b11fa764943ce3c368b7eda4bc9

+ 16 - 10
src/PixiEditor.ChangeableDocument/Changeables/Document.cs

@@ -90,13 +90,13 @@ internal class Document : IChangeable, IReadOnlyDocument
         using var paint = new Paint();
 
         Surface image;
-        
+
         if (layer is IReadOnlyImageNode imageNode)
         {
             var chunkyImage = imageNode.GetLayerImageAtFrame(frame);
             using Surface chunkSurface = new Surface(chunkyImage.CommittedSize);
             chunkyImage.DrawCommittedRegionOn(
-                new RectI(0, 0, chunkyImage.CommittedSize.X, chunkyImage.CommittedSize.Y), 
+                new RectI(0, 0, chunkyImage.CommittedSize.X, chunkyImage.CommittedSize.Y),
                 ChunkResolution.Full,
                 chunkSurface.DrawingSurface,
                 VecI.Zero);
@@ -109,7 +109,7 @@ internal class Document : IChangeable, IReadOnlyDocument
             /*TODO: this*/
             // image = new Surface(layer.Execute(new RenderingContext(frame, Size)));
         }
-        
+
         //todo: idk if it's correct
         surface.DrawingSurface.Canvas.DrawSurface(image.DrawingSurface, 0, 0, paint);
 
@@ -139,6 +139,11 @@ internal class Document : IChangeable, IReadOnlyDocument
     /// </summary>
     public void ForEveryMember(Action<StructureNode> action) => ForEveryMember(NodeGraph, action);
 
+    public void InitProcessingColorSpace(ColorSpace processingColorSpace)
+    {
+        ProcessingColorSpace = processingColorSpace;
+    }
+
     private void ForEveryReadonlyMember(IReadOnlyNodeGraph graph, Action<IReadOnlyStructureNode> action)
     {
         graph.TryTraverse((node) =>
@@ -174,7 +179,7 @@ internal class Document : IChangeable, IReadOnlyDocument
     {
         return NodeGraph.Nodes.Any(x => x.Id == id);
     }
-    
+
     /// <summary>
     ///     Checks if a node in NodeGraph with the given <paramref name="id"/> exists and is of type <typeparamref name="T"/>.
     /// </summary>
@@ -193,7 +198,7 @@ internal class Document : IChangeable, IReadOnlyDocument
     /// <returns>True if the member can be found, otherwise false</returns>
     public bool HasMember(Guid guid)
     {
-        return HasNode<StructureNode>(guid); 
+        return HasNode<StructureNode>(guid);
     }
 
     /// <summary>
@@ -248,7 +253,7 @@ internal class Document : IChangeable, IReadOnlyDocument
     {
         return NodeGraph.Nodes.FirstOrDefault(x => x.Id == guid);
     }
-    
+
     IReadOnlyNode IReadOnlyDocument.FindNode(Guid guid) => FindNodeOrThrow<Node>(guid);
 
     public T? FindNode<T>(Guid guid) where T : Node
@@ -278,7 +283,7 @@ internal class Document : IChangeable, IReadOnlyDocument
     public bool TryFindMember(Guid guid, [NotNullWhen(true)] out StructureNode? member)
     {
         member = FindMember(guid);
-        return member != null; 
+        return member != null;
     }
 
     /// <summary>
@@ -342,13 +347,13 @@ internal class Document : IChangeable, IReadOnlyDocument
         if (NodeGraph.OutputNode == null) return [];
 
         var list = new List<Node>();
-        
+
         var targetNode = FindNode(guid);
         if (targetNode == null)
         {
             return [];
         }
-        
+
         FillNodePath(targetNode, list);
         return list;
     }
@@ -367,6 +372,7 @@ internal class Document : IChangeable, IReadOnlyDocument
         {
             return [];
         }
+
         FillNodePath<StructureNode>(targetNode, list);
         return list.ToList();
     }
@@ -382,7 +388,7 @@ internal class Document : IChangeable, IReadOnlyDocument
 
             return true;
         });
-        
+
         return true;
     }
 

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Factories/ImageLayerNodeFactory.cs

@@ -7,6 +7,6 @@ public class ImageLayerNodeFactory : NodeFactory<ImageLayerNode>
 {
     public override ImageLayerNode CreateNode(IReadOnlyDocument document)
     {
-        return new ImageLayerNode(document.Size);
+        return new ImageLayerNode(document.Size, document.ProcessingColorSpace);
     }
 }

+ 5 - 3
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageLayerNode.cs

@@ -24,19 +24,21 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
     public bool LockTransparency { get; set; }
 
     private VecI startSize;
+    private ColorSpace colorSpace;
     private ChunkyImage layerImage => keyFrames[0]?.Data as ChunkyImage;
 
     private Texture fullResrenderedSurface;
     private int renderedSurfaceFrame = -1;
 
-    public ImageLayerNode(VecI size)
+    public ImageLayerNode(VecI size, ColorSpace colorSpace)
     {
         if (keyFrames.Count == 0)
         {
-            keyFrames.Add(new KeyFrameData(Guid.NewGuid(), 0, 0, ImageLayerKey) { Data = new ChunkyImage(size) });
+            keyFrames.Add(new KeyFrameData(Guid.NewGuid(), 0, 0, ImageLayerKey) { Data = new ChunkyImage(size, colorSpace) });
         }
 
         this.startSize = size;
+        this.colorSpace = colorSpace;
     }
 
     public override RectD? GetTightBounds(KeyFrameTime frameTime)
@@ -213,7 +215,7 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
 
     public override Node CreateCopy()
     {
-        var image = new ImageLayerNode(startSize) { MemberName = this.MemberName, };
+        var image = new ImageLayerNode(startSize, colorSpace) { MemberName = this.MemberName, };
 
         image.keyFrames.Clear();
 

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

@@ -103,4 +103,5 @@ public interface IReadOnlyDocument : IDisposable
     IReadOnlyReferenceLayer? ReferenceLayer { get; }
     public DocumentRenderer Renderer { get; }
     public ColorSpace ProcessingColorSpace { get; }
+    public void InitProcessingColorSpace(ColorSpace processingColorSpace);
 }

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

@@ -39,7 +39,7 @@ internal class CreateRasterKeyFrame_Change : Change
 
         ImageLayerNode targetNode = target.FindMemberOrThrow<ImageLayerNode>(_targetLayerGuid);
 
-        ChunkyImage img = cloneFromImage?.CloneFromCommitted() ?? new ChunkyImage(target.Size);
+        ChunkyImage img = cloneFromImage?.CloneFromCommitted() ?? new ChunkyImage(target.Size, target.ProcessingColorSpace);
 
         var keyFrame =
             new RasterKeyFrame(createdKeyFrameId, targetNode.Id, _frame, target);

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

@@ -37,7 +37,7 @@ internal class ApplyLayerMask_Change : Change
             throw new InvalidOperationException("Cannot apply layer mask, no mask");
 
         var layerImage = layer.GetLayerImageAtFrame(frame);
-        ChunkyImage newLayerImage = new ChunkyImage(target.Size);
+        ChunkyImage newLayerImage = new ChunkyImage(target.Size, target.ProcessingColorSpace);
         newLayerImage.AddRasterClip(layer.EmbeddedMask);
         newLayerImage.EnqueueDrawCommitedChunkyImage(VecI.Zero, layerImage);
         newLayerImage.CommitChanges();
@@ -68,7 +68,7 @@ internal class ApplyLayerMask_Change : Change
         if (savedLayer is null || savedMask is null)
             throw new InvalidOperationException("Cannot restore layer mask, no saved data");
 
-        ChunkyImage newMask = new ChunkyImage(target.Size);
+        ChunkyImage newMask = new ChunkyImage(target.Size, target.ProcessingColorSpace);
         savedMask.ApplyChunksToImage(newMask);
         var affectedChunksMask = newMask.FindAffectedArea();
         newMask.CommitChanges();

+ 14 - 5
src/PixiEditor.ChangeableDocument/Changes/Drawing/LineBasedPen_UpdateableChange.cs

@@ -42,6 +42,10 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
         this.spacing = spacing;
         points.Add(pos);
         this.frame = frame;
+        
+        srcPaint.Shader?.Dispose();
+        srcPaint.Shader = null;
+        
         if (this.antiAliasing && !erasing)
         {
             srcPaint.BlendMode = BlendMode.SrcOver;
@@ -82,7 +86,7 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
         int opCount = image.QueueLength;
 
         var bresenham = BresenhamLineHelper.GetBresenhamLine(from, to);
-        
+
         float spacingPixels = strokeWidth * spacing;
 
         foreach (var point in bresenham)
@@ -96,7 +100,7 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
             {
                 ApplySoftnessGradient((VecD)point);
             }
-            
+
             image.EnqueueDrawEllipse((RectD)rect, color, color, 0, 0, antiAliasing, srcPaint);
         }
 
@@ -110,12 +114,17 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
         if (points.Count == 1)
         {
             var rect = new RectI(points[0] - new VecI((int)(strokeWidth / 2f)), new VecI((int)strokeWidth));
-            targetImage.EnqueueDrawEllipse((RectD)rect, color, color, 1, 0, antiAliasing, srcPaint);
+            if (antiAliasing)
+            {
+                ApplySoftnessGradient(points[0]);
+            }
+
+            targetImage.EnqueueDrawEllipse((RectD)rect, color, color, 0, 0, antiAliasing, srcPaint);
             return;
         }
 
         VecF lastPos = points[0];
-        
+
         float spacingInPixels = strokeWidth * this.spacing;
 
         for (int i = 0; i < points.Count; i++)
@@ -142,7 +151,7 @@ internal class LineBasedPen_UpdateableChange : UpdateableChange
         radius = MathF.Max(1, radius);
         srcPaint.Shader = Shader.CreateRadialGradient(
             pos, radius, [color, color.WithAlpha(0)],
-            [hardness - 0.04f, 1f], ShaderTileMode.Clamp);
+            [Math.Max(hardness - 0.05f, 0), 0.95f], ShaderTileMode.Clamp);
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,

+ 33 - 5
src/PixiEditor.ChangeableDocument/Changes/Properties/ChangeProcessingColorSpace_Change.cs

@@ -1,4 +1,6 @@
 using Drawie.Backend.Core.Surfaces.ImageData;
+using Drawie.Numerics;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.ChangeInfos.Properties;
 
 namespace PixiEditor.ChangeableDocument.Changes.Properties;
@@ -7,31 +9,57 @@ internal class ChangeProcessingColorSpace_Change : Change
 {
     private ColorSpace toColorSpace;
     private ColorSpace original;
-    
+
     [GenerateMakeChangeAction]
     public ChangeProcessingColorSpace_Change(ColorSpace newColorSpace)
     {
         this.toColorSpace = newColorSpace;
     }
-    
+
     public override bool InitializeAndValidate(Document target)
     {
         original = target.ProcessingColorSpace;
-        return true;  
+        return true;
     }
 
-    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
+    public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply,
+        out bool ignoreInUndo)
     {
         ignoreInUndo = false;
         target.ProcessingColorSpace = toColorSpace;
 
+        ConvertImageNodes(target, toColorSpace);
+
         return new ProcessingColorSpace_ChangeInfo(toColorSpace);
     }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
         target.ProcessingColorSpace = original;
-        
+
+        ConvertImageNodes(target, original);
+
         return new ProcessingColorSpace_ChangeInfo(original);
     }
+
+    private void ConvertImageNodes(Document target, ColorSpace newColorSpace)
+    {
+        foreach (var node in target.NodeGraph.Nodes)
+        {
+            if (node is ImageLayerNode imageLayerNode)
+            {
+                foreach (var keyFrame in imageLayerNode.KeyFrames)
+                {
+                    if (keyFrame.Data is ChunkyImage chunkyImage)
+                    {
+                        ChunkyImage img = new ChunkyImage(chunkyImage.LatestSize, newColorSpace);
+                        img.EnqueueDrawCommitedChunkyImage(VecI.Zero, chunkyImage);
+                        img.CommitChanges();
+
+                        keyFrame.Data = img;
+                    }
+                }
+            }
+        }
+    }
 }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/Properties/LayerStructure/CreateStructureMemberMask_Change.cs

@@ -22,7 +22,7 @@ internal class CreateStructureMemberMask_Change : Change
         var member = target.FindMemberOrThrow(targetMember);
         if (member.EmbeddedMask is not null)
             throw new InvalidOperationException("Cannot create a mask; the target member already has one");
-        member.EmbeddedMask = new ChunkyImage(target.Size);
+        member.EmbeddedMask = new ChunkyImage(target.Size, target.ProcessingColorSpace);
 
         ignoreInUndo = false;
         return new StructureMemberMask_ChangeInfo(targetMember, true);

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/Structure/RasterizeMember_Change.cs

@@ -44,7 +44,7 @@ internal class RasterizeMember_Change : Change
         
         IRasterizable rasterizable = (IRasterizable)node;
         
-        ImageLayerNode imageLayer = new ImageLayerNode(target.Size);
+        ImageLayerNode imageLayer = new ImageLayerNode(target.Size, target.ProcessingColorSpace);
         imageLayer.MemberName = node.DisplayName;
 
         target.NodeGraph.AddNode(imageLayer);

BIN
src/PixiEditor.Extensions.Sdk/build/PixiEditor.Api.CGlueMSBuild.dll


+ 1 - 0
src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller/ViewModels/UpdateController.cs

@@ -36,6 +36,7 @@ public class UpdateController
         {
             Installer = new UpdateModule.UpdateInstaller(files[0], UpdateDirectory);
             log.AppendLine($"Installing update from {files[0]} to {UpdateDirectory}");
+            Console.WriteLine("Installing update, DO NOT CLOSE THIS WINDOW");
             Installer.Install(log);
         }
     }

+ 4 - 1
src/PixiEditor.UpdateModule/UpdateInstaller.cs

@@ -32,16 +32,19 @@ public class UpdateInstaller
             processes[0].WaitForExit();
             log.AppendLine("Processes killed.");
         }
-
+        
         log.AppendLine("Extracting files");
+        
         ZipFile.ExtractToDirectory(ArchiveFileName, UpdateFilesPath, true);
         
         log.AppendLine("Files extracted");
         string dirWithFiles = Directory.GetDirectories(UpdateFilesPath)[0];
         log.AppendLine($"Copying files from {dirWithFiles} to {TargetDirectory}");
+        
         CopyFilesToDestination(dirWithFiles, log);
         log.AppendLine("Files copied");
         log.AppendLine("Deleting archive and update files");
+        
         DeleteArchive();
     }
 

+ 9 - 3
src/PixiEditor.Windows/WindowsProcessUtility.cs

@@ -18,12 +18,18 @@ public class WindowsProcessUtility : IProcessUtility
         {
             FileName = path,
             Verb = "runas",
-            UseShellExecute = true,
+            UseShellExecute = createWindow,
             CreateNoWindow = !createWindow,
-            WindowStyle = createWindow ? ProcessWindowStyle.Normal : ProcessWindowStyle.Minimized,
+            RedirectStandardOutput = !createWindow,
+            RedirectStandardError = !createWindow,
+            WindowStyle = createWindow ? ProcessWindowStyle.Normal : ProcessWindowStyle.Hidden
         };
 
-        return Process.Start(startInfo);  
+        Process p = new Process();
+        p.StartInfo = startInfo;
+
+        p.Start();
+        return p;
     }
 
     public bool IsRunningAsAdministrator()

+ 12 - 4
src/PixiEditor/Helpers/DocumentViewModelBuilder.cs

@@ -7,6 +7,7 @@ using PixiEditor.ViewModels.Document;
 using PixiEditor.ChangeableDocument.Changeables.Graph;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using Drawie.Backend.Core;
+using Drawie.Backend.Core.Surfaces.ImageData;
 using PixiEditor.Extensions.CommonApi.Palettes;
 using Drawie.Numerics;
 using PixiEditor.Parser;
@@ -32,6 +33,7 @@ internal class DocumentViewModelBuilder
     public NodeGraphBuilder Graph { get; set; }
     public string ImageEncoderUsed { get; set; } = "QOI";
     public bool UsesLegacyColorBlending { get; set; } = false;
+    public Version? PixiParserVersionUsed { get; set; }
 
     public DocumentViewModelBuilder WithSize(int width, int height)
     {
@@ -206,6 +208,12 @@ internal class DocumentViewModelBuilder
         SerializerVersion = documentSerializerVersion;
         return this;
     }
+
+    public DocumentViewModelBuilder WithPixiParserVersion(Version version)
+    {
+        PixiParserVersionUsed = version;
+        return this;
+    }
 }
 
 internal class AnimationDataBuilder
@@ -310,7 +318,7 @@ internal class NodeGraphBuilder
         return this;
     }
 
-    public NodeGraphBuilder WithImageLayerNode(string name, Surface image, out int id)
+    public NodeGraphBuilder WithImageLayerNode(string name, Surface image, ColorSpace colorSpace, out int id)
     {
         this.WithNodeOfType(typeof(ImageLayerNode))
             .WithName(name)
@@ -320,7 +328,7 @@ internal class NodeGraphBuilder
                 new KeyFrameData
                 {
                     AffectedElement = ImageLayerNode.ImageLayerKey,
-                    Data = new ChunkyImage(image),
+                    Data = new ChunkyImage(image, colorSpace),
                     Duration = 0,
                     StartFrame = 0,
                     IsVisible = true
@@ -331,7 +339,7 @@ internal class NodeGraphBuilder
         return this;
     }
 
-    public NodeGraphBuilder WithImageLayerNode(string name, VecI size, out int id)
+    public NodeGraphBuilder WithImageLayerNode(string name, VecI size, ColorSpace colorSpace, out int id)
     {
         this.WithNodeOfType(typeof(ImageLayerNode))
             .WithName(name)
@@ -341,7 +349,7 @@ internal class NodeGraphBuilder
                 new KeyFrameData
                 {
                     AffectedElement = ImageLayerNode.ImageLayerKey,
-                    Data = new ChunkyImage(size),
+                    Data = new ChunkyImage(size, colorSpace),
                     Duration = 0,
                     StartFrame = 0,
                     IsVisible = true

+ 1 - 0
src/PixiEditor/Helpers/Extensions/PixiParserDocumentEx.cs

@@ -29,6 +29,7 @@ internal static class PixiParserDocumentEx
         }
 
         return DocumentViewModel.Build(b => b
+            .WithPixiParserVersion(document.Version)
             .WithSerializerData(document.SerializerName, document.SerializerVersion)
             .WithLegacyColorBlending(document.LegacyColorBlending)
             .WithSize(document.Width, document.Height)

+ 5 - 3
src/PixiEditor/Helpers/Extensions/PixiParserPixiV4DocumentEx.cs

@@ -3,6 +3,7 @@ using ChunkyImageLib;
 using Drawie.Backend.Core;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Surfaces;
+using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
 using PixiEditor.Extensions.CommonApi.Palettes;
 using Drawie.Numerics;
@@ -24,6 +25,7 @@ internal static class PixiParserPixiV4DocumentEx
         return DocumentViewModel.Build(b =>
         {
             b.ImageEncoderUsed = "PNG";
+            b.PixiParserVersionUsed = document.Version;
             b.WithSize(document.Width, document.Height)
                 .WithPalette(document.Palette, x => new PaletteColor(x.R, x.G, x.B))
                 .WithSwatches(document.Swatches, x => new(x.R, x.G, x.B))
@@ -100,7 +102,7 @@ internal static class PixiParserPixiV4DocumentEx
             if (folder.Mask is not null)
             {
                 inputValues["MaskIsVisible"] = folder.Mask.Enabled;
-                additionalValues["embeddedMask"] = new ChunkyImage(ConvertToNewMaskFormat(Surface.Load(folder.Mask.ImageBytes), document.Width, document.Height));
+                additionalValues["embeddedMask"] = new ChunkyImage(ConvertToNewMaskFormat(Surface.Load(folder.Mask.ImageBytes), document.Width, document.Height), ColorSpace.CreateSrgb());
             }
 
             PropertyConnection contentConnection = new PropertyConnection()
@@ -141,7 +143,7 @@ internal static class PixiParserPixiV4DocumentEx
             if (layer.Mask is not null)
             {
                 inputValues["MaskIsVisible"] = layer.Mask.Enabled;
-                additionalValues["embeddedMask"] = new ChunkyImage(ConvertToNewMaskFormat(Surface.Load(layer.Mask.ImageBytes), document.Width, document.Height)); 
+                additionalValues["embeddedMask"] = new ChunkyImage(ConvertToNewMaskFormat(Surface.Load(layer.Mask.ImageBytes), document.Width, document.Height), ColorSpace.CreateSrgb()); 
             }
 
             PropertyConnection connection = new PropertyConnection()
@@ -160,7 +162,7 @@ internal static class PixiParserPixiV4DocumentEx
                     new KeyFrameData()
                     {
                         AffectedElement = ImageLayerNode.ImageLayerKey,
-                        Data = new ChunkyImage(Surface.Load(layer.ImageBytes)),
+                        Data = new ChunkyImage(Surface.Load(layer.ImageBytes), ColorSpace.CreateSrgb()),
                         Duration = 0,
                         StartFrame = 0,
                         IsVisible = true

+ 42 - 30
src/PixiEditor/Models/DocumentModels/Public/DocumentOperationsModule.cs

@@ -424,16 +424,17 @@ internal class DocumentOperationsModule : IDocumentOperations
     /// </summary>
     public void Undo()
     {
-        IMidChangeUndoableExecutor executor = Internals.ChangeController.TryGetExecutorFeature<IMidChangeUndoableExecutor>();
+        IMidChangeUndoableExecutor executor =
+            Internals.ChangeController.TryGetExecutorFeature<IMidChangeUndoableExecutor>();
         if (executor is { CanUndo: true })
         {
             executor.OnMidChangeUndo();
             return;
         }
-        
-        if(Internals.ChangeController.IsBlockingChangeActive)
+
+        if (Internals.ChangeController.IsBlockingChangeActive)
             return;
-        
+
         Internals.ChangeController.TryStopActiveExecutor();
         Internals.ActionAccumulator.AddActions(new Undo_Action());
     }
@@ -443,14 +444,15 @@ internal class DocumentOperationsModule : IDocumentOperations
     /// </summary>
     public void Redo()
     {
-        IMidChangeUndoableExecutor executor = Internals.ChangeController.TryGetExecutorFeature<IMidChangeUndoableExecutor>();
+        IMidChangeUndoableExecutor executor =
+            Internals.ChangeController.TryGetExecutorFeature<IMidChangeUndoableExecutor>();
         if (executor is { CanRedo: true })
         {
             executor.OnMidChangeRedo();
             return;
         }
-        
-        if(Internals.ChangeController.IsBlockingChangeActive)
+
+        if (Internals.ChangeController.IsBlockingChangeActive)
             return;
 
         Internals.ChangeController.TryStopActiveExecutor();
@@ -549,7 +551,7 @@ internal class DocumentOperationsModule : IDocumentOperations
     {
         if (Internals.ChangeController.IsBlockingChangeActive)
             return;
-        
+
         Internals.ChangeController.TryStopActiveExecutor();
 
         Internals.ChangeController.TryStartExecutor(new PasteImageExecutor(image, startPos, memberGuid, drawOnMask));
@@ -597,9 +599,9 @@ internal class DocumentOperationsModule : IDocumentOperations
     {
         if (Internals.ChangeController.IsBlockingChangeActive)
             return;
-        
+
         Internals.ChangeController.TryStopActiveExecutor();
-        
+
         Internals.ActionAccumulator.AddActions(
             new PasteImage_Action(image, corners, memberGuid, ignoreClipSymmetriesEtc, drawOnMask, atFrame, default),
             new EndPasteImage_Action());
@@ -614,9 +616,9 @@ internal class DocumentOperationsModule : IDocumentOperations
     {
         if (Internals.ChangeController.IsBlockingChangeActive)
             return;
-        
+
         Internals.ChangeController.TryStopActiveExecutor();
-        
+
         Internals.ActionAccumulator.AddFinishedActions(
             new ClipCanvas_Action(Document.AnimationHandler.ActiveFrameBindable));
     }
@@ -633,7 +635,7 @@ internal class DocumentOperationsModule : IDocumentOperations
     {
         if (Internals.ChangeController.IsBlockingChangeActive)
             return;
-        
+
         Internals.ChangeController.TryStopActiveExecutor();
 
         Internals.ActionAccumulator.AddFinishedActions(new FlipImage_Action(flipType, frame, membersToFlip));
@@ -655,7 +657,7 @@ internal class DocumentOperationsModule : IDocumentOperations
             return;
 
         Internals.ChangeController.TryStopActiveExecutor();
-        
+
         Internals.ActionAccumulator.AddFinishedActions(new RotateImage_Action(rotation, membersToRotate, frame));
     }
 
@@ -666,7 +668,7 @@ internal class DocumentOperationsModule : IDocumentOperations
     {
         if (Internals.ChangeController.IsBlockingChangeActive)
             return;
-        
+
         Internals.ChangeController.TryStopActiveExecutor();
 
         Internals.ActionAccumulator.AddFinishedActions(new CenterContent_Action(structureMembers.ToList(), frame));
@@ -680,7 +682,7 @@ internal class DocumentOperationsModule : IDocumentOperations
     {
         if (Internals.ChangeController.IsBlockingChangeActive)
             return;
-        
+
         Internals.ChangeController.TryStopActiveExecutor();
 
         RectD referenceImageRect =
@@ -695,7 +697,8 @@ internal class DocumentOperationsModule : IDocumentOperations
     /// </summary>
     public void DeleteReferenceLayer()
     {
-        if (Internals.ChangeController.IsBlockingChangeActive || Document.ReferenceLayerHandler.ReferenceTexture is null)
+        if (Internals.ChangeController.IsBlockingChangeActive ||
+            Document.ReferenceLayerHandler.ReferenceTexture is null)
             return;
 
         Internals.ChangeController.TryStopActiveExecutor();
@@ -708,11 +711,12 @@ internal class DocumentOperationsModule : IDocumentOperations
     /// </summary>
     public void TransformReferenceLayer()
     {
-        if (Document.ReferenceLayerHandler.ReferenceTexture is null || Internals.ChangeController.IsBlockingChangeActive)
+        if (Document.ReferenceLayerHandler.ReferenceTexture is null ||
+            Internals.ChangeController.IsBlockingChangeActive)
             return;
-        
+
         Internals.ChangeController.TryStopActiveExecutor();
-        
+
         Internals.ChangeController.TryStartExecutor(new TransformReferenceLayerExecutor());
     }
 
@@ -721,9 +725,10 @@ internal class DocumentOperationsModule : IDocumentOperations
     /// </summary>
     public void ResetReferenceLayerPosition()
     {
-        if (Document.ReferenceLayerHandler.ReferenceTexture is null || Internals.ChangeController.IsBlockingChangeActive)
+        if (Document.ReferenceLayerHandler.ReferenceTexture is null ||
+            Internals.ChangeController.IsBlockingChangeActive)
             return;
-        
+
         Internals.ChangeController.TryStopActiveExecutor();
 
         VecD size = new(Document.ReferenceLayerHandler.ReferenceTexture.Size.X,
@@ -742,7 +747,7 @@ internal class DocumentOperationsModule : IDocumentOperations
             return;
 
         Internals.ChangeController.TryStopActiveExecutor();
-        
+
         if (Document.SelectedStructureMember is not { } member || Document.SelectionPathBindable.IsEmpty)
             return;
 
@@ -758,9 +763,9 @@ internal class DocumentOperationsModule : IDocumentOperations
     {
         if (Internals.ChangeController.IsBlockingChangeActive)
             return;
-        
+
         Internals.ChangeController.TryStopActiveExecutor();
-        
+
         var bounds = Document.SelectionPathBindable.TightBounds;
         if (Document.SelectionPathBindable.IsEmpty || bounds.Width <= 0 || bounds.Height <= 0)
             return;
@@ -781,9 +786,9 @@ internal class DocumentOperationsModule : IDocumentOperations
     {
         if (Internals.ChangeController.IsBlockingChangeActive)
             return;
-        
+
         Internals.ChangeController.TryStopActiveExecutor();
-        
+
         var selection = Document.SelectionPathBindable;
         var inverse = new VectorPath();
         inverse.AddRect(new RectD(new(0, 0), Document.SizeBindable));
@@ -796,7 +801,7 @@ internal class DocumentOperationsModule : IDocumentOperations
     {
         if (Internals.ChangeController.IsBlockingChangeActive)
             return;
-        
+
         Internals.ChangeController.TryStopActiveExecutor();
 
         Internals.ActionAccumulator.AddFinishedActions(new RasterizeMember_Action(memberId));
@@ -806,7 +811,7 @@ internal class DocumentOperationsModule : IDocumentOperations
     {
         if (Internals.ChangeController.IsBlockingChangeActive)
             return;
-        
+
         Internals.ChangeController.TryStopActiveExecutor();
 
         IAction targetAction = new InvokeAction_PassthroughAction(action);
@@ -816,6 +821,13 @@ internal class DocumentOperationsModule : IDocumentOperations
 
     public void UseLinearSrgbProcessing()
     {
-        Internals.ActionAccumulator.AddFinishedActions(new ChangeProcessingColorSpace_Action(ColorSpace.CreateSrgbLinear()));
+        Internals.ActionAccumulator.AddFinishedActions(
+            new ChangeProcessingColorSpace_Action(ColorSpace.CreateSrgbLinear()));
+    }
+
+    public void UseSrgbProcessing()
+    {
+        Internals.ActionAccumulator.AddFinishedActions(
+            new ChangeProcessingColorSpace_Action(ColorSpace.CreateSrgb()));
     }
 }

+ 1 - 1
src/PixiEditor/Models/Serialization/Factories/ChunkyImageSerializationFactory.cs

@@ -35,7 +35,7 @@ public class ChunkyImageSerializationFactory : SerializationFactory<byte[], Chun
                 return false;
             }
 
-            original = new ChunkyImage(surface.Size);
+            original = new ChunkyImage(surface.Size, Config.ProcessingProcessingColorSpace);
             original.EnqueueDrawImage(VecI.Zero, surface);
             original.CommitChanges();
             return true;

+ 5 - 2
src/PixiEditor/Models/Serialization/SerializationConfig.cs

@@ -1,13 +1,16 @@
-using PixiEditor.Parser.Skia;
+using Drawie.Backend.Core.Surfaces.ImageData;
+using PixiEditor.Parser.Skia;
 
 namespace PixiEditor.Models.Serialization;
 
 public class SerializationConfig
 {
     public ImageEncoder Encoder { get; set; }
+    public ColorSpace ProcessingProcessingColorSpace { get; set; }
     
-    public SerializationConfig(ImageEncoder encoder)
+    public SerializationConfig(ImageEncoder encoder, ColorSpace processingColorSpace)
     {
         Encoder = encoder;
+        ProcessingProcessingColorSpace = processingColorSpace;
     }
 }

+ 1 - 1
src/PixiEditor/ViewModels/Document/DocumentViewModel.Serialization.cs

@@ -52,7 +52,7 @@ internal partial class DocumentViewModel
     {
         NodeGraph graph = new();
         ImageEncoder encoder = new QoiEncoder();
-        var serializationConfig = new SerializationConfig(encoder);
+        var serializationConfig = new SerializationConfig(encoder, ColorSpace.CreateSrgb());
         var doc = Internals.Tracker.Document;
 
         Dictionary<Guid, int> nodeIdMap = new();

+ 18 - 7
src/PixiEditor/ViewModels/Document/DocumentViewModel.cs

@@ -294,9 +294,13 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
 
         var acc = viewModel.Internals.ActionAccumulator;
 
-        if (builderInstance.UsesLegacyColorBlending || IsFileWithOldColorBlending(serializerData))
+        ColorSpace targetProcessingColorSpace = ColorSpace.CreateSrgbLinear();
+        if (builderInstance.UsesLegacyColorBlending ||
+            IsFileWithOldColorBlending(serializerData, builderInstance.PixiParserVersionUsed))
         {
-            acc.AddFinishedActions(new ChangeProcessingColorSpace_Action(ColorSpace.CreateSrgb()));
+            targetProcessingColorSpace = ColorSpace.CreateSrgb();
+            viewModel.Internals.Tracker.Document.InitProcessingColorSpace(ColorSpace.CreateSrgb());
+            viewModel.UsesLegacyBlending = true;
         }
 
         viewModel.Internals.ChangeController.SymmetryDraggedInlet(
@@ -320,7 +324,8 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         viewModel.Palette = new ObservableRangeCollection<PaletteColor>(builderInstance.Palette);
 
         SerializationConfig config =
-            new SerializationConfig(BuiltInEncoders.Encoders[builderInstance.ImageEncoderUsed]);
+            new SerializationConfig(BuiltInEncoders.Encoders[builderInstance.ImageEncoderUsed],
+                targetProcessingColorSpace);
 
         List<SerializationFactory> allFactories =
             ViewModelMain.Current.Services.GetServices<SerializationFactory>().ToList();
@@ -447,19 +452,25 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
                 }
             }
         }
-        
-        bool IsFileWithOldColorBlending((string serializerName, string serializerVersion) serializerData)
+
+        bool IsFileWithOldColorBlending((string serializerName, string serializerVersion) serializerData,
+            Version? pixiParserVersionUsed)
         {
-            if(string.IsNullOrEmpty(serializerData.serializerName) && string.IsNullOrEmpty(serializerData.serializerVersion))
+            if (pixiParserVersionUsed != null && pixiParserVersionUsed.Major < 5)
             {
                 return true;
             }
 
+            if (serializerData.serializerVersion == null || serializerData.serializerName == null)
+            {
+                return false;
+            }
+
             try
             {
                 Version parsedVersion = new Version(serializerData.serializerVersion);
 
-                return serializerData.serializerName == "PixiEditor" 
+                return serializerData.serializerName == "PixiEditor"
                        && parsedVersion is { Major: 2, Minor: 0, Build: 0, Revision: >= 28 and <= 31 };
             }
             catch (Exception)

+ 10 - 3
src/PixiEditor/ViewModels/SubViewModels/FileViewModel.cs

@@ -16,6 +16,7 @@ using PixiEditor.ChangeableDocument.Changeables.Graph;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using Drawie.Backend.Core;
 using Drawie.Backend.Core.Numerics;
+using Drawie.Backend.Core.Surfaces.ImageData;
 using PixiEditor.Exceptions;
 using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Extensions.CommonApi.UserPreferences.Settings.PixiEditor;
@@ -261,7 +262,9 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
             .WithGraph(x => x
                 .WithImageLayerNode(
                     new LocalizedString("IMAGE"),
-                    image, out int id)
+                    image, 
+                    ColorSpace.CreateSrgbLinear(),
+                    out int id)
                 .WithOutputNode(id, "Output")
             ));
 
@@ -297,7 +300,9 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
             .WithGraph(x => x
                 .WithImageLayerNode(
                     new LocalizedString("IMAGE"),
-                    surface, out int id)
+                    surface,
+                    ColorSpace.CreateSrgbLinear(),
+                    out int id)
                 .WithOutputNode(id, "Output")
             ));
 
@@ -325,7 +330,9 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
                 .WithGraph(x => x
                     .WithImageLayerNode(
                         new LocalizedString("BASE_LAYER_NAME"),
-                        new VecI(newFile.Width, newFile.Height), out int id)
+                        new VecI(newFile.Width, newFile.Height), 
+                        ColorSpace.CreateSrgbLinear(),
+                        out int id)
                     .WithOutputNode(id, "Output")
                 ));
 

+ 0 - 1
src/PixiEditor/Views/Main/Tools/Toolbar.axaml

@@ -13,7 +13,6 @@
             BorderThickness="{DynamicResource ThemeBorderThickness}"
             Cursor="Arrow"
             Padding="5"
-            Margin="10"
             Height="40"
             HorizontalAlignment="Left"
             Background="{DynamicResource ThemeBackgroundBrush1}">

+ 19 - 9
src/PixiEditor/Views/Main/Tools/ToolsPicker.axaml

@@ -16,8 +16,13 @@
             Cursor="Arrow"
             Width="48"
             Background="{DynamicResource ThemeBackgroundBrush1}">
-        <StackPanel>
-            <Border Background="{DynamicResource ThemeBackgroundBrush2}">
+        <Grid>
+            <Grid.RowDefinitions>
+                <RowDefinition Height="25" />
+                <RowDefinition Height="*" />
+            </Grid.RowDefinitions>
+            
+            <Border Grid.Row="0" Background="{DynamicResource ThemeBackgroundBrush2}">
                 <StackPanel Orientation="Horizontal">
                     <Button Classes="pixi-icon" Content="{DynamicResource icon-chevron-left}"
                             Command="{Binding SwitchToolSetCommand, ElementName=picker}">
@@ -31,17 +36,22 @@
                             <system:Boolean>True</system:Boolean>
                         </Button.CommandParameter>
                     </Button>
-                    <Border Padding="5" Margin="5 0" ClipToBounds="False" Background="{DynamicResource ThemeBackgroundBrush2}"
-                            CornerRadius="{DynamicResource ControlCornerRadius}">
-                       <TextBlock ui:Translator.Key="{Binding ElementName=picker, Path=ToolSet.Name}" VerticalAlignment="Center"/> 
-                    </Border>
+                    <!--<ComboBox ItemsSource="{Binding ElementName=picker, Path=ToolSets}"
+                              SelectedItem="{Binding ElementName=picker, Path=ToolSet}"
+                              SelectedIndex="0">
+                        <ComboBox.ItemTemplate>
+                            <DataTemplate>
+                                <TextBlock ui:Translator.Key="{Binding Name}" />
+                            </DataTemplate>
+                        </ComboBox.ItemTemplate>
+                    </ComboBox>-->
                 </StackPanel>
             </Border>
-            <ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
+            <ScrollViewer Grid.Row="1" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
                 <ItemsControl ItemsSource="{Binding ElementName=picker, Path=ToolSet.Tools}" Padding="0 2">
                     <ItemsControl.ItemTemplate>
                         <DataTemplate DataType="tools:ToolViewModel">
-                            <tools1:ToolPickerButton DataContext="{Binding}" 
+                            <tools1:ToolPickerButton DataContext="{Binding}"
                                                      IsSelected="{Binding IsActive}" />
                         </DataTemplate>
                     </ItemsControl.ItemTemplate>
@@ -52,6 +62,6 @@
                     </ItemsControl.ItemsPanel>
                 </ItemsControl>
             </ScrollViewer>
-        </StackPanel>
+        </Grid>
     </Border>
 </UserControl>

+ 12 - 3
src/PixiEditor/Views/Main/Tools/ToolsPicker.axaml.cs

@@ -1,4 +1,5 @@
-using System.Windows.Input;
+using System.Collections.ObjectModel;
+using System.Windows.Input;
 using Avalonia;
 using Avalonia.Controls;
 using PixiEditor.Models.Handlers;
@@ -7,6 +8,8 @@ namespace PixiEditor.Views.Main.Tools;
 
 internal partial class ToolsPicker : UserControl
 {
+    public static readonly StyledProperty<ObservableCollection<IToolSetHandler>> ToolSetsProperty = AvaloniaProperty.Register<ToolsPicker, ObservableCollection<IToolSetHandler>>("ToolSets");
+
     public static readonly StyledProperty<IToolSetHandler> ToolSetProperty =
         AvaloniaProperty.Register<ToolsPicker, IToolSetHandler>(
             nameof(ToolSet));
@@ -16,15 +19,21 @@ internal partial class ToolsPicker : UserControl
         set => SetValue(ToolSetProperty, value);
     }
 
+    public ObservableCollection<IToolSetHandler> ToolSets
+    {
+        get { return (ObservableCollection<IToolSetHandler>)GetValue(ToolSetsProperty); }
+        set { SetValue(ToolSetsProperty, value); }
+    }
+    
     public static readonly StyledProperty<ICommand> SwitchToolSetCommandProperty = AvaloniaProperty.Register<ToolsPicker, ICommand>(
         "SwitchToolSetCommand");
-
+    
     public ICommand SwitchToolSetCommand
     {
         get => GetValue(SwitchToolSetCommandProperty);
         set => SetValue(SwitchToolSetCommandProperty, value);
     }
-
+    
     public ToolsPicker()
     {
         InitializeComponent();

+ 28 - 9
src/PixiEditor/Views/Main/ViewportControls/Viewport.axaml

@@ -130,7 +130,7 @@
                                           Classes="OverlayToggleButton pixi-icon"
                                           Content="{DynamicResource icon-gridlines}"
                                           IsChecked="{Binding GridLinesVisible, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=viewportControls:Viewport}, Mode=TwoWay}" />
-                            
+
                             <ToggleButton Margin="10 0 0 0" Width="32" Height="32"
                                           ui:Translator.TooltipKey="TOGGLE_SNAPPING"
                                           Classes="OverlayToggleButton pixi-icon"
@@ -141,14 +141,33 @@
                 </Border>
             </overlays:TogglableFlyout.Child>
         </overlays:TogglableFlyout>
-        <tools:Toolbar ZIndex="100" VerticalAlignment="Top" DataContext="{Binding Source={viewModels:MainVM}, Path=.}" />
-
-        <tools:ToolsPicker ZIndex="100"
-                           Margin="10 55 0 0"
-                           HorizontalAlignment="Left"
-                           VerticalAlignment="Top"
-                           ToolSet="{Binding Source={viewModels:MainVM}, Path=ToolsSubViewModel.ActiveToolSet}"
-                           SwitchToolSetCommand="{xaml:Command Name=PixiEditor.Tools.SwitchToolSet, UseProvided=True}" />
+        <Grid ZIndex="100" VerticalAlignment="Top" HorizontalAlignment="Left" Margin="10">
+            <Grid.RowDefinitions>
+                <RowDefinition Height="45" />
+                <RowDefinition Height="35" />
+                <RowDefinition Height="*" />
+            </Grid.RowDefinitions>
+            <tools:Toolbar
+                DataContext="{Binding Source={viewModels:MainVM}, Path=.}" />
+            <Border Grid.Row="1" ClipToBounds="False"
+                    HorizontalAlignment="Left"
+                    Padding="5 0"
+                    Margin="0 2.5 0 5"
+                    Height="25"
+                    BorderBrush="{DynamicResource ThemeBorderMidBrush}"
+                    BorderThickness="1"
+                    Background="{DynamicResource ThemeBackgroundBrush2}"
+                    CornerRadius="{DynamicResource ControlCornerRadius}">
+                <TextBlock
+                    ui:Translator.Key="{Binding Source={viewModels:MainVM}, Path=ToolsSubViewModel.ActiveToolSet.Name}"
+                    VerticalAlignment="Center" />
+            </Border>
+            <tools:ToolsPicker Grid.Row="2" 
+                HorizontalAlignment="Left"
+                ToolSet="{Binding Source={viewModels:MainVM}, Path=ToolsSubViewModel.ActiveToolSet, Mode=TwoWay}"
+                               ToolSets="{Binding Source={viewModels:MainVM}, Path=ToolsSubViewModel.AllToolSets, Mode=OneWay}"
+                               SwitchToolSetCommand="{xaml:Command Name=PixiEditor.Tools.SwitchToolSet, UseProvided=True}" />
+        </Grid>
         <rendering:Scene
             Focusable="False" Name="scene"
             ZIndex="1"

+ 2 - 2
src/PixiEditor/Views/Rendering/Scene.cs

@@ -139,7 +139,7 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
     {
         AffectsRender<Scene>(BoundsProperty, WidthProperty, HeightProperty, ScaleProperty, AngleRadiansProperty,
             FlipXProperty,
-            FlipYProperty, DocumentProperty, AllOverlaysProperty);
+            FlipYProperty, DocumentProperty, AllOverlaysProperty, ContentDimensionsProperty);
 
         FadeOutProperty.Changed.AddClassHandler<Scene>(FadeOutChanged);
         CheckerImagePathProperty.Changed.AddClassHandler<Scene>(CheckerImagePathChanged);
@@ -153,7 +153,7 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
 
     private static void Refresh(Scene scene, AvaloniaPropertyChangedEventArgs args)
     {
-        scene.InvalidateVisual();
+        scene.QueueNextFrame();
     }
 
     public Scene()

+ 1 - 0
src/PixiEditor/Views/Windows/BetaExampleButton.axaml.cs

@@ -71,6 +71,7 @@ public partial class BetaExampleButton : UserControl
         CloseCommand.Execute(null);
         
         ViewModelMain.Current.FileSubViewModel.OpenRecoveredDotPixi(null, bytes);
+        ViewModelMain.Current.DocumentManagerSubViewModel.Documents[^1].Operations.UseSrgbProcessing();
         Analytics.SendOpenExample(FileName);
     }
 

+ 5 - 5
tests/ChunkyImageLibTest/ChunkyImageTests.cs

@@ -23,7 +23,7 @@ public class ChunkyImageTests
     [Fact]
     public void Dispose_ComplexImage_ReturnsAllChunks()
     {
-        ChunkyImage image = new ChunkyImage(new VecI(ChunkyImage.FullChunkSize, ChunkyImage.FullChunkSize));
+        ChunkyImage image = new ChunkyImage(new VecI(ChunkyImage.FullChunkSize, ChunkyImage.FullChunkSize), ColorSpace.CreateSrgb());
         image.EnqueueDrawRectangle(new(new(5, 5), new(80, 80), 0, 2, Colors.AliceBlue, Colors.Snow));
         using (Chunk target = Chunk.Create(ColorSpace.CreateSrgb()))
         {
@@ -51,7 +51,7 @@ public class ChunkyImageTests
     public void GetCommittedPixel_RedImage_ReturnsRedPixel()
     {
         const int chunkSize = ChunkyImage.FullChunkSize;
-        ChunkyImage image = new ChunkyImage(new VecI(chunkSize * 2));
+        ChunkyImage image = new ChunkyImage(new VecI(chunkSize * 2), ColorSpace.CreateSrgb());
         image.EnqueueDrawRectangle
             (new ShapeData(new VecD(chunkSize), new VecD(chunkSize * 2), 0, 0, Colors.Transparent, Colors.Red));
         image.CommitChanges();
@@ -64,7 +64,7 @@ public class ChunkyImageTests
     public void GetMostUpToDatePixel_BlendModeSrc_ReturnsCorrectPixel()
     {
         const int chunkSize = ChunkyImage.FullChunkSize;
-        ChunkyImage image = new ChunkyImage(new VecI(chunkSize * 2));
+        ChunkyImage image = new ChunkyImage(new VecI(chunkSize * 2), ColorSpace.CreateSrgb());
         image.EnqueueDrawRectangle
             (new ShapeData(new VecD(chunkSize), new VecD(chunkSize * 2), 0, 0, Colors.Transparent, Colors.Red));
         Assert.Equal(Colors.Red, image.GetMostUpToDatePixel(new VecI(chunkSize + chunkSize / 2)));
@@ -76,7 +76,7 @@ public class ChunkyImageTests
     public void GetMostUpToDatePixel_BlendModeSrcOver_ReturnsCorrectPixel()
     {
         const int chunkSize = ChunkyImage.FullChunkSize;
-        ChunkyImage image = new ChunkyImage(new VecI(chunkSize * 2));
+        ChunkyImage image = new ChunkyImage(new VecI(chunkSize * 2), ColorSpace.CreateSrgb());
         image.EnqueueDrawRectangle
             (new ShapeData(new VecD(chunkSize), new VecD(chunkSize * 2), 0, 0, Colors.Transparent, Colors.Red));
         image.CommitChanges();
@@ -97,7 +97,7 @@ public class ChunkyImageTests
     public void EnqueueDrawRectangle_OutsideOfImage_PartsAreNotDrawn()
     {
         const int chunkSize = ChunkyImage.FullChunkSize;
-        using ChunkyImage image = new(new VecI(chunkSize));
+        using ChunkyImage image = new(new VecI(chunkSize), ColorSpace.CreateSrgb());
         image.EnqueueDrawRectangle(new ShapeData(
                 VecD.Zero,
                 new VecD(chunkSize * 10),

+ 4 - 0
tests/PixiEditor.Backend.Tests/MockDocument.cs

@@ -75,4 +75,8 @@ public class MockDocument : IReadOnlyDocument
     public IReadOnlyReferenceLayer? ReferenceLayer { get; }
     public DocumentRenderer Renderer { get; }
     public ColorSpace ProcessingColorSpace { get; }
+    public void InitProcessingColorSpace(ColorSpace processingColorSpace)
+    {
+        throw new NotImplementedException();
+    }
 }

+ 3 - 2
tests/PixiEditor.Backend.Tests/NodeSystemTests.cs

@@ -1,5 +1,6 @@
 using System.Reflection;
 using Drawie.Backend.Core.Bridge;
+using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Interop.Avalonia.Core;
 using Drawie.Interop.VulkanAvalonia;
 using Drawie.Skia;
@@ -113,7 +114,7 @@ public class NodeSystemTests
 
         List<SerializationFactory> factories = new();
         QoiEncoder encoder = new QoiEncoder();
-        SerializationConfig config = new SerializationConfig(encoder);
+        SerializationConfig config = new SerializationConfig(encoder, ColorSpace.CreateSrgbLinear());
 
         foreach (var factoryType in allFoundFactories)
         {
@@ -149,7 +150,7 @@ public class NodeSystemTests
                         && x is { IsAbstract: false, IsInterface: false }).ToList();
 
         QoiEncoder encoder = new QoiEncoder();
-        SerializationConfig config = new SerializationConfig(encoder);
+        SerializationConfig config = new SerializationConfig(encoder, ColorSpace.CreateSrgbLinear());
 
         var factoryTypes = typeof(SerializationFactory).Assembly.GetTypes()
             .Where(x => x.IsAssignableTo(typeof(SerializationFactory))