소스 검색

Merge branch 'master' into development

flabbet 2 년 전
부모
커밋
72e64f5a7b
52개의 변경된 파일420개의 추가작업 그리고 358개의 파일을 삭제
  1. 1 7
      azure-pipelines.yml
  2. 1 1
      incompatible.json
  3. 12 13
      src/ChunkyImageLib/ChunkyImage.cs
  4. 1 1
      src/ChunkyImageLib/Operations/BresenhamLineOperation.cs
  5. 1 0
      src/Custom.ruleset
  6. 14 14
      src/Installer/installer-setup-x64-light.iss
  7. 14 14
      src/Installer/installer-setup-x86-light.iss
  8. 8 2
      src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFillHelper.cs
  9. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFill_Change.cs
  10. 5 0
      src/PixiEditor.ChangeableDocument/Changes/Drawing/TransformSelectedArea_UpdateableChange.cs
  11. 1 1
      src/PixiEditor.ChangeableDocument/Changes/Root/CenterContent_Change.cs
  12. 2 6
      src/PixiEditor.ChangeableDocument/Changes/Root/ClipCanvas_Change.cs
  13. 3 0
      src/PixiEditor.ChangeableDocument/Changes/Root/ResizeBasedChangeBase.cs
  14. 7 1
      src/PixiEditor.ChangeableDocument/Changes/Root/ResizeCanvas_Change.cs
  15. 7 1
      src/PixiEditor.ChangeableDocument/Changes/Root/ResizeImage_Change.cs
  16. 46 38
      src/PixiEditor.ChangeableDocument/Changes/Selection/MagicWand/MagicWandHelper.cs
  17. 2 2
      src/PixiEditor.ChangeableDocument/Changes/Selection/SelectLasso_UpdateableChange.cs
  18. 1 1
      src/PixiEditor.ChangeableDocument/PixiEditor.ChangeableDocument.csproj
  19. 1 1
      src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller.csproj
  20. 1 1
      src/PixiEditor.UpdateModule/PixiEditor.UpdateModule.csproj
  21. 1 1
      src/PixiEditor.Zoombox/PixiEditor.Zoombox.csproj
  22. 1 1
      src/PixiEditor/Helpers/ChangeInfoListOptimizer.cs
  23. 0 81
      src/PixiEditor/Helpers/CoordinatesHelper.cs
  24. 3 3
      src/PixiEditor/Helpers/DocumentViewModelBuilder.cs
  25. 4 5
      src/PixiEditor/Helpers/Extensions/PixiParserDocumentEx.cs
  26. 4 2
      src/PixiEditor/Models/Commands/XAML/Command.cs
  27. 3 3
      src/PixiEditor/Models/DocumentModels/ActionAccumulator.cs
  28. 2 2
      src/PixiEditor/Models/DocumentModels/DocumentUpdater.cs
  29. 4 3
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/LineToolExecutor.cs
  30. 84 76
      src/PixiEditor/Models/IO/Exporter.cs
  31. 5 1
      src/PixiEditor/Models/UserPreferences/PreferencesConstants.cs
  32. 2 2
      src/PixiEditor/PixiEditor.csproj
  33. 2 2
      src/PixiEditor/Properties/AssemblyInfo.cs
  34. 2 2
      src/PixiEditor/ViewModels/SaveFilePopupViewModel.cs
  35. 28 3
      src/PixiEditor/ViewModels/SubViewModels/Document/DocumentViewModel.cs
  36. 67 20
      src/PixiEditor/ViewModels/SubViewModels/Main/FileViewModel.cs
  37. 2 9
      src/PixiEditor/ViewModels/SubViewModels/Main/StylusViewModel.cs
  38. 1 1
      src/PixiEditor/ViewModels/SubViewModels/Main/WindowViewModel.cs
  39. 2 1
      src/PixiEditor/ViewModels/SubViewModels/UserPreferences/Settings/FileSettings.cs
  40. 51 16
      src/PixiEditor/Views/Dialogs/AboutPopup.xaml
  41. 4 3
      src/PixiEditor/Views/MainWindow.xaml.cs
  42. 2 2
      src/PixiEditor/Views/UserControls/FixedViewport.xaml.cs
  43. 2 1
      src/PixiEditor/Views/UserControls/Palettes/PaletteColorAdder.xaml
  44. 0 3
      src/PixiEditor/Views/UserControls/Palettes/PaletteColorAdder.xaml.cs
  45. 1 1
      src/PixiEditor/Views/UserControls/Viewport.xaml
  46. 2 2
      src/PixiEditor/Views/UserControls/Viewport.xaml.cs
  47. 1 3
      src/PixiEditorTests/PixiEditorTests.csproj
  48. 7 0
      src/global.json
  49. 1 1
      windows-x64-release-dev.yml
  50. 1 1
      windows-x64-release.yml
  51. 1 1
      windows-x86-release-dev.yml
  52. 1 1
      windows-x86-release.yml

+ 1 - 7
azure-pipelines.yml

@@ -17,12 +17,6 @@ steps:
   inputs:
     restoreSolution: '$(solution)'
 
-- task: DotNetCoreCLI@2
-  inputs:
-    command: 'custom'
-    custom: 'tool'
-    arguments: 'install --global Codecov.Tool'
-
 - task: DotNetCoreCLI@2
   displayName: Build
   inputs:
@@ -35,7 +29,7 @@ steps:
   inputs:
     command: test
     projects: '**/*Tests/*.csproj'
-    arguments: '--configuration $(buildConfiguration) --collect "Code coverage"'
+    arguments: '--configuration $(buildConfiguration)'
 
 #- task: PowerShell@2
 #  inputs:

+ 1 - 1
incompatible.json

@@ -1 +1 @@
-["0.1.3.0", "0.1.3.1", "0.1.3.2", "0.1.3.3", "0.1.3.4", "0.1.3.5", "0.1.3.6", "0.1.4.0", "0.1.5.0", "0.1.6.0"]
+["0.1.3.0", "0.1.3.1", "0.1.3.2", "0.1.3.3", "0.1.3.4", "0.1.3.5", "0.1.3.6", "0.1.4.0", "0.1.5.0", "0.1.6.0", "0.1.7.0", "0.1.8.0"]

+ 12 - 13
src/ChunkyImageLib/ChunkyImage.cs

@@ -137,27 +137,26 @@ public class ChunkyImage : IReadOnlyChunkyImage, IDisposable
             return rect;
         }
     }
-    
-    public RectI? FindPreciseBounds()
+
+    /// <exception cref="ObjectDisposedException">This image is disposed</exception>
+    public RectI? FindPreciseCommittedBounds()
     {
         lock (lockObject)
         {
             ThrowIfDisposed();
-            RectI? chunkBounds = FindLatestBounds();
-            if(!chunkBounds.HasValue) return null;
 
             RectI? preciseBounds = null;
-
-            foreach (var chunk in committedChunks[ChunkResolution.Full])
+            foreach (var (chunkPos, chunk) in committedChunks[ChunkResolution.Full])
             {
-                if(!chunkBounds.Value.Intersects(new RectI(chunk.Key, new VecI(FullChunkSize)))) continue;
-                
-                RectI? chunkPreciseBounds = chunk.Value.FindPreciseBounds();
-                if(!chunkPreciseBounds.HasValue) continue;
-                
-                preciseBounds ??= chunkPreciseBounds.Value;
-                preciseBounds = preciseBounds.Value.Union(chunkPreciseBounds.Value);
+                RectI? chunkPreciseBounds = chunk.FindPreciseBounds();
+                if(chunkPreciseBounds is null) 
+                    continue;
+                RectI globalChunkBounds = chunkPreciseBounds.Value.Offset(chunkPos * FullChunkSize);
+
+                preciseBounds ??= globalChunkBounds;
+                preciseBounds = preciseBounds.Value.Union(globalChunkBounds);
             }
+            preciseBounds = preciseBounds?.Intersect(new RectI(VecI.Zero, CommittedSize));
 
             return preciseBounds;
         }

+ 1 - 1
src/ChunkyImageLib/Operations/BresenhamLineOperation.cs

@@ -40,7 +40,7 @@ internal class BresenhamLineOperation : IDrawOperation
 
     public HashSet<VecI> FindAffectedChunks(VecI imageSize)
     {
-        RectI bounds = RectI.FromTwoPoints(from, to + new VecI(1));
+        RectI bounds = RectI.FromTwoPixels(from, to);
         return OperationHelper.FindChunksTouchingRectangle(bounds, ChunkyImage.FullChunkSize);
     }
 

+ 1 - 0
src/Custom.ruleset

@@ -58,6 +58,7 @@
     <Rule Id="SA1310" Action="None" />
     <Rule Id="SA1311" Action="None" />
     <Rule Id="SA1313" Action="None" />
+    <Rule Id="SA1316" Action="None" />
     <Rule Id="SA1400" Action="None" />
     <Rule Id="SA1401" Action="None" />
     <Rule Id="SA1402" Action="None" />

+ 14 - 14
src/Installer/installer-setup-x64-light.iss

@@ -4,8 +4,8 @@
 // requires netcorecheck.exe and netcorecheck_x64.exe (see download link below)
 #define UseNetCoreCheck
 #ifdef UseNetCoreCheck
-  ;#define UseDotNet60
-  #define UseDotNet60Desktop
+  ;#define UseDotNet70
+  #define UseDotNet70Desktop
 #endif
 
 // custom setup info
@@ -401,24 +401,24 @@ var
   Version: String;
 begin
 
-#ifdef UseDotNet60
-  // https://dotnet.microsoft.com/download/dotnet/6.0
-  if not IsNetCoreInstalled('Microsoft.NETCore.App 6.0.0') then begin
-    AddDependency('dotnet60' + GetArchitectureSuffix + '.exe',
+#ifdef UseDotNet70
+  // https://dotnet.microsoft.com/download/dotnet/7.0
+  if not IsNetCoreInstalled('Microsoft.NETCore.App 7.0.0') then begin
+    AddDependency('dotnet70' + GetArchitectureSuffix + '.exe',
       '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
-      '.NET Runtime 6.0' + GetArchitectureTitle,
-      GetString('https://download.visualstudio.microsoft.com/download/pr/34df41d5-c813-4e30-8aa3-3603ce6600c0/976e801af82c7108abbcb736a8bc5c14/dotnet-runtime-6.0.0-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/b9cfdb9e-d5cd-4024-b318-00390b729d2f/65690f2440f40654898020cdfffa1050/dotnet-runtime-6.0.0-win-x64.exe'),
+      '.NET Runtime 7.0' + GetArchitectureTitle,
+      GetString('https://download.visualstudio.microsoft.com/download/pr/75c0d7c7-9f30-46fd-9675-a301f0e051f4/ec04d5cc40aa6537a4af21fad6bf8ba9/dotnet-runtime-7.0.0-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/87bc5966-97cc-498c-8381-bff4c43aafc6/baca88b989e7d2871e989d33a667d8e9/dotnet-runtime-7.0.0-win-x64.exe'),
       '', False, False, False);
   end;
 #endif
 
-#ifdef UseDotNet60Desktop
-  // https://dotnet.microsoft.com/download/dotnet/6.0
-  if not IsNetCoreInstalled('Microsoft.WindowsDesktop.App 6.0.0') then begin
-    AddDependency('dotnet60desktop' + GetArchitectureSuffix + '.exe',
+#ifdef UseDotNet70Desktop
+  // https://dotnet.microsoft.com/download/dotnet/7.0
+  if not IsNetCoreInstalled('Microsoft.WindowsDesktop.App 7.0.0') then begin
+    AddDependency('dotnet70desktop' + GetArchitectureSuffix + '.exe',
       '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
-      '.NET Desktop Runtime 6.0' + GetArchitectureTitle,
-      GetString('https://download.visualstudio.microsoft.com/download/pr/a1ca7d0d-ce01-4878-b952-3fa1e6d9a7c6/e386db367490b631b8c013a9fb0f3794/windowsdesktop-runtime-6.0.0-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/a865ccae-2219-4184-bcd6-0178dc580589/ba452d37e8396b7a49a9adc0e1a07e87/windowsdesktop-runtime-6.0.0-win-x64.exe'),
+      '.NET Desktop Runtime 7.0' + GetArchitectureTitle,
+      GetString('https://download.visualstudio.microsoft.com/download/pr/d05a833c-2cf9-4d06-89ae-a0f3e10c5c91/c668ff42e23c2f67aa3d80227860585f/windowsdesktop-runtime-7.0.0-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/5b2fbe00-507e-450e-8b52-43ab052aadf2/79d54c3a19ce3fce314f2367cf4e3b21/windowsdesktop-runtime-7.0.0-win-x64.exe'),
       '', False, False, False);
   end;
 #endif

+ 14 - 14
src/Installer/installer-setup-x86-light.iss

@@ -4,8 +4,8 @@
 // requires netcorecheck.exe and netcorecheck_x64.exe (see download link below)
 #define UseNetCoreCheck
 #ifdef UseNetCoreCheck
-  ;#define UseDotNet60
-  #define UseDotNet60Desktop
+  ;#define UseDotNet70
+  #define UseDotNet70Desktop
 #endif
 
 // custom setup info
@@ -400,24 +400,24 @@ var
   Version: String;
 begin
 
-#ifdef UseDotNet60
-  // https://dotnet.microsoft.com/download/dotnet/6.0
-  if not IsNetCoreInstalled('Microsoft.NETCore.App 6.0.0') then begin
-    AddDependency('dotnet60' + GetArchitectureSuffix + '.exe',
+#ifdef UseDotNet70
+  // https://dotnet.microsoft.com/download/dotnet/7.0
+  if not IsNetCoreInstalled('Microsoft.NETCore.App 7.0.0') then begin
+    AddDependency('dotnet70' + GetArchitectureSuffix + '.exe',
       '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
-      '.NET Runtime 6.0' + GetArchitectureTitle,
-      GetString('https://download.visualstudio.microsoft.com/download/pr/34df41d5-c813-4e30-8aa3-3603ce6600c0/976e801af82c7108abbcb736a8bc5c14/dotnet-runtime-6.0.0-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/b9cfdb9e-d5cd-4024-b318-00390b729d2f/65690f2440f40654898020cdfffa1050/dotnet-runtime-6.0.0-win-x64.exe'),
+      '.NET Runtime 7.0' + GetArchitectureTitle,
+      GetString('https://download.visualstudio.microsoft.com/download/pr/75c0d7c7-9f30-46fd-9675-a301f0e051f4/ec04d5cc40aa6537a4af21fad6bf8ba9/dotnet-runtime-7.0.0-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/87bc5966-97cc-498c-8381-bff4c43aafc6/baca88b989e7d2871e989d33a667d8e9/dotnet-runtime-7.0.0-win-x64.exe'),
       '', False, False, False);
   end;
 #endif
 
-#ifdef UseDotNet60Desktop
-  // https://dotnet.microsoft.com/download/dotnet/6.0
-  if not IsNetCoreInstalled('Microsoft.WindowsDesktop.App 6.0.0') then begin
-    AddDependency('dotnet60desktop' + GetArchitectureSuffix + '.exe',
+#ifdef UseDotNet70Desktop
+  // https://dotnet.microsoft.com/download/dotnet/7.0
+  if not IsNetCoreInstalled('Microsoft.WindowsDesktop.App 7.0.0') then begin
+    AddDependency('dotnet70desktop' + GetArchitectureSuffix + '.exe',
       '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart',
-      '.NET Desktop Runtime 6.0' + GetArchitectureTitle,
-      GetString('https://download.visualstudio.microsoft.com/download/pr/a1ca7d0d-ce01-4878-b952-3fa1e6d9a7c6/e386db367490b631b8c013a9fb0f3794/windowsdesktop-runtime-6.0.0-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/a865ccae-2219-4184-bcd6-0178dc580589/ba452d37e8396b7a49a9adc0e1a07e87/windowsdesktop-runtime-6.0.0-win-x64.exe'),
+      '.NET Desktop Runtime 7.0' + GetArchitectureTitle,
+      GetString('https://download.visualstudio.microsoft.com/download/pr/d05a833c-2cf9-4d06-89ae-a0f3e10c5c91/c668ff42e23c2f67aa3d80227860585f/windowsdesktop-runtime-7.0.0-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/5b2fbe00-507e-450e-8b52-43ab052aadf2/79d54c3a19ce3fce314f2367cf4e3b21/windowsdesktop-runtime-7.0.0-win-x64.exe'),
       '', False, False, False);
   end;
 #endif

+ 8 - 2
src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFillHelper.cs

@@ -69,8 +69,10 @@ public static class FloodFillHelper
         // once the chunk is filled all places where it spills over to neighboring chunks are saved in the stack
         Stack<(VecI chunkPos, VecI posOnChunk)> positionsToFloodFill = new();
         positionsToFloodFill.Push((initChunkPos, initPosOnChunk));
+        int iter = -1;
         while (positionsToFloodFill.Count > 0)
         {
+            iter++;
             var (chunkPos, posOnChunk) = positionsToFloodFill.Pop();
 
             if (!drawingChunks.ContainsKey(chunkPos))
@@ -116,7 +118,8 @@ public static class FloodFillHelper
                 uLongColor,
                 drawingColor,
                 posOnChunk,
-                colorRange);
+                colorRange,
+                iter != 0);
 
             if (maybeArray is null)
                 continue;
@@ -145,10 +148,13 @@ public static class FloodFillHelper
         ulong colorBits,
         Color color,
         VecI pos,
-        ColorBounds bounds)
+        ColorBounds bounds,
+        bool checkFirstPixel)
     {
         if (referenceChunk.Surface.GetSRGBPixel(pos) == color || drawingChunk.Surface.GetSRGBPixel(pos) == color)
             return null;
+        if (checkFirstPixel && !bounds.IsWithinBounds(referenceChunk.Surface.GetSRGBPixel(pos)))
+            return null;
 
         byte[] pixelStates = new byte[chunkSize * chunkSize];
         DrawSelection(pixelStates, selection, globalSelectionBounds, chunkPos, chunkSize);

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/Drawing/FloodFill/FloodFill_Change.cs

@@ -25,7 +25,7 @@ internal class FloodFill_Change : Change
 
     public override bool InitializeAndValidate(Document target)
     {
-        if (pos.X < 0 || pos.Y < 0 || pos.X >= target.Size.X || pos.Y >= target.Size.X)
+        if (pos.X < 0 || pos.Y < 0 || pos.X >= target.Size.X || pos.Y >= target.Size.Y)
             return false;
         
         return DrawingChangeHelper.IsValidForDrawing(target, memberGuid, drawOnMask);

+ 5 - 0
src/PixiEditor.ChangeableDocument/Changes/Drawing/TransformSelectedArea_UpdateableChange.cs

@@ -163,6 +163,11 @@ internal class TransformSelectedArea_UpdateableChange : UpdateableChange
             var chunks = DrawingChangeHelper.ApplyStoredChunksDisposeAndSetToNull(target, guid, drawOnMask, ref storageCopy);
             infos.Add(DrawingChangeHelper.CreateChunkChangeInfo(guid, chunks, drawOnMask).AsT1);
         }
+
+        (var toDispose, target.Selection.SelectionPath) = (target.Selection.SelectionPath, new VectorPath(originalPath!));
+        toDispose.Dispose();
+        infos.Add(new Selection_ChangeInfo(new VectorPath(target.Selection.SelectionPath)));
+
         savedChunks = null;
         return infos;
     }

+ 1 - 1
src/PixiEditor.ChangeableDocument/Changes/Root/CenterContent_Change.cs

@@ -41,7 +41,7 @@ internal class CenterContent_Change : Change
         foreach (var layerGuid in _affectedLayers)
         {
             Layer layer = document.FindMemberOrThrow<Layer>(layerGuid);
-            RectI? tightBounds = layer.LayerImage.FindPreciseBounds();
+            RectI? tightBounds = layer.LayerImage.FindPreciseCommittedBounds();
             if (tightBounds.HasValue)
             {
                 currentBounds = currentBounds.HasValue ? currentBounds.Value.Union(tightBounds.Value) : tightBounds;

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

@@ -5,12 +5,8 @@ namespace PixiEditor.ChangeableDocument.Changes.Root;
 
 internal class ClipCanvas_Change : ResizeBasedChangeBase
 {
-
     [GenerateMakeChangeAction]
-    public ClipCanvas_Change()
-    {
-        
-    }
+    public ClipCanvas_Change() { }
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
     {
@@ -19,7 +15,7 @@ internal class ClipCanvas_Change : ResizeBasedChangeBase
         {
             if (member is Layer layer)
             {
-                var layerBounds = layer.LayerImage.FindPreciseBounds();
+                var layerBounds = layer.LayerImage.FindPreciseCommittedBounds();
                 if (layerBounds.HasValue)
                 {
                     bounds ??= layerBounds.Value;

+ 3 - 0
src/PixiEditor.ChangeableDocument/Changes/Root/ResizeBasedChangeBase.cs

@@ -31,6 +31,9 @@ internal abstract class ResizeBasedChangeBase : Change
     
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Revert(Document target)
     {
+        if (target.Size == _originalSize)
+            return new None();
+
         target.Size = _originalSize;
         target.ForEveryMember((member) =>
         {

+ 7 - 1
src/PixiEditor.ChangeableDocument/Changes/Root/ResizeCanvas_Change.cs

@@ -18,7 +18,7 @@ internal class ResizeCanvas_Change : ResizeBasedChangeBase
 
     public override bool InitializeAndValidate(Document target)
     {
-        if (target.Size == newSize || newSize.X < 1 || newSize.Y < 1)
+        if (newSize.X < 1 || newSize.Y < 1)
             return false;
         
         return base.InitializeAndValidate(target);
@@ -26,6 +26,12 @@ internal class ResizeCanvas_Change : ResizeBasedChangeBase
 
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
     {
+        if (_originalSize == newSize)
+        {
+            ignoreInUndo = true;
+            return new None();
+        }
+
         target.Size = newSize;
         target.VerticalSymmetryAxisX = Math.Clamp(_originalVerAxisX, 0, target.Size.X);
         target.HorizontalSymmetryAxisY = Math.Clamp(_originalHorAxisY, 0, target.Size.Y);

+ 7 - 1
src/PixiEditor.ChangeableDocument/Changes/Root/ResizeImage_Change.cs

@@ -27,7 +27,7 @@ internal class ResizeImage_Change : Change
     
     public override bool InitializeAndValidate(Document target)
     {
-        if (target.Size == newSize || newSize.X < 1 || newSize.Y < 1)
+        if (newSize.X < 1 || newSize.Y < 1)
             return false;
         
         originalSize = target.Size;
@@ -76,6 +76,12 @@ internal class ResizeImage_Change : Change
     
     public override OneOf<None, IChangeInfo, List<IChangeInfo>> Apply(Document target, bool firstApply, out bool ignoreInUndo)
     {
+        if (originalSize == newSize)
+        {
+            ignoreInUndo = true;
+            return new None();
+        }
+
         target.Size = newSize;
         target.VerticalSymmetryAxisX = Math.Clamp(originalVerAxisX, 0, target.Size.X);
         target.HorizontalSymmetryAxisY = Math.Clamp(originalHorAxisY, 0, target.Size.Y);

+ 46 - 38
src/PixiEditor.ChangeableDocument/Changes/Selection/MagicWand/MagicWandHelper.cs

@@ -14,19 +14,19 @@ internal class MagicWandHelper
     private static readonly VecI Left = new VecI(-1, 0);
     private static readonly VecI Right = new VecI(1, 0);
 
-    private static MagicWandVisualizer visualizer = new MagicWandVisualizer(Path.Combine("Debugging", "MagicWand"));
+    //private static MagicWandVisualizer visualizer = new MagicWandVisualizer(Path.Combine("Debugging", "MagicWand"));
 
     private class UnvisitedStack
     {
         private int chunkSize;
-        private readonly VecI imageSizeInChunks;
+        private readonly VecI imageSize;
         private Stack<(VecI chunkPos, VecI posOnChunk)> likelyUnvisited = new();
         private HashSet<VecI> certainlyVisited = new();
 
-        public UnvisitedStack(int chunkSize, VecI imageSizeInChunks)
+        public UnvisitedStack(int chunkSize, VecI imageSize)
         {
             this.chunkSize = chunkSize;
-            this.imageSizeInChunks = imageSizeInChunks;
+            this.imageSize = imageSize;
         }
 
         public void PushAll(VecI chunkPos)
@@ -34,21 +34,27 @@ internal class MagicWandHelper
             VecI chunkOffset = chunkPos * chunkSize;
             for (int i = 0; i < chunkSize; i++)
             {
-                if (chunkPos.Y > 0)
-                    likelyUnvisited.Push((new(chunkPos.X, chunkPos.Y - 1), new(i, chunkSize - 1)));
-                certainlyVisited.Add(chunkOffset + new VecI(i, 0));
-
-                if (chunkPos.Y < imageSizeInChunks.Y - 1)
-                    likelyUnvisited.Push((new(chunkPos.X, chunkPos.Y + 1), new(i, 0)));
-                certainlyVisited.Add(chunkOffset + new VecI(i, chunkSize - 1));
-
-                if (chunkPos.X > 0)
-                    likelyUnvisited.Push((new(chunkPos.X - 1, chunkPos.Y), new(chunkSize - 1, i)));
-                certainlyVisited.Add(chunkOffset + new VecI(0, i));
+                // separated into a function to prevent stackalloc stackoverflow
+                PushArrayIteration(i);
+            }
+            void PushArrayIteration(int i)
+            {
+                Span<(VecI, VecI, VecI)> options = stackalloc (VecI, VecI, VecI)[]
+                {
+                    (new(chunkPos.X, chunkPos.Y - 1), new(i, chunkSize - 1), new(i, 0)), // Top
+                    (new(chunkPos.X, chunkPos.Y + 1), new(i, 0), new(i, chunkSize - 1)), // Bottom
+                    (new(chunkPos.X - 1, chunkPos.Y), new(chunkSize - 1, i), new(0, i)), // Left
+                    (new(chunkPos.X + 1, chunkPos.Y), new(0, i), new(chunkSize - 1, i)) // Right
+                };
 
-                if (chunkPos.X < imageSizeInChunks.X - 1)
-                    likelyUnvisited.Push((new(chunkPos.X + 1, chunkPos.Y), new(0, i)));
-                certainlyVisited.Add(chunkOffset + new VecI(chunkSize - 1, i));
+                foreach (var (otherChunkPos, otherPosInChunk, refPos) in options)
+                {
+                    VecI global = otherChunkPos * chunkSize + otherPosInChunk;
+                    if (global.X < 0 || global.Y < 0 || global.X >= imageSize.X || global.Y >= imageSize.Y)
+                        continue;
+                    likelyUnvisited.Push((otherChunkPos, otherPosInChunk));
+                    certainlyVisited.Add(chunkOffset + refPos);
+                }
             }
         }
 
@@ -62,25 +68,27 @@ internal class MagicWandHelper
             VecI chunkOffset = chunkPos * chunkSize;
             for (int i = 0; i < chunkSize; i++)
             {
-                if (chunkPos.Y > 0 && visitedArray[i]) //Top
-                    likelyUnvisited.Push((new(chunkPos.X, chunkPos.Y - 1), new(i, chunkSize - 1)));
-                if (visitedArray[i])
-                    certainlyVisited.Add(chunkOffset + new VecI(i, 0));
-
-                if (chunkPos.Y < imageSizeInChunks.Y - 1 && visitedArray[chunkSize * (chunkSize - 1) + i]) // Bottom
-                    likelyUnvisited.Push((new(chunkPos.X, chunkPos.Y + 1), new(i, 0)));
-                if (visitedArray[chunkSize * (chunkSize - 1) + i])
-                    certainlyVisited.Add(chunkOffset + new VecI(i, chunkSize - 1));
-
-                if (chunkPos.X > 0 && visitedArray[i * chunkSize]) // Left
-                    likelyUnvisited.Push((new(chunkPos.X - 1, chunkPos.Y), new(chunkSize - 1, i)));
-                if (visitedArray[i * chunkSize])
-                    certainlyVisited.Add(chunkOffset + new VecI(0, i));
-
-                if (chunkPos.X < imageSizeInChunks.X - 1 && visitedArray[i * chunkSize + (chunkSize - 1)]) // Right
-                    likelyUnvisited.Push((new(chunkPos.X + 1, chunkPos.Y), new(0, i)));
-                if (visitedArray[i * chunkSize + (chunkSize - 1)])
-                    certainlyVisited.Add(chunkOffset + new VecI(chunkSize - 1, i));
+                // separated into a function to prevent stackalloc stackoverflow
+                PushArrayIteration(i);
+            }
+            void PushArrayIteration(int i)
+            {
+                Span<(int, VecI, VecI, VecI)> options = stackalloc (int, VecI, VecI, VecI)[]
+                {
+                    (i, new(chunkPos.X, chunkPos.Y - 1), new(i, chunkSize - 1), new(i, 0)), // Top
+                    (chunkSize * (chunkSize - 1) + i, new(chunkPos.X, chunkPos.Y + 1), new(i, 0), new(i, chunkSize - 1)), // Bottom
+                    (i * chunkSize, new(chunkPos.X - 1, chunkPos.Y), new(chunkSize - 1, i), new(0, i)), // Left
+                    (i * chunkSize + (chunkSize - 1), new(chunkPos.X + 1, chunkPos.Y), new(0, i), new(chunkSize - 1, i)) // Right
+                };
+
+                foreach (var (refIndex, otherChunkPos, otherPosInChunk, refPos) in options)
+                {
+                    VecI otherGlobal = otherChunkPos * chunkSize + otherPosInChunk;
+                    if (!visitedArray[refIndex] || otherGlobal.X < 0 || otherGlobal.Y < 0 || otherGlobal.X >= imageSize.X || otherGlobal.Y >= imageSize.Y)
+                        continue;
+                    certainlyVisited.Add(chunkOffset + refPos);
+                    likelyUnvisited.Push((otherChunkPos, otherPosInChunk));
+                }
             }
         }
 
@@ -122,7 +130,7 @@ internal class MagicWandHelper
 
         HashSet<VecI> processedEmptyChunks = new();
 
-        UnvisitedStack positionsToFloodFill = new(chunkSize, imageSizeInChunks);
+        UnvisitedStack positionsToFloodFill = new(chunkSize, document.Size);
 
         Lines lines = new();
         VectorPath selection = new();

+ 2 - 2
src/PixiEditor.ChangeableDocument/Changes/Selection/SelectLasso_UpdateableChange.cs

@@ -33,13 +33,13 @@ internal class SelectLasso_UpdateableChange : UpdateableChange
         var toDispose = target.Selection.SelectionPath;
         if (mode == SelectionMode.New)
         {
-            var copy = new VectorPath(path);
+            var copy = path.PointCount > 2 ? new VectorPath(path) : new VectorPath();
             copy.Close();
             target.Selection.SelectionPath = copy;
         }
         else
         {
-            target.Selection.SelectionPath = originalPath!.Op(path, mode.ToVectorPathOp());
+            target.Selection.SelectionPath = path.PointCount > 2 ? originalPath!.Op(path, mode.ToVectorPathOp()) : new VectorPath(originalPath!);
         }
         toDispose.Dispose();
 

+ 1 - 1
src/PixiEditor.ChangeableDocument/PixiEditor.ChangeableDocument.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net6.0</TargetFramework>
+    <TargetFramework>net7.0</TargetFramework>
     <ImplicitUsings>enable</ImplicitUsings>
     <Nullable>enable</Nullable>
     <WarningsAsErrors>Nullable</WarningsAsErrors>

+ 1 - 1
src/PixiEditor.UpdateInstaller/PixiEditor.UpdateInstaller.csproj

@@ -2,7 +2,7 @@
 
   <PropertyGroup>
     <OutputType>WinExe</OutputType>
-    <TargetFramework>net6.0-windows</TargetFramework>
+    <TargetFramework>net7.0-windows</TargetFramework>
     <UseWPF>true</UseWPF>
     <ApplicationManifest>app.manifest</ApplicationManifest>
     <Platforms>AnyCPU;x64;x86</Platforms>

+ 1 - 1
src/PixiEditor.UpdateModule/PixiEditor.UpdateModule.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net6.0</TargetFramework>
+    <TargetFramework>net7.0</TargetFramework>
     <Platforms>AnyCPU;x64;x86</Platforms>
   </PropertyGroup>
 

+ 1 - 1
src/PixiEditor.Zoombox/PixiEditor.Zoombox.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net6.0-windows</TargetFramework>
+    <TargetFramework>net7.0-windows</TargetFramework>
     <Nullable>enable</Nullable>
     <UseWPF>true</UseWPF>
     <WarningsAsErrors>Nullable</WarningsAsErrors>

+ 1 - 1
src/PixiEditor/Helpers/ChangeInfoListOptimizer.cs

@@ -18,7 +18,7 @@ internal class ChangeInfoListOptimizer
                 continue;
             if (info is Selection_ChangeInfo && !selectionInfoOccured)
                 selectionInfoOccured = true;
-            else if (selectionInfoOccured)
+            else if (info is Selection_ChangeInfo)
                 continue;
             output.Add(info);
         }

+ 0 - 81
src/PixiEditor/Helpers/CoordinatesHelper.cs

@@ -1,81 +0,0 @@
-namespace PixiEditor.Helpers;
-
-internal class CoordinatesHelper
-{
-    /*
-    public static (Coordinates, Coordinates) GetSquareOrLineCoordinates(IReadOnlyList<Coordinates> coords)
-    {
-        if (DoCoordsFormLine(coords))
-        {
-            return GetLineCoordinates(coords);
-        }
-        return GetSquareCoordiantes(coords);
-    }
-
-    private static bool DoCoordsFormLine(IReadOnlyList<Coordinates> coords)
-    {
-        var p1 = coords[0];
-        var p2 = coords[^1];
-        //find delta and mirror to first quadrant
-        float dX = Math.Abs(p2.X - p1.X);
-        float dY = Math.Abs(p2.Y - p1.Y);
-
-        //normalize
-        float length = (float)Math.Sqrt(dX * dX + dY * dY);
-        if (length == 0)
-            return false;
-        dX = dX / length;
-        dY = dY / length;
-
-        return dX < 0.25f || dY < 0.25f; //angle < 15 deg or angle > 75 deg (sin 15 ~= 0.25)
-    }
-
-    public static (Coordinates, Coordinates) GetLineCoordinates(IReadOnlyList<Coordinates> mouseMoveCords)
-    {
-        int xStart = mouseMoveCords[0].X;
-        int yStart = mouseMoveCords[0].Y;
-
-        int xEnd = mouseMoveCords[^1].X;
-        int yEnd = mouseMoveCords[^1].Y;
-
-
-        if (Math.Abs(xStart - xEnd) > Math.Abs(yStart - yEnd))
-        {
-            yEnd = yStart;
-        }
-        else
-        {
-            xEnd = xStart;
-        }
-        return (new(xStart, yStart), new(xEnd, yEnd));
-    }
-
-    /// <summary>
-    ///     Extracts square from rectangle mouse drag, used to draw symmetric shapes.
-    /// </summary>
-    public static (Coordinates, Coordinates) GetSquareCoordiantes(IReadOnlyList<Coordinates> mouseMoveCords)
-    {
-        var end = mouseMoveCords[^1];
-        var start = mouseMoveCords[0];
-
-        //find delta and mirror to first quadrant
-        var dX = Math.Abs(start.X - end.X);
-        var dY = Math.Abs(start.Y - end.Y);
-
-        float sqrt2 = (float)Math.Sqrt(2);
-        //vector of length 1 at 45 degrees;
-        float diagX, diagY;
-        diagX = diagY = 1 / sqrt2;
-
-        //dot product of delta and diag, returns length of [delta projected onto diag]
-        float projectedLength = diagX * dX + diagY * dY;
-        //project above onto axes
-        float axisLength = projectedLength / sqrt2;
-
-        //final coords
-        float x = -Math.Sign(start.X - end.X) * axisLength;
-        float y = -Math.Sign(start.Y - end.Y) * axisLength;
-        end = new Coordinates((int)x + start.X, (int)y + start.Y);
-        return (start, end);
-    }*/
-}

+ 3 - 3
src/PixiEditor/Helpers/DocumentViewModelBuilder.cs

@@ -122,7 +122,7 @@ internal class DocumentViewModelBuilder : ChildrenBuilder
         private int? width;
         private int? height;
         
-        public SurfaceBuilder Surface { get; set; }
+        public SurfaceBuilder? Surface { get; set; }
 
         public int Width
         {
@@ -180,8 +180,8 @@ internal class DocumentViewModelBuilder : ChildrenBuilder
             {
                 throw new InvalidOperationException("You must first set the width and height of the layer. You can do this by calling WithRect() or setting the Width and Height properties.");
             }
-            
-            var surfaceBuilder = new SurfaceBuilder(new Surface(new VecI(Math.Max(Width, 1), Math.Max(Height, 1))));
+
+            var surfaceBuilder = new SurfaceBuilder(new Surface(new VecI(Width, Height)));
             surface(surfaceBuilder);
             Surface = surfaceBuilder;
             return this;

+ 4 - 5
src/PixiEditor/Helpers/Extensions/PixiParserDocumentEx.cs

@@ -28,10 +28,7 @@ internal static class PixiParserDocumentEx
                 }
                 else if (member is ImageLayer layer)
                 {
-                    /*if (layer.Height > 0 && layer.Width > 0)
-                    {*/
-                        builder.WithLayer(x => BuildLayer(x, layer));
-                    //}
+                    builder.WithLayer(x => BuildLayer(x, layer));
                 }
                 else
                 {
@@ -56,10 +53,12 @@ internal static class PixiParserDocumentEx
                 .WithOpacity(layer.Opacity)
                 .WithBlendMode((PixiEditor.ChangeableDocument.Enums.BlendMode)(int)layer.BlendMode)
                 .WithSize(layer.Width, layer.Height)
-                .WithSurface(x => x.WithImage(layer.ImageBytes, layer.OffsetX, layer.OffsetY))
                 .WithMask(layer.Mask,
                     (x, m) => x.WithVisibility(m.Enabled).WithSurface(m.Width, m.Height,
                         x => x.WithImage(m.ImageBytes, m.OffsetX, m.OffsetY)));
+
+            if (layer.Width > 0 && layer.Height > 0)
+                builder.WithSurface(x => x.WithImage(layer.ImageBytes, layer.OffsetX, layer.OffsetY));
         }
     }
 }

+ 4 - 2
src/PixiEditor/Models/Commands/XAML/Command.cs

@@ -1,4 +1,6 @@
-using System.Windows.Input;
+using System.ComponentModel;
+using System.Windows;
+using System.Windows.Input;
 using System.Windows.Markup;
 using PixiEditor.Helpers;
 
@@ -20,7 +22,7 @@ internal class Command : MarkupExtension
 
     public override object ProvideValue(IServiceProvider serviceProvider)
     {
-        if (Windows.ApplicationModel.DesignMode.DesignModeEnabled)
+        if ((bool)(DesignerProperties.IsInDesignModeProperty.GetMetadata(typeof(DependencyObject)).DefaultValue))
         {
             var attribute = DesignCommandHelpers.GetCommandAttribute(Name);
             return GetICommand(

+ 3 - 3
src/PixiEditor/Models/DocumentModels/ActionAccumulator.cs

@@ -115,7 +115,7 @@ internal class ActionAccumulator
             var renderResult = await renderer.UpdateGatheredChunks(affectedChunks, undoBoundaryPassed);
             
             // lock bitmaps
-            foreach (var (_, bitmap) in document.Bitmaps)
+            foreach (var (_, bitmap) in document.LazyBitmaps)
             {
                 bitmap.Lock();
             }
@@ -126,7 +126,7 @@ internal class ActionAccumulator
             AddDirtyRects(renderResult);
 
             // unlock bitmaps
-            foreach (var (_, bitmap) in document.Bitmaps)
+            foreach (var (_, bitmap) in document.LazyBitmaps)
             {
                 bitmap.Unlock();
             }
@@ -191,7 +191,7 @@ internal class ActionAccumulator
             {
                 case DirtyRect_RenderInfo info:
                     {
-                        var bitmap = document.Bitmaps[info.Resolution];
+                        var bitmap = document.LazyBitmaps[info.Resolution];
                         RectI finalRect = new RectI(VecI.Zero, new(bitmap.PixelWidth, bitmap.PixelHeight));
 
                         RectI dirtyRect = new RectI(info.Pos, info.Size).Intersect(finalRect);

+ 2 - 2
src/PixiEditor/Models/DocumentModels/DocumentUpdater.cs

@@ -304,7 +304,7 @@ internal class DocumentUpdater
             doc.Surfaces[res] = CreateDrawingSurface(newBitmaps[res]);
         }
 
-        doc.Bitmaps = newBitmaps;
+        doc.LazyBitmaps = newBitmaps;
 
         doc.InternalSetSize(info.Size);
         doc.InternalSetVerticalSymmetryAxisX(info.VerticalSymmetryAxisX);
@@ -315,7 +315,7 @@ internal class DocumentUpdater
         doc.PreviewBitmap = CreateBitmap(previewSize);
         doc.PreviewSurface = CreateDrawingSurface(doc.PreviewBitmap);
 
-        doc.RaisePropertyChanged(nameof(doc.Bitmaps));
+        doc.RaisePropertyChanged(nameof(doc.LazyBitmaps));
         doc.RaisePropertyChanged(nameof(doc.PreviewBitmap));
 
         UpdateMemberBitmapsRecursively(doc.StructureRoot, previewSize);

+ 4 - 3
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/LineToolExecutor.cs

@@ -66,13 +66,14 @@ internal class LineToolExecutor : UpdateableChangeExecutor
     {
         if (!started)
         {
-            onEnded(this);
+            onEnded!(this);
             return;
         }
-        transforming = true;
+        
         document!.LineToolOverlayViewModel.LineStart = startPos + new VecD(0.5);
         document!.LineToolOverlayViewModel.LineEnd = curPos + new VecD(0.5);
         document!.LineToolOverlayViewModel.IsEnabled = true;
+        transforming = true;
     }
 
     public override void OnLineOverlayMoved(VecD start, VecD end)
@@ -89,7 +90,7 @@ internal class LineToolExecutor : UpdateableChangeExecutor
 
         document!.LineToolOverlayViewModel.IsEnabled = false;
         internals!.ActionAccumulator.AddFinishedActions(new EndDrawLine_Action());
-        onEnded(this);
+        onEnded!(this);
     }
 
     public override void ForceStop()

+ 84 - 76
src/PixiEditor/Models/IO/Exporter.cs

@@ -6,8 +6,10 @@ using System.Windows.Media.Imaging;
 using ChunkyImageLib;
 using ChunkyImageLib.DataHolders;
 using Microsoft.Win32;
+using PixiEditor.DrawingApi.Core.ColorsImpl;
 using PixiEditor.DrawingApi.Core.Numerics;
 using PixiEditor.DrawingApi.Core.Surface.ImageData;
+using PixiEditor.DrawingApi.Core.Surface.PaintImpl;
 using PixiEditor.Helpers;
 using PixiEditor.Models.Dialogs;
 using PixiEditor.Models.Enums;
@@ -15,70 +17,102 @@ using PixiEditor.ViewModels.SubViewModels.Document;
 
 namespace PixiEditor.Models.IO;
 
+internal enum DialogSaveResult
+{
+    Success = 0,
+    InvalidPath = 1,
+    ConcurrencyError = 2,
+    UnknownError = 3,
+    Cancelled = 4,
+}
+
+internal enum SaveResult
+{
+    Success = 0,
+    InvalidPath = 1,
+    ConcurrencyError = 2,
+    UnknownError = 3,
+}
+
 internal class Exporter
 {
     /// <summary>
-    ///     Saves document as .pixi file that contains all document data.
+    /// Attempts to save file using a SaveFileDialog
     /// </summary>
-    /// <param name="document">Document to save.</param>
-    /// <param name="path">Path where file was saved.</param>
-    public static bool SaveAsEditableFileWithDialog(DocumentViewModel document, out string path)
+    public static DialogSaveResult TrySaveWithDialog(DocumentViewModel document, out string path, VecI? exportSize = null)
     {
+        path = "";
         SaveFileDialog dialog = new SaveFileDialog
         {
             Filter = SupportedFilesHelper.BuildSaveFilter(true),
             FilterIndex = 0,
             DefaultExt = "pixi"
-
         };
-        if ((bool)dialog.ShowDialog())
-        {
-            FileType filetype = SupportedFilesHelper.GetSaveFileTypeFromFilterIndex(true, dialog.FilterIndex);
-            path = SaveAsEditableFile(document, dialog.FileName, filetype);
-            return true;
-        }
 
-        path = string.Empty;
-        return false;
+        bool? result = dialog.ShowDialog();
+        if (result is null || result == false)
+            return DialogSaveResult.Cancelled;
+
+        var fileType = SupportedFilesHelper.GetSaveFileTypeFromFilterIndex(true, dialog.FilterIndex);
+
+        var saveResult = TrySaveUsingDataFromDialog(document, dialog.FileName, fileType, out string fixedPath, exportSize);
+        if (saveResult == SaveResult.Success)
+            path = fixedPath;
+
+        return (DialogSaveResult)saveResult;
     }
 
     /// <summary>
-    /// Saves editable file to chosen path and returns it.
+    /// Takes data as returned by SaveFileDialog and attempts to use it to save the document
     /// </summary>
-    /// <param name="document">Document to be saved.</param>
-    /// <param name="path">Path where to save file.</param>
-    /// <returns>Path.</returns>
-    public static string SaveAsEditableFile(DocumentViewModel document, string path, FileType requestedType = FileType.Unset)
+    public static SaveResult TrySaveUsingDataFromDialog(DocumentViewModel document, string pathFromDialog, FileType fileTypeFromDialog, out string finalPath, VecI? exportSize = null)
     {
-        var typeFromPath = ParseImageFormat(Path.GetExtension(path));
-        FileType finalType = (typeFromPath, requestedType) switch
-        {
-            (FileType.Unset, FileType.Unset) => FileType.Pixi,
-            (var first, FileType.Unset) => first,
-            (FileType.Unset, var second) => second,
-            _ => typeFromPath,
-        };
+        finalPath = FixFileExtension(pathFromDialog, fileTypeFromDialog);
+        var saveResult = TrySave(document, finalPath, exportSize);
+        if (saveResult != SaveResult.Success)
+            finalPath = "";
 
-        if (typeFromPath == FileType.Unset)
-        {
-            path = AppendExtension(path, SupportedFilesHelper.GetFileTypeDialogData(finalType));
-        }
+        return saveResult;
+    }
 
-        if (finalType != FileType.Pixi)
-        {
-            var bitmap = document.Bitmaps[ChunkResolution.Full];
-            SaveAs(encodersFactory[finalType](), path, bitmap.PixelWidth, bitmap.PixelHeight, bitmap);
-        }
-        else if (Directory.Exists(Path.GetDirectoryName(path)))
+    private static string FixFileExtension(string pathWithOrWithoutExtension, FileType requestedType)
+    {
+        if (requestedType == FileType.Unset)
+            throw new ArgumentException("A valid filetype is required", nameof(requestedType));
+
+        var typeFromPath = SupportedFilesHelper.ParseImageFormat(Path.GetExtension(pathWithOrWithoutExtension));
+        if (typeFromPath != FileType.Unset && typeFromPath == requestedType)
+            return pathWithOrWithoutExtension;
+        return AppendExtension(pathWithOrWithoutExtension, SupportedFilesHelper.GetFileTypeDialogData(requestedType));
+    }
+
+    /// <summary>
+    /// Attempts to save the document into the given location, filetype is inferred from path
+    /// </summary>
+    public static SaveResult TrySave(DocumentViewModel document, string pathWithExtension, VecI? exportSize = null)
+    {
+        string directory = Path.GetDirectoryName(pathWithExtension);
+        if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
+            return SaveResult.InvalidPath;
+
+        var typeFromPath = SupportedFilesHelper.ParseImageFormat(Path.GetExtension(pathWithExtension));
+
+        if (typeFromPath != FileType.Pixi)
         {
-            Parser.PixiParser.Serialize(document.ToSerializable(), path);
+            var maybeBitmap = document.MaybeRenderWholeImage();
+            if (maybeBitmap.IsT0)
+                return SaveResult.ConcurrencyError;
+            var bitmap = maybeBitmap.AsT1;
+
+            if (!TrySaveAs(encodersFactory[typeFromPath](), pathWithExtension, bitmap, exportSize))
+                return SaveResult.UnknownError;
         }
         else
         {
-            SaveAsEditableFileWithDialog(document, out path);
+            Parser.PixiParser.Serialize(document.ToSerializable(), pathWithExtension);
         }
 
-        return path;
+        return SaveResult.Success;
     }
 
     private static string AppendExtension(string path, FileTypeDialogData data)
@@ -91,11 +125,6 @@ internal class Exporter
         return Path.Combine(Path.GetDirectoryName(path), filename);
     }
 
-    public static FileType ParseImageFormat(string extension)
-    {
-        return SupportedFilesHelper.ParseImageFormat(extension);
-    }
-
     static Dictionary<FileType, Func<BitmapEncoder>> encodersFactory = new Dictionary<FileType, Func<BitmapEncoder>>();
 
     static Exporter()
@@ -106,28 +135,6 @@ internal class Exporter
         encodersFactory[FileType.Gif] = () => new GifBitmapEncoder();
     }
 
-    /// <summary>
-    ///     Creates ExportFileDialog to get width, height and path of file.
-    /// </summary>
-    /// <param name="bitmap">Bitmap to be saved as file.</param>
-    /// <param name="fileDimensions">Size of file.</param>
-    public static bool Export(WriteableBitmap bitmap, VecI fileDimensions, out string path)
-    {
-        ExportFileDialog info = new ExportFileDialog(fileDimensions);
-
-        // If OK on dialog has been clicked
-        if (info.ShowDialog())
-        {
-            if (encodersFactory.ContainsKey(info.ChosenFormat))
-                SaveAs(encodersFactory[info.ChosenFormat](), info.FilePath, info.FileWidth, info.FileHeight, bitmap);
-            
-            path = info.FilePath;
-            return true;
-        }
-
-        path = string.Empty;
-        return false;
-    }
     public static void SaveAsGZippedBytes(string path, Surface surface)
     {
         SaveAsGZippedBytes(path, surface, new RectI(VecI.Zero, surface.Size));
@@ -156,25 +163,26 @@ internal class Exporter
     }
 
     /// <summary>
-    ///     Saves image to PNG file.
+    /// Saves image to PNG file. Messes with the passed bitmap.
     /// </summary>
-    /// <param name="encoder">encoder to do the job.</param>
-    /// <param name="savePath">Save file path.</param>
-    /// <param name="exportWidth">File width.</param>
-    /// <param name="exportHeight">File height.</param>
-    /// <param name="bitmap">Bitmap to save.</param>
-    private static void SaveAs(BitmapEncoder encoder, string savePath, int exportWidth, int exportHeight, WriteableBitmap bitmap)
+    private static bool TrySaveAs(BitmapEncoder encoder, string savePath, Surface bitmap, VecI? exportSize)
     {
         try
         {
-            bitmap = bitmap.Resize(exportWidth, exportHeight, WriteableBitmapExtensions.Interpolation.NearestNeighbor);
+            if (exportSize is not null && exportSize != bitmap.Size)
+                bitmap = bitmap.ResizeNearestNeighbor((VecI)exportSize);
+
+            if (encoder is (JpegBitmapEncoder or BmpBitmapEncoder))
+                bitmap.DrawingSurface.Canvas.DrawColor(Colors.White, DrawingApi.Core.Surface.BlendMode.Multiply);
+
             using var stream = new FileStream(savePath, FileMode.Create);
-            encoder.Frames.Add(BitmapFrame.Create(bitmap));
+            encoder.Frames.Add(BitmapFrame.Create(bitmap.ToWriteableBitmap()));
             encoder.Save(stream);
         }
         catch (Exception err)
         {
-            NoticeDialog.Show(err.ToString(), "Error");
+            return false;
         }
+        return true;
     }
 }

+ 5 - 1
src/PixiEditor/Models/UserPreferences/PreferencesConstants.cs

@@ -1,6 +1,10 @@
-namespace PixiEditor.Models.UserPreferences;
+namespace PixiEditor.Models.UserPreferences;
 
 internal static class PreferencesConstants
 {
     public const string FavouritePalettes = "FavouritePalettes";
+    public const string RecentlyOpened = "RecentlyOpened";
+
+    public const string MaxOpenedRecently = "MaxOpenedRecently";
+    public const int MaxOpenedRecentlyDefault = 8;
 }

+ 2 - 2
src/PixiEditor/PixiEditor.csproj

@@ -2,7 +2,7 @@
 
 	<PropertyGroup>
 		<OutputType>WinExe</OutputType>
-		<TargetFramework>net6.0-windows10.0.22000.0</TargetFramework>
+		<TargetFramework>net7.0-windows</TargetFramework>
 		<UseWPF>true</UseWPF>
 		<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
 		<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
@@ -16,9 +16,9 @@
 		<Authors>Krzysztof Krysiński, Egor Mozgovoy, CPK</Authors>
 		<Configurations>Debug;Release;MSIX;MSIX Debug;Dev Release</Configurations>
 		<Platforms>AnyCPU;x64;x86</Platforms>
-		<SupportedOSPlatformVersion>7.0</SupportedOSPlatformVersion>
         <ImplicitUsings>true</ImplicitUsings>
         <AssemblyVersion></AssemblyVersion>
+        <LangVersion>11</LangVersion>
 	</PropertyGroup>
 
 	<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='MSIX|AnyCPU'">

+ 2 - 2
src/PixiEditor/Properties/AssemblyInfo.cs

@@ -50,5 +50,5 @@ using System.Windows;
 // You can specify all the values or you can default the Build and Revision Numbers
 // by using the '*' as shown below:
 // [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("0.1.9.2")]
-[assembly: AssemblyFileVersion("0.1.9.2")]
+[assembly: AssemblyVersion("0.1.9.3")]
+[assembly: AssemblyFileVersion("0.1.9.3")]

+ 2 - 2
src/PixiEditor/ViewModels/SaveFilePopupViewModel.cs

@@ -65,7 +65,7 @@ internal class SaveFilePopupViewModel : ViewModelBase
         {
             if (string.IsNullOrEmpty(path.FileName) == false)
             {
-                ChosenFormat = Exporter.ParseImageFormat(Path.GetExtension(path.SafeFileName));
+                ChosenFormat = SupportedFilesHelper.ParseImageFormat(Path.GetExtension(path.SafeFileName));
                 return path.FileName;
             }
         }
@@ -93,4 +93,4 @@ internal class SaveFilePopupViewModel : ViewModelBase
         ((Window)parameter).DialogResult = true;
         CloseButton(parameter);
     }
-}
+}

+ 28 - 3
src/PixiEditor/ViewModels/SubViewModels/Document/DocumentViewModel.cs

@@ -123,7 +123,7 @@ internal partial class DocumentViewModel : NotifyableObject
     public StructureMemberViewModel? SelectedStructureMember { get; private set; } = null;
 
     public Dictionary<ChunkResolution, DrawingSurface> Surfaces { get; set; } = new();
-    public Dictionary<ChunkResolution, WriteableBitmap> Bitmaps { get; set; } = new()
+    public Dictionary<ChunkResolution, WriteableBitmap> LazyBitmaps { get; set; } = new()
     {
         [ChunkResolution.Full] = new WriteableBitmap(64, 64, 96, 96, PixelFormats.Pbgra32, null),
         [ChunkResolution.Half] = new WriteableBitmap(32, 32, 96, 96, PixelFormats.Pbgra32, null),
@@ -162,7 +162,7 @@ internal partial class DocumentViewModel : NotifyableObject
         LineToolOverlayViewModel = new();
         LineToolOverlayViewModel.LineMoved += (_, args) => Internals.ChangeController.LineOverlayMovedInlet(args.Item1, args.Item2);
 
-        foreach (KeyValuePair<ChunkResolution, WriteableBitmap> bitmap in Bitmaps)
+        foreach (KeyValuePair<ChunkResolution, WriteableBitmap> bitmap in LazyBitmaps)
         {
             DrawingSurface? surface = DrawingSurface.Create(
                 new ImageInfo(bitmap.Value.PixelWidth, bitmap.Value.PixelHeight, ColorType.Bgra8888, AlphaType.Premul, ColorSpace.CreateSrgb()),
@@ -216,7 +216,7 @@ internal partial class DocumentViewModel : NotifyableObject
             if (!member.IsVisible)
                 acc.AddActions(new StructureMemberIsVisible_Action(member.IsVisible, member.GuidValue));
 
-            if (member is DocumentViewModelBuilder.LayerBuilder layer)
+            if (member is DocumentViewModelBuilder.LayerBuilder layer && layer.Surface is not null)
             {
                 PasteImage(member.GuidValue, layer.Surface, layer.Width, layer.Height, layer.OffsetX, layer.OffsetY, false);
             }
@@ -278,6 +278,31 @@ internal partial class DocumentViewModel : NotifyableObject
         RaisePropertyChanged(nameof(AllChangesSaved));
     }
 
+    public OneOf<Error, Surface> MaybeRenderWholeImage()
+    {
+        try
+        {
+            Surface finalSurface = new Surface(SizeBindable);
+            VecI sizeInChunks = (VecI)((VecD)SizeBindable / ChunkyImage.FullChunkSize).Ceiling();
+            for (int i = 0; i < sizeInChunks.X; i++)
+            {
+                for (int j = 0; j < sizeInChunks.Y; j++)
+                {
+                    var maybeChunk = ChunkRenderer.MergeWholeStructure(new(i, j), ChunkResolution.Full, Internals.Tracker.Document.StructureRoot);
+                    if (maybeChunk.IsT1)
+                        continue;
+                    using Chunk chunk = maybeChunk.AsT0;
+                    finalSurface.DrawingSurface.Canvas.DrawSurface(chunk.Surface.DrawingSurface, i * ChunkyImage.FullChunkSize, j * ChunkyImage.FullChunkSize);
+                } 
+            }
+            return finalSurface;
+        }
+        catch (ObjectDisposedException)
+        {
+            return new Error();
+        }
+    }
+
     /// <summary>
     /// Takes the selected area and converts it into a surface
     /// </summary>

+ 67 - 20
src/PixiEditor/ViewModels/SubViewModels/Main/FileViewModel.cs

@@ -1,4 +1,6 @@
-using System.IO;
+using System;
+using System.IO;
+using System.Reflection.Metadata;
 using System.Windows.Input;
 using System.Windows.Shapes;
 using ChunkyImageLib;
@@ -49,7 +51,21 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
             HasRecent = true;
         }
 
-        IPreferences.Current.AddCallback("MaxOpenedRecently", UpdateMaxRecentlyOpened);
+        IPreferences.Current.AddCallback(PreferencesConstants.MaxOpenedRecently, UpdateMaxRecentlyOpened);
+    }
+
+    public void AddRecentlyOpened(string path)
+    {
+        if (RecentlyOpened.Contains(path))
+            return;
+        
+        RecentlyOpened.Insert(0, path);
+        int maxCount = IPreferences.Current.GetPreference<int>(PreferencesConstants.MaxOpenedRecently, PreferencesConstants.MaxOpenedRecentlyDefault);
+        while (RecentlyOpened.Count > maxCount)
+        {
+            RecentlyOpened.RemoveAt(RecentlyOpened.Count - 1);
+        }
+        IPreferences.Current.UpdateLocalPreference(PreferencesConstants.RecentlyOpened, RecentlyOpened.Select(x => x.FilePath));
     }
 
     public void RemoveRecentlyOpened(object parameter)
@@ -91,7 +107,7 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
         {
             NoticeDialog.Show("The file does not exist", "Failed to open the file");
             RecentlyOpened.Remove(path);
-            IPreferences.Current.UpdateLocalPreference("RecentlyOpened", RecentlyOpened.Select(x => x.FilePath));
+            IPreferences.Current.UpdateLocalPreference(PreferencesConstants.RecentlyOpened, RecentlyOpened.Select(x => x.FilePath));
             return;
         }
 
@@ -165,6 +181,7 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
     {
         DocumentViewModel document = Importer.ImportDocument(path);
         AddDocumentViewModelToTheSystem(document);
+        AddRecentlyOpened(document.FullFilePath);
     }
 
     /// <summary>
@@ -198,6 +215,7 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
                     .WithSize(dialog.FileWidth, dialog.FileHeight)
                     .WithSurface(Importer.ImportImage(dialog.FilePath, new VecI(dialog.FileWidth, dialog.FileHeight)))));
             doc.FullFilePath = path;
+            AddRecentlyOpened(path);
         }
     }
 
@@ -241,25 +259,34 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
 
     public bool SaveDocument(DocumentViewModel document, bool asNew)
     {
-        string path = "";
-        bool success = false;
+        string finalPath = null;
         if (asNew || string.IsNullOrEmpty(document.FullFilePath))
         {
-            success = Exporter.SaveAsEditableFileWithDialog(document, out path);
+            var result = Exporter.TrySaveWithDialog(document, out string path);
+            if (result == DialogSaveResult.Cancelled)
+                return false;
+            if (result != DialogSaveResult.Success)
+            {
+                ShowSaveError(result);
+                return false;
+            }
+            finalPath = path;
+            AddRecentlyOpened(path);
         }
         else
         {
-            path = Exporter.SaveAsEditableFile(document, document.FullFilePath);
-            success = path != null;
-        }
-
-        if (success)
-        {
-            document.FullFilePath = path;
-            document.MarkAsSaved();
+            var result = Exporter.TrySave(document, document.FullFilePath);
+            if (result != SaveResult.Success)
+            {
+                ShowSaveError((DialogSaveResult)result);
+                return false;
+            }
+            finalPath = document.FullFilePath;
         }
 
-        return success;
+        document.FullFilePath = finalPath;
+        document.MarkAsSaved();
+        return true;
     }
 
     /// <summary>
@@ -269,15 +296,35 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
     [Command.Basic("PixiEditor.File.Export", "Export", "Export image", CanExecute = "PixiEditor.HasDocument", Key = Key.S, Modifiers = ModifierKeys.Control | ModifierKeys.Alt | ModifierKeys.Shift)]
     public void ExportFile()
     {
-        ViewModelMain.Current.ActionDisplay = "";
         DocumentViewModel doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
         if (doc is null)
             return;
+        ViewModelMain.Current.ActionDisplay = "";
+
+        ExportFileDialog info = new ExportFileDialog(doc.SizeBindable);
+        if (info.ShowDialog())
+        {
+            SaveResult result = Exporter.TrySaveUsingDataFromDialog(doc, info.FilePath, info.ChosenFormat, out string finalPath, new(info.FileWidth, info.FileHeight));
+            if (result == SaveResult.Success)
+                ProcessHelper.OpenInExplorer(finalPath);
+            else
+                ShowSaveError((DialogSaveResult)result);
+        }
+    }
 
-        var bitmap = doc.Bitmaps[ChunkResolution.Full];
-        if (Exporter.Export(bitmap, new VecI(bitmap.PixelWidth, bitmap.PixelHeight), out string path))
+    private void ShowSaveError(DialogSaveResult result)
+    {
+        switch (result)
         {
-            ProcessHelper.OpenInExplorer(path);
+            case DialogSaveResult.InvalidPath:
+                NoticeDialog.Show("Error", "Couldn't save the file to the specified location");
+                break;
+            case DialogSaveResult.ConcurrencyError:
+                NoticeDialog.Show("Internal error", "An internal error occured while saving. Please try again.");
+                break;
+            case DialogSaveResult.UnknownError:
+                NoticeDialog.Show("Error", "An error occured while saving.");
+                break;
         }
     }
 
@@ -303,7 +350,7 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
     private List<RecentlyOpenedDocument> GetRecentlyOpenedDocuments()
     {
         IEnumerable<string> paths = IPreferences.Current.GetLocalPreference(nameof(RecentlyOpened), new JArray()).ToObject<string[]>()
-            .Take(IPreferences.Current.GetPreference("MaxOpenedRecently", 8));
+            .Take(IPreferences.Current.GetPreference(PreferencesConstants.MaxOpenedRecently, 8));
 
         List<RecentlyOpenedDocument> documents = new List<RecentlyOpenedDocument>();
 

+ 2 - 9
src/PixiEditor/ViewModels/SubViewModels/Main/StylusViewModel.cs

@@ -56,14 +56,7 @@ internal class StylusViewModel : SubViewModel<ViewModelMain>
 
     private void UpdateUseTouchGesture()
     {
-        if (Owner.ToolsSubViewModel.ActiveTool is not (MoveViewportToolViewModel or ZoomToolViewModel))
-        {
-            UseTouchGestures = IsPenModeEnabled;
-        }
-        else
-        {
-            UseTouchGestures = true;
-        }
+        UseTouchGestures = Owner.ToolsSubViewModel.ActiveTool is MoveViewportToolViewModel or ZoomToolViewModel || IsPenModeEnabled;
     }
 
     [Command.Internal("PixiEditor.Stylus.StylusOutOfRange")]
@@ -75,7 +68,7 @@ internal class StylusViewModel : SubViewModel<ViewModelMain>
     [Command.Internal("PixiEditor.Stylus.StylusSystemGesture")]
     public void StylusSystemGesture(StylusSystemGestureEventArgs e)
     {
-        if (e.SystemGesture == SystemGesture.Drag || e.SystemGesture == SystemGesture.Tap)
+        if (e.SystemGesture is SystemGesture.Drag or SystemGesture.Tap)
         {
             return;
         }

+ 1 - 1
src/PixiEditor/ViewModels/SubViewModels/Main/WindowViewModel.cs

@@ -17,7 +17,7 @@ internal class WindowViewModel : SubViewModel<ViewModelMain>
 {
     private CommandController commandController;
     private ShortcutPopup? shortcutPopup;
-    private ShortcutPopup ShortcutPopup => shortcutPopup ?? (shortcutPopup = new(commandController));
+    private ShortcutPopup ShortcutPopup => shortcutPopup ??= new(commandController);
 
     private AboutPopup? _aboutPopup;
     private AboutPopup AboutPopup => _aboutPopup ??= new();

+ 2 - 1
src/PixiEditor/ViewModels/SubViewModels/UserPreferences/Settings/FileSettings.cs

@@ -1,4 +1,5 @@
 using PixiEditor.Models;
+using PixiEditor.Models.UserPreferences;
 
 namespace PixiEditor.ViewModels.SubViewModels.UserPreferences.Settings;
 
@@ -38,7 +39,7 @@ internal class FileSettings : SettingsGroup
         }
     }
 
-    private int maxOpenedRecently = GetPreference(nameof(MaxOpenedRecently), 8);
+    private int maxOpenedRecently = GetPreference(PreferencesConstants.MaxOpenedRecently, PreferencesConstants.MaxOpenedRecentlyDefault);
 
     public int MaxOpenedRecently
     {

+ 51 - 16
src/PixiEditor/Views/Dialogs/AboutPopup.xaml

@@ -10,7 +10,7 @@
         xmlns:userControls="clr-namespace:PixiEditor.Views.UserControls"
         mc:Ignorable="d" WindowStyle="None"
         Title="About" WindowStartupLocation="CenterScreen"
-        Height="490" Width="400" Name="aboutPopup" MinWidth="100" MinHeight="100">
+        Height="510" Width="400" Name="aboutPopup" MinWidth="100" MinHeight="100">
     <Window.CommandBindings>
         <CommandBinding Command="{x:Static SystemCommands.CloseWindowCommand}" CanExecute="CommandBinding_CanExecute"
                         Executed="CommandBinding_Executed_Close" />
@@ -42,9 +42,12 @@
                         <ImageBrush ImageSource="/Images/SocialMedia/Avatars/flabbet.png"/>
                     </Ellipse.Fill>
                 </Ellipse>
-                <Button Content="Krzysztof Krysiński (flabbet)" VerticalAlignment="Center" Style="{StaticResource HyperlinkTextButton}"
-                        Margin="10 0 0 0" FontSize="14"
-                        Command="{cmds:Command PixiEditor.Links.OpenHyperlink, UseProvided=True}" CommandParameter="https://github.com/flabbet"/>
+                <Label Style="{StaticResource SettingsText}" Margin="10 0 0 0" FontSize="14">
+                    <Hyperlink Command="{cmds:Command PixiEditor.Links.OpenHyperlink, UseProvided=True}" CommandParameter="https://github.com/flabbet" Style="{StaticResource SettingsLink}">
+                        <Run Text="Krzysztof Krysiński (flabbet)"/>
+                        <Run Text="" FontFamily="{StaticResource Feather}"/>
+                    </Hyperlink>
+                </Label>
             </StackPanel>
             <StackPanel Orientation="Horizontal" Margin="20 5">
                 <Ellipse Width="32" Height="32">
@@ -52,8 +55,12 @@
                         <ImageBrush ImageSource="/Images/SocialMedia/Avatars/Equbuxu.png"/>
                     </Ellipse.Fill>
                 </Ellipse>
-                <Button Content="Egor Mozgovoy (Equbuxu)" Style="{StaticResource HyperlinkTextButton}" VerticalAlignment="Center" Margin="10 0 0 0" FontSize="14"
-                        Command="{cmds:Command PixiEditor.Links.OpenHyperlink, UseProvided=True}" CommandParameter="https://github.com/equbuxu"/>
+                <Label Style="{StaticResource SettingsText}"  Margin="10 0 0 0" FontSize="14">
+                    <Hyperlink Command="{cmds:Command PixiEditor.Links.OpenHyperlink, UseProvided=True}" CommandParameter="https://github.com/equbuxu" Style="{StaticResource SettingsLink}">
+                        <Run Text="Egor Mozgovoy (Equbuxu)"/>
+                        <Run Text="" FontFamily="{StaticResource Feather}"/>
+                    </Hyperlink>
+                </Label>
             </StackPanel>
             <StackPanel Orientation="Horizontal" Margin="20 0">
                 <Ellipse Width="32" Height="32">
@@ -61,19 +68,47 @@
                         <ImageBrush ImageSource="/Images/SocialMedia/Avatars/CPK.png"/>
                     </Ellipse.Fill>
                 </Ellipse>
-                <Button Style="{StaticResource HyperlinkTextButton}" Content="Philip Kreuz (CPK)" VerticalAlignment="Center" Margin="10 0 0 0" FontSize="14"
-                        Command="{cmds:Command PixiEditor.Links.OpenHyperlink, UseProvided=True}" CommandParameter="https://github.com/CPKreuz"/>
+                <Label Style="{StaticResource SettingsText}" Margin="10 0 0 0" FontSize="14">
+                    <Hyperlink Command="{cmds:Command PixiEditor.Links.OpenHyperlink, UseProvided=True}" CommandParameter="https://github.com/CPKreuz" Style="{StaticResource SettingsLink}">
+                        <Run Text="Phillip Kreuz (CPK)"/>
+                        <Run Text="" FontFamily="{StaticResource Feather}"/>
+                    </Hyperlink>
+                </Label>
             </StackPanel>
             
-            <Button Style="{StaticResource HyperlinkTextButton}" Content="And other awesome contributors" VerticalAlignment="Center" Margin="20 10 0 0" FontSize="14"
-                    Command="{cmds:Command PixiEditor.Links.OpenHyperlink, UseProvided=True}" CommandParameter="https://github.com/PixiEditor/PixiEditor/graphs/contributors"/>
+            <Label Style="{StaticResource SettingsText}" Margin="20 10 0 0" FontSize="14">
+                <Hyperlink Command="{cmds:Command PixiEditor.Links.OpenHyperlink, UseProvided=True}" CommandParameter="https://github.com/PixiEditor/PixiEditor/graphs/contributors"
+                           Style="{StaticResource SettingsLink}">
+                    <Run Text="And other awesome contributors"/>
+                    <Run Text="" FontFamily="{StaticResource Feather}"/>
+                </Hyperlink>
+            </Label>
             
-            <Button Style="{StaticResource HyperlinkTextButton}" Content="License" VerticalAlignment="Center" Margin="10 20 0 0" FontSize="14"
-                    Command="{cmds:Command PixiEditor.Links.OpenLicense}"/>
-            <Button Style="{StaticResource HyperlinkTextButton}" Content="Third Party Licenses" VerticalAlignment="Center" Margin="10 0 0 0" FontSize="14"
-                    Command="{cmds:Command PixiEditor.Links.OpenOtherLicenses}"/>
-            <Button Style="{StaticResource HyperlinkTextButton}" Content="Documentation" VerticalAlignment="Center" Margin="10 0 0 0" FontSize="14"
-                    Command="{cmds:Command PixiEditor.Links.OpenDocumentation}"/>
+            <Separator Margin="0 10 0 0"/>
+            
+            <Label Style="{StaticResource SettingsText}" Margin="20 10 0 0" FontSize="14">
+                <Hyperlink Command="{cmds:Command PixiEditor.Links.OpenLicense}"
+                           Style="{StaticResource SettingsLink}">
+                    <Run Text="License"/>
+                    <Run Text="" FontFamily="{StaticResource Feather}"/>
+                </Hyperlink>
+            </Label>
+            
+            <Label Style="{StaticResource SettingsText}" Margin="20 10 0 0" FontSize="14">
+                <Hyperlink Command="{cmds:Command PixiEditor.Links.OpenOtherLicenses}"
+                           Style="{StaticResource SettingsLink}">
+                    <Run Text="Third Party Licenses"/>
+                    <Run Text="" FontFamily="{StaticResource Feather}"/>
+                </Hyperlink>
+            </Label>
+            
+            <Label Style="{StaticResource SettingsText}" Margin="20 10 0 0" FontSize="14">
+                <Hyperlink Command="{cmds:Command PixiEditor.Links.OpenDocumentation}"
+                           Style="{StaticResource SettingsLink}">
+                    <Run Text="Documentation"/>
+                    <Run Text="" FontFamily="{StaticResource Feather}"/>
+                </Hyperlink>
+            </Label>
             
             <userControls:AlignableWrapPanel DockPanel.Dock="Bottom" HorizontalContentAlignment="Center" HorizontalAlignment="Center" Margin="0,20,0,15">
                     <Button Command="{cmds:Command PixiEditor.Links.OpenHyperlink, UseProvided=True}" CommandParameter="https://pixieditor.net"

+ 4 - 3
src/PixiEditor/Views/MainWindow.xaml.cs

@@ -56,6 +56,8 @@ internal partial class MainWindow : Window
         {
             UpdateTaskbarIcon(x ? DataContext?.DocumentManagerSubViewModel.ActiveDocument : null);
         });
+
+        DataContext.DocumentManagerSubViewModel.ActiveDocumentChanged += DocumentChanged;
     }
 
     public static MainWindow CreateWithDocuments(IEnumerable<(string? originalPath, byte[] dotPixiBytes)> documents)
@@ -98,14 +100,13 @@ internal partial class MainWindow : Window
         ((HwndSource)PresentationSource.FromVisual(this)).AddHook(Helpers.WindowSizeHelper.SetMaxSizeHook);
     }
 
-    /*
-    private void BitmapManager_DocumentChanged(object sender, Models.Events.DocumentChangedEventArgs e)
+    private void DocumentChanged(object sender, Models.Events.DocumentChangedEventArgs e)
     {
         if (preferences.GetPreference("ImagePreviewInTaskbar", false))
         {
             UpdateTaskbarIcon(e.NewDocument);
         }
-    }*/
+    }
 
     private void UpdateTaskbarIcon(DocumentViewModel document)
     {

+ 2 - 2
src/PixiEditor/Views/UserControls/FixedViewport.xaml.cs

@@ -43,7 +43,7 @@ internal partial class FixedViewport : UserControl, INotifyPropertyChanged
 
     public WriteableBitmap? TargetBitmap
     {
-        get => Document?.Bitmaps.TryGetValue(CalculateResolution(), out WriteableBitmap? value) == true ? value : null;
+        get => Document?.LazyBitmaps.TryGetValue(CalculateResolution(), out WriteableBitmap? value) == true ? value : null;
     }
 
     public Guid GuidValue { get; } = Guid.NewGuid();
@@ -51,7 +51,7 @@ internal partial class FixedViewport : UserControl, INotifyPropertyChanged
     public FixedViewport()
     {
         InitializeComponent();
-        Binding binding = new Binding { Source = this, Path = new PropertyPath("Document.Bitmaps") };
+        Binding binding = new Binding { Source = this, Path = new PropertyPath($"{nameof(Document)}.{nameof(Document.LazyBitmaps)}") };
         SetBinding(BitmapsProperty, binding);
 
         Loaded += OnLoad;

+ 2 - 1
src/PixiEditor/Views/UserControls/Palettes/PaletteColorAdder.xaml

@@ -16,7 +16,8 @@
             Style="{StaticResource DefaultColorPickerStyle}" Width="50" Focusable="False" Margin="0 0 10 0"
             ShowAlpha="False"/>
         <Button Name="AddButton" Margin="0" Width="24" Height="24" 
-                Style="{StaticResource ToolButtonStyle}" 
+                Style="{StaticResource ToolButtonStyle}"
+                ToolTip="Add color"
                 Cursor="Hand"  Click="Button_Click">
             <Button.Background>
                 <ImageBrush ImageSource="/Images/Plus-square.png"/>

+ 0 - 3
src/PixiEditor/Views/UserControls/Palettes/PaletteColorAdder.xaml.cs

@@ -7,9 +7,6 @@ using BackendColor = PixiEditor.DrawingApi.Core.ColorsImpl.Color;
 
 namespace PixiEditor.Views.UserControls.Palettes;
 
-/// <summary>
-/// Interaction logic for PaletteColorAdder.xaml
-/// </summary>
 internal partial class PaletteColorAdder : UserControl
 {
     public WpfObservableRangeCollection<BackendColor> Colors

+ 1 - 1
src/PixiEditor/Views/UserControls/Viewport.xaml

@@ -120,7 +120,7 @@
         <zoombox:Zoombox
             Tag="{Binding ElementName=vpUc}"
             x:Name="zoombox"
-            UseTouchGestures="{Binding UseTouchGestures, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}, Mode=OneWayToSource}"
+            UseTouchGestures="{Binding UseTouchGestures, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}, Mode=OneWay}"
             Scale="{Binding ZoomboxScale, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}, Mode=OneWayToSource}"
             Center="{Binding Center, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}, Mode=OneWayToSource}"
             Angle="{Binding Angle, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:Viewport}, Mode=OneWayToSource}"

+ 2 - 2
src/PixiEditor/Views/UserControls/Viewport.xaml.cs

@@ -269,7 +269,7 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
     {
         get
         {
-            return Document?.Bitmaps.TryGetValue(CalculateResolution(), out WriteableBitmap? value) == true ? value : null;
+            return Document?.LazyBitmaps.TryGetValue(CalculateResolution(), out WriteableBitmap? value) == true ? value : null;
         }
     }
 
@@ -286,7 +286,7 @@ internal partial class Viewport : UserControl, INotifyPropertyChanged
     {
         InitializeComponent();
 
-        Binding binding = new Binding { Source = this, Path = new PropertyPath("Document.Bitmaps") };
+        Binding binding = new Binding { Source = this, Path = new PropertyPath($"{nameof(Document)}.{nameof(Document.LazyBitmaps)}") };
         SetBinding(BitmapsProperty, binding);
 
         MainImage!.Loaded += OnImageLoaded;

+ 1 - 3
src/PixiEditorTests/PixiEditorTests.csproj

@@ -1,7 +1,7 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFramework>net6.0-windows10.0.22000.0</TargetFramework>
+    <TargetFramework>net7.0-windows</TargetFramework>
 
     <IsPackable>false</IsPackable>
 
@@ -30,7 +30,6 @@
   </ItemGroup>
 
   <ItemGroup>
-    <PackageReference Include="Codecov" Version="1.13.0" />
     <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="3.3.2">
       <PrivateAssets>all</PrivateAssets>
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@@ -38,7 +37,6 @@
     <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
     <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
-    <PackageReference Include="Moq" Version="4.17.2" />
     <PackageReference Include="OpenCover" Version="4.7.1221" />
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">

+ 7 - 0
src/global.json

@@ -0,0 +1,7 @@
+{
+  "sdk": {
+    "version": "7.0.0",
+    "rollForward": "latestMajor",
+    "allowPrerelease": false
+  }
+}

+ 1 - 1
windows-x64-release-dev.yml

@@ -20,7 +20,7 @@ steps:
 - task: UseDotNet@2
   inputs:
     packageType: 'sdk'
-    version: '6.x'
+    version: '7.x'
 - task: NuGetToolInstaller@1
 
 - task: NuGetCommand@2

+ 1 - 1
windows-x64-release.yml

@@ -20,7 +20,7 @@ steps:
 - task: UseDotNet@2
   inputs:
     packageType: 'sdk'
-    version: '6.x'
+    version: '7.x'
 - task: NuGetToolInstaller@1
 
 - task: NuGetCommand@2

+ 1 - 1
windows-x86-release-dev.yml

@@ -20,7 +20,7 @@ steps:
 - task: UseDotNet@2
   inputs:
     packageType: 'sdk'
-    version: '6.x'
+    version: '7.x'
 
 - task: NuGetToolInstaller@1
 

+ 1 - 1
windows-x86-release.yml

@@ -20,7 +20,7 @@ steps:
 - task: UseDotNet@2
   inputs:
     packageType: 'sdk'
-    version: '6.x'
+    version: '7.x'
 
 - task: NuGetToolInstaller@1