Browse Source

Merge pull request #874 from PixiEditor/fixes/31.03.2025

Fixes/31.03.2025
Krzysztof Krysiński 4 months ago
parent
commit
b5008c8580
44 changed files with 671 additions and 332 deletions
  1. 6 4
      src/ChunkyImageLib/DataHolders/ShapeData.cs
  2. 48 9
      src/ChunkyImageLib/Operations/RectangleOperation.cs
  3. 1 1
      src/Drawie
  4. 2 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/Shapes/IReadOnlyRectangleData.cs
  5. 33 6
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/RectangleVectorData.cs
  6. 4 2
      src/PixiEditor.ChangeableDocument/Changes/NodeGraph/NodePosition_UpdateableChange.cs
  7. 2 2
      src/PixiEditor.Linux/LinuxProcessUtility.cs
  8. 4 3
      src/PixiEditor.MacOs/MacOsProcessUtility.cs
  9. 2 2
      src/PixiEditor.OperatingSystem/IProcessUtility.cs
  10. 102 81
      src/PixiEditor.UI.Common/Accents/Base.axaml
  11. 20 6
      src/PixiEditor.UpdateInstaller.Exe/Program.cs
  12. 13 3
      src/PixiEditor.UpdateModule/UpdateInstaller.cs
  13. 4 3
      src/PixiEditor.Windows/WindowsProcessUtility.cs
  14. 12 4
      src/PixiEditor/Data/Localization/Languages/en.json
  15. 4 4
      src/PixiEditor/Helpers/ProcessHelper.cs
  16. 8 1
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/DrawableShapeToolExecutor.cs
  17. 2 2
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/RasterRectangleToolExecutor.cs
  18. 6 0
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorRectangleToolExecutor.cs
  19. 8 0
      src/PixiEditor/Models/Handlers/Tools/ICornerRadiusTool.cs
  20. 4 2
      src/PixiEditor/Models/Handlers/Tools/IRasterRectangleToolHandler.cs
  21. 4 2
      src/PixiEditor/Models/Handlers/Tools/IVectorRectangleToolHandler.cs
  22. 12 1
      src/PixiEditor/Models/IO/CustomDocumentFormats/SvgDocumentBuilder.cs
  23. 10 1
      src/PixiEditor/ViewModels/Menu/MenuBarViewModel.cs
  24. 159 126
      src/PixiEditor/ViewModels/SubViewModels/UpdateViewModel.cs
  25. 0 2
      src/PixiEditor/ViewModels/Tools/ShapeTool.cs
  26. 1 0
      src/PixiEditor/ViewModels/Tools/ToolSettings/Settings/BoolSettingViewModel.cs
  27. 3 1
      src/PixiEditor/ViewModels/Tools/ToolSettings/Settings/Setting.cs
  28. 2 2
      src/PixiEditor/ViewModels/Tools/ToolSettings/Settings/SizeSettingViewModel.cs
  29. 3 1
      src/PixiEditor/ViewModels/Tools/ToolSettings/Toolbars/SettingAttributes.cs
  30. 1 1
      src/PixiEditor/ViewModels/Tools/ToolSettings/Toolbars/ShapeToolbar.cs
  31. 1 1
      src/PixiEditor/ViewModels/Tools/ToolSettings/Toolbars/ToolbarFactory.cs
  32. 22 5
      src/PixiEditor/ViewModels/Tools/Tools/RasterRectangleToolViewModel.cs
  33. 22 6
      src/PixiEditor/ViewModels/Tools/Tools/VectorRectangleToolViewModel.cs
  34. 4 2
      src/PixiEditor/ViewModels/ViewModelMain.cs
  35. 1 5
      src/PixiEditor/Views/Main/ActionDisplayBar.axaml
  36. 73 5
      src/PixiEditor/Views/Main/MainTitleBar.axaml
  37. 20 2
      src/PixiEditor/Views/Main/MainTitleBar.axaml.cs
  38. 1 1
      src/PixiEditor/Views/Main/Tools/Toolbar.axaml
  39. 1 2
      src/PixiEditor/Views/Nodes/Properties/BooleanPropertyView.axaml
  40. 1 1
      src/PixiEditor/Views/Nodes/Properties/GenericEnumPropertyView.axaml
  41. 6 0
      src/PixiEditor/Views/Nodes/Properties/GenericEnumPropertyView.axaml.cs
  42. 3 1
      src/PixiEditor/Views/Tools/ToolSettings/Settings/BoolSettingView.axaml
  43. 28 21
      tests/ChunkyImageLibTest/ChunkyImageTests.cs
  44. 8 7
      tests/ChunkyImageLibTest/RectangleOperationTests.cs

+ 6 - 4
src/ChunkyImageLib/DataHolders/ShapeData.cs

@@ -8,7 +8,7 @@ namespace ChunkyImageLib.DataHolders;
 
 
 public record struct ShapeData
 public record struct ShapeData
 {
 {
-    public ShapeData(VecD center, VecD size, double rotation, float strokeWidth, Paintable stroke, Paintable fillPaintable, BlendMode blendMode = BlendMode.SrcOver)
+    public ShapeData(VecD center, VecD size, double cornerRadius, double rotation, float strokeWidth, Paintable stroke, Paintable fillPaintable, BlendMode blendMode = BlendMode.SrcOver)
     {
     {
         Stroke = stroke;
         Stroke = stroke;
         FillPaintable = fillPaintable;
         FillPaintable = fillPaintable;
@@ -16,6 +16,7 @@ public record struct ShapeData
         Size = size;
         Size = size;
         Angle = rotation;
         Angle = rotation;
         StrokeWidth = strokeWidth;
         StrokeWidth = strokeWidth;
+        CornerRadius = cornerRadius;
         BlendMode = blendMode;
         BlendMode = blendMode;
     }
     }
     public Paintable Stroke { get; }
     public Paintable Stroke { get; }
@@ -25,15 +26,16 @@ public record struct ShapeData
 
 
     /// <summary>Can be negative to show flipping </summary>
     /// <summary>Can be negative to show flipping </summary>
     public VecD Size { get; }
     public VecD Size { get; }
+
+    public double CornerRadius { get; }
     public double Angle { get; }
     public double Angle { get; }
     public float StrokeWidth { get; }
     public float StrokeWidth { get; }
-
     public bool AntiAliasing { get; set; } = false;
     public bool AntiAliasing { get; set; } = false;
     
     
 
 
     public ShapeData AsMirroredAcrossHorAxis(double horAxisY)
     public ShapeData AsMirroredAcrossHorAxis(double horAxisY)
-        => new ShapeData(Center.ReflectY(horAxisY), new(Size.X, -Size.Y), -Angle, StrokeWidth, Stroke, FillPaintable, BlendMode);
+        => new ShapeData(Center.ReflectY(horAxisY), new(Size.X, -Size.Y), CornerRadius, -Angle, StrokeWidth, Stroke, FillPaintable, BlendMode);
     public ShapeData AsMirroredAcrossVerAxis(double verAxisX)
     public ShapeData AsMirroredAcrossVerAxis(double verAxisX)
-        => new ShapeData(Center.ReflectX(verAxisX), new(-Size.X, Size.Y), -Angle, StrokeWidth, Stroke, FillPaintable, BlendMode);
+        => new ShapeData(Center.ReflectX(verAxisX), new(-Size.X, Size.Y), CornerRadius, -Angle, StrokeWidth, Stroke, FillPaintable, BlendMode);
 
 
 }
 }

+ 48 - 9
src/ChunkyImageLib/Operations/RectangleOperation.cs

@@ -39,37 +39,58 @@ internal class RectangleOperation : IMirroredDrawOperation
         surf.Canvas.Translate(-chunkPos * ChunkyImage.FullChunkSize);
         surf.Canvas.Translate(-chunkPos * ChunkyImage.FullChunkSize);
         surf.Canvas.RotateRadians((float)Data.Angle, (float)rect.Center.X, (float)rect.Center.Y);
         surf.Canvas.RotateRadians((float)Data.Angle, (float)rect.Center.X, (float)rect.Center.Y);
 
 
+        double maxRadiusInPx = Math.Min(Data.Size.X, Data.Size.Y) / 2;
+        double radiusInPx = Data.CornerRadius * maxRadiusInPx;
+
         if (Data.AntiAliasing)
         if (Data.AntiAliasing)
         {
         {
-            DrawAntiAliased(surf, rect);
+            DrawAntiAliased(surf, rect, radiusInPx);
         }
         }
         else
         else
         {
         {
-            DrawPixelPerfect(surf, rect, innerRect);
+            DrawPixelPerfect(surf, rect, innerRect, radiusInPx);
         }
         }
 
 
         surf.Canvas.RestoreToCount(initial);
         surf.Canvas.RestoreToCount(initial);
     }
     }
 
 
-    private void DrawPixelPerfect(DrawingSurface surf, RectD rect, RectD innerRect)
+    private void DrawPixelPerfect(DrawingSurface surf, RectD rect, RectD innerRect, double radius)
     {
     {
         // draw fill
         // draw fill
         if (Data.FillPaintable.AnythingVisible)
         if (Data.FillPaintable.AnythingVisible)
         {
         {
             int saved = surf.Canvas.Save();
             int saved = surf.Canvas.Save();
-            surf.Canvas.ClipRect(innerRect);
+            if (radius == 0)
+            {
+                surf.Canvas.ClipRect(innerRect);
+            }
+            else
+            {
+                surf.Canvas.ClipRoundRect(innerRect, new VecD(radius), ClipOperation.Intersect);
+            }
+
             surf.Canvas.DrawPaintable(Data.FillPaintable, Data.BlendMode);
             surf.Canvas.DrawPaintable(Data.FillPaintable, Data.BlendMode);
             surf.Canvas.RestoreToCount(saved);
             surf.Canvas.RestoreToCount(saved);
         }
         }
 
 
         // draw stroke
         // draw stroke
         surf.Canvas.Save();
         surf.Canvas.Save();
-        surf.Canvas.ClipRect(rect);
-        surf.Canvas.ClipRect(innerRect, ClipOperation.Difference);
+        if (radius == 0)
+        {
+            surf.Canvas.ClipRect(rect);
+            surf.Canvas.ClipRect(innerRect, ClipOperation.Difference);
+        }
+        else
+        {
+            VecD vecRadius = new VecD(radius);
+            surf.Canvas.ClipRoundRect(rect, vecRadius, ClipOperation.Intersect);
+            surf.Canvas.ClipRoundRect(innerRect, vecRadius, ClipOperation.Difference);
+        }
+
         surf.Canvas.DrawPaintable(Data.Stroke, Data.BlendMode);
         surf.Canvas.DrawPaintable(Data.Stroke, Data.BlendMode);
     }
     }
 
 
-    private void DrawAntiAliased(DrawingSurface surf, RectD rect)
+    private void DrawAntiAliased(DrawingSurface surf, RectD rect, double radius)
     {
     {
         // draw fill
         // draw fill
         if (Data.FillPaintable.AnythingVisible)
         if (Data.FillPaintable.AnythingVisible)
@@ -79,7 +100,15 @@ internal class RectangleOperation : IMirroredDrawOperation
             paint.StrokeWidth = 0;
             paint.StrokeWidth = 0;
             paint.SetPaintable(Data.FillPaintable);
             paint.SetPaintable(Data.FillPaintable);
             paint.Style = PaintStyle.Fill;
             paint.Style = PaintStyle.Fill;
-            surf.Canvas.DrawRect((float)rect.Left, (float)rect.Top, (float)rect.Width, (float)rect.Height, paint);
+            if (radius == 0)
+            {
+                surf.Canvas.DrawRect((float)rect.Left, (float)rect.Top, (float)rect.Width, (float)rect.Height, paint);
+            }
+            else
+            {
+                surf.Canvas.DrawRoundRect((float)rect.Left, (float)rect.Top, (float)rect.Width,
+                    (float)rect.Height, (float)radius, (float)radius, paint);
+            }
 
 
             surf.Canvas.RestoreToCount(saved);
             surf.Canvas.RestoreToCount(saved);
         }
         }
@@ -90,7 +119,17 @@ internal class RectangleOperation : IMirroredDrawOperation
         paint.SetPaintable(Data.StrokeWidth > 0 ? Data.Stroke : Data.FillPaintable);
         paint.SetPaintable(Data.StrokeWidth > 0 ? Data.Stroke : Data.FillPaintable);
         paint.Style = PaintStyle.Stroke;
         paint.Style = PaintStyle.Stroke;
         RectD innerRect = rect.Inflate(-Data.StrokeWidth / 2f);
         RectD innerRect = rect.Inflate(-Data.StrokeWidth / 2f);
-        surf.Canvas.DrawRect((float)innerRect.Left, (float)innerRect.Top, (float)innerRect.Width, (float)innerRect.Height, paint);
+
+        if (radius == 0)
+        {
+            surf.Canvas.DrawRect((float)innerRect.Left, (float)innerRect.Top, (float)innerRect.Width,
+                (float)innerRect.Height, paint);
+        }
+        else
+        {
+            surf.Canvas.DrawRoundRect((float)innerRect.Left, (float)innerRect.Top, (float)innerRect.Width,
+                (float)innerRect.Height, (float)radius, (float)radius, paint);
+        }
     }
     }
 
 
     public AffectedArea FindAffectedArea(VecI imageSize)
     public AffectedArea FindAffectedArea(VecI imageSize)

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit 02c8bda10d6536f1d8a613ef877526756a789132
+Subproject commit 2aaeb9ebb6f2d3b5824721e8ace0765eb58305ca

+ 2 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/Shapes/IReadOnlyRectangleData.cs

@@ -2,8 +2,9 @@
 
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces.Shapes;
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces.Shapes;
 
 
-public interface IReadOnlyRectangleData : IReadOnlyShapeVectorData // TODO: Add IReadOnlyStrokeJoinable
+public interface IReadOnlyRectangleData : IReadOnlyShapeVectorData
 {
 {
     public VecD Center { get; }
     public VecD Center { get; }
     public VecD Size { get; }
     public VecD Size { get; }
+    public double CornerRadius { get; }
 }
 }

+ 33 - 6
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/RectangleVectorData.cs

@@ -12,6 +12,7 @@ public class RectangleVectorData : ShapeVectorData, IReadOnlyRectangleData
 {
 {
     public VecD Center { get; set; }
     public VecD Center { get; set; }
     public VecD Size { get; set; }
     public VecD Size { get; set; }
+    public double CornerRadius { get; set; }
 
 
     public override RectD GeometryAABB => RectD.FromCenterAndSize(Center, Size);
     public override RectD GeometryAABB => RectD.FromCenterAndSize(Center, Size);
 
 
@@ -67,7 +68,7 @@ public class RectangleVectorData : ShapeVectorData, IReadOnlyRectangleData
         {
         {
             paint.SetPaintable(FillPaintable);
             paint.SetPaintable(FillPaintable);
             paint.Style = PaintStyle.Fill;
             paint.Style = PaintStyle.Fill;
-            canvas.DrawRect(RectD.FromCenterAndSize(Center, Size), paint);
+            DrawRect(canvas, paint);
         }
         }
 
 
         if (StrokeWidth > 0 && Stroke.AnythingVisible)
         if (StrokeWidth > 0 && Stroke.AnythingVisible)
@@ -76,7 +77,8 @@ public class RectangleVectorData : ShapeVectorData, IReadOnlyRectangleData
             paint.Style = PaintStyle.Stroke;
             paint.Style = PaintStyle.Stroke;
 
 
             paint.StrokeWidth = StrokeWidth;
             paint.StrokeWidth = StrokeWidth;
-            canvas.DrawRect(RectD.FromCenterAndSize(Center, Size), paint);
+
+            DrawRect(canvas, paint);
         }
         }
 
 
         if (applyTransform)
         if (applyTransform)
@@ -85,6 +87,22 @@ public class RectangleVectorData : ShapeVectorData, IReadOnlyRectangleData
         }
         }
     }
     }
 
 
+    private void DrawRect(Canvas canvas, Paint paint)
+    {
+        double maxRadiusPx = Math.Min(Size.X, Size.Y) / 2f;
+        double radiusPx = CornerRadius * maxRadiusPx;
+
+        if (radiusPx == 0)
+        {
+            canvas.DrawRect(RectD.FromCenterAndSize(Center, Size), paint);
+        }
+        else
+        {
+            RectD rect = RectD.FromCenterAndSize(Center, Size);
+            canvas.DrawRoundRect((float)rect.Pos.X, (float)rect.Pos.Y, (float)rect.Width, (float)rect.Height, (float)radiusPx, (float)radiusPx, paint);
+        }
+    }
+
     public override bool IsValid()
     public override bool IsValid()
     {
     {
         return Size is { X: > 0, Y: > 0 };
         return Size is { X: > 0, Y: > 0 };
@@ -92,13 +110,21 @@ public class RectangleVectorData : ShapeVectorData, IReadOnlyRectangleData
 
 
     protected override int GetSpecificHash()
     protected override int GetSpecificHash()
     {
     {
-        return HashCode.Combine(Center, Size);
+        return HashCode.Combine(Center, Size, CornerRadius);
     }
     }
 
 
     public override VectorPath ToPath(bool transformed = false)
     public override VectorPath ToPath(bool transformed = false)
     {
     {
         VectorPath path = new VectorPath();
         VectorPath path = new VectorPath();
-        path.AddRect(RectD.FromCenterAndSize(Center, Size));
+        if (CornerRadius == 0)
+        {
+            path.AddRect(RectD.FromCenterAndSize(Center, Size));
+        }
+        else
+        {
+            path.AddRoundRect(RectD.FromCenterAndSize(Center, Size), new VecD(CornerRadius));
+        }
+
         if (transformed)
         if (transformed)
         {
         {
             path.Transform(TransformationMatrix);
             path.Transform(TransformationMatrix);
@@ -109,7 +135,8 @@ public class RectangleVectorData : ShapeVectorData, IReadOnlyRectangleData
 
 
     protected bool Equals(RectangleVectorData other)
     protected bool Equals(RectangleVectorData other)
     {
     {
-        return base.Equals(other) && Center.Equals(other.Center) && Size.Equals(other.Size);
+        return base.Equals(other) && Center.Equals(other.Center) && Size.Equals(other.Size) &&
+               CornerRadius.Equals(other.CornerRadius);
     }
     }
 
 
     public override bool Equals(object? obj)
     public override bool Equals(object? obj)
@@ -134,6 +161,6 @@ public class RectangleVectorData : ShapeVectorData, IReadOnlyRectangleData
 
 
     public override int GetHashCode()
     public override int GetHashCode()
     {
     {
-        return HashCode.Combine(base.GetHashCode(), Center, Size);
+        return HashCode.Combine(base.GetHashCode(), Center, Size, CornerRadius);
     }
     }
 }
 }

+ 4 - 2
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/NodePosition_UpdateableChange.cs

@@ -62,11 +62,13 @@ internal class NodePosition_UpdateableChange : UpdateableChange
         out bool ignoreInUndo)
         out bool ignoreInUndo)
     {
     {
         ignoreInUndo = false;
         ignoreInUndo = false;
-      
+
         VecD delta = NewPosition - startPosition;
         VecD delta = NewPosition - startPosition;
+        bool setDirect = false;
         if (NewPosition == startPosition)
         if (NewPosition == startPosition)
         {
         {
             delta = NewPosition;
             delta = NewPosition;
+            setDirect = true;
         }
         }
             
             
         List<IChangeInfo> changes = new();
         List<IChangeInfo> changes = new();
@@ -74,7 +76,7 @@ internal class NodePosition_UpdateableChange : UpdateableChange
         foreach (var nodeId in NodeIds)
         foreach (var nodeId in NodeIds)
         {
         {
             var node = target.FindNode<Node>(nodeId);
             var node = target.FindNode<Node>(nodeId);
-            node.Position = originalPositions[nodeId] + delta;
+            node.Position = setDirect ? delta : originalPositions[nodeId] + delta;
             changes.Add(new NodePosition_ChangeInfo(nodeId, node.Position));
             changes.Add(new NodePosition_ChangeInfo(nodeId, node.Position));
         }
         }
         
         

+ 2 - 2
src/PixiEditor.Linux/LinuxProcessUtility.cs

@@ -7,12 +7,12 @@ namespace PixiEditor.Linux;
 
 
 public class LinuxProcessUtility : IProcessUtility
 public class LinuxProcessUtility : IProcessUtility
 {
 {
-    public Process RunAsAdmin(string path)
+    public Process RunAsAdmin(string path, string args)
     {
     {
         throw new NotImplementedException("Running as admin is not supported on Linux");
         throw new NotImplementedException("Running as admin is not supported on Linux");
     }
     }
 
 
-    public Process RunAsAdmin(string path, bool createWindow)
+    public Process RunAsAdmin(string path, string args, bool createWindow)
     {
     {
         throw new NotImplementedException("Running as admin is not supported on Linux");
         throw new NotImplementedException("Running as admin is not supported on Linux");
     }
     }

+ 4 - 3
src/PixiEditor.MacOs/MacOsProcessUtility.cs

@@ -5,17 +5,18 @@ namespace PixiEditor.MacOs;
 
 
 internal class MacOsProcessUtility : IProcessUtility
 internal class MacOsProcessUtility : IProcessUtility
 {
 {
-    public Process RunAsAdmin(string path)
+    public Process RunAsAdmin(string path, string args)
     {
     {
-        return RunAsAdmin(path, true);
+        return RunAsAdmin(path, args, true);
     }
     }
 
 
-    public Process RunAsAdmin(string path, bool createWindow)
+    public Process RunAsAdmin(string path, string args, bool createWindow)
     {
     {
         ProcessStartInfo startInfo = new ProcessStartInfo
         ProcessStartInfo startInfo = new ProcessStartInfo
         {
         {
             FileName = path,
             FileName = path,
             Verb = "runas",
             Verb = "runas",
+            Arguments = args,
             UseShellExecute = createWindow,
             UseShellExecute = createWindow,
             CreateNoWindow = !createWindow,
             CreateNoWindow = !createWindow,
             RedirectStandardOutput = !createWindow,
             RedirectStandardOutput = !createWindow,

+ 2 - 2
src/PixiEditor.OperatingSystem/IProcessUtility.cs

@@ -4,8 +4,8 @@ namespace PixiEditor.OperatingSystem;
 
 
 public interface IProcessUtility
 public interface IProcessUtility
 {
 {
-    public Process RunAsAdmin(string path);
-    public Process RunAsAdmin(string path, bool createWindow);
+    public Process RunAsAdmin(string path, string? args);
+    public Process RunAsAdmin(string path, string? args, bool createWindow);
     public bool IsRunningAsAdministrator();
     public bool IsRunningAsAdministrator();
     public Process ShellExecute(string toExecute);
     public Process ShellExecute(string toExecute);
     public Process ShellExecute(string toExecute, string args);
     public Process ShellExecute(string toExecute, string args);

+ 102 - 81
src/PixiEditor.UI.Common/Accents/Base.axaml

@@ -1,6 +1,6 @@
 <ResourceDictionary xmlns="https://github.com/avaloniaui"
 <ResourceDictionary xmlns="https://github.com/avaloniaui"
-        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-        xmlns:system="clr-namespace:System;assembly=System.Runtime">
+                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+                    xmlns:system="clr-namespace:System;assembly=System.Runtime">
 
 
     <ResourceDictionary.ThemeDictionaries>
     <ResourceDictionary.ThemeDictionaries>
         <ResourceDictionary x:Key="Dark">
         <ResourceDictionary x:Key="Dark">
@@ -14,6 +14,7 @@
             <Color x:Key="AccentHighColor">#c0334e</Color>
             <Color x:Key="AccentHighColor">#c0334e</Color>
             <Color x:Key="ThemeAccent2Color">#5fad65</Color>
             <Color x:Key="ThemeAccent2Color">#5fad65</Color>
             <Color x:Key="ThemeAccent2HighColor">#3dd276</Color>
             <Color x:Key="ThemeAccent2HighColor">#3dd276</Color>
+            <Color x:Key="ThemeAccent3Color">DodgerBlue</Color>
 
 
             <Color x:Key="ThemeForegroundColor">#FFFFFF</Color>
             <Color x:Key="ThemeForegroundColor">#FFFFFF</Color>
             <Color x:Key="ThemeForegroundSecondaryColor">#B3FFFFFF</Color>
             <Color x:Key="ThemeForegroundSecondaryColor">#B3FFFFFF</Color>
@@ -34,15 +35,15 @@
             <Color x:Key="GlyphColor">#444</Color>
             <Color x:Key="GlyphColor">#444</Color>
             <Color x:Key="GlyphBackground">White</Color>
             <Color x:Key="GlyphBackground">White</Color>
             <Color x:Key="ThumbColor">#606060</Color>
             <Color x:Key="ThumbColor">#606060</Color>
-            
+
             <Color x:Key="SelectionFillColor">#510051ff</Color>
             <Color x:Key="SelectionFillColor">#510051ff</Color>
 
 
             <Color x:Key="NotificationCardBackgroundColor">#303030</Color>
             <Color x:Key="NotificationCardBackgroundColor">#303030</Color>
-            
+
             <!--                          -->
             <!--                          -->
             <!--     <| Node Colors |>    -->
             <!--     <| Node Colors |>    -->
             <!--                          -->
             <!--                          -->
-            
+
             <!-- Sockets -->
             <!-- Sockets -->
             <Color x:Key="DefaultSocketColor">#FF00FF</Color>
             <Color x:Key="DefaultSocketColor">#FF00FF</Color>
             <Color x:Key="ImageSocketColor">#99c47a</Color>
             <Color x:Key="ImageSocketColor">#99c47a</Color>
@@ -63,14 +64,14 @@
             <Color x:Key="TextDataSocketColor">#f2f2f2</Color>
             <Color x:Key="TextDataSocketColor">#f2f2f2</Color>
             <Color x:Key="Matrix3X3SocketColor">#ffea4f</Color>
             <Color x:Key="Matrix3X3SocketColor">#ffea4f</Color>
             <GradientStops x:Key="ShapeDataSocketGradient">
             <GradientStops x:Key="ShapeDataSocketGradient">
-                <GradientStop Offset="0" Color="{StaticResource EllipseDataSocketColor}"/>
-                <GradientStop Offset="0.33" Color="{StaticResource EllipseDataSocketColor}"/>
-                <GradientStop Offset="0.33" Color="{StaticResource TextDataSocketColor}"/>
-                <GradientStop Offset="0.66" Color="{StaticResource TextDataSocketColor}"/>
-                <GradientStop Offset="0.66" Color="{StaticResource PointsDataSocketColor}"/>
-                <GradientStop Offset="1" Color="{StaticResource PointsDataSocketColor}"/>
+                <GradientStop Offset="0" Color="{StaticResource EllipseDataSocketColor}" />
+                <GradientStop Offset="0.33" Color="{StaticResource EllipseDataSocketColor}" />
+                <GradientStop Offset="0.33" Color="{StaticResource TextDataSocketColor}" />
+                <GradientStop Offset="0.66" Color="{StaticResource TextDataSocketColor}" />
+                <GradientStop Offset="0.66" Color="{StaticResource PointsDataSocketColor}" />
+                <GradientStop Offset="1" Color="{StaticResource PointsDataSocketColor}" />
             </GradientStops>
             </GradientStops>
-            
+
             <!-- Zones & Frames -->
             <!-- Zones & Frames -->
             <Color x:Key="PixiEditorModifyImageBorderColor">#68abdf</Color>
             <Color x:Key="PixiEditorModifyImageBorderColor">#68abdf</Color>
             <Color x:Key="PixiEditorModifyImageNodeBackgroundColor">#4068abdf</Color>
             <Color x:Key="PixiEditorModifyImageNodeBackgroundColor">#4068abdf</Color>
@@ -80,7 +81,7 @@
 
 
             <Color x:Key="NodeFrameBorderColor">#101010</Color>
             <Color x:Key="NodeFrameBorderColor">#101010</Color>
             <Color x:Key="NodeFrameBackgroundColor">#40101010</Color>
             <Color x:Key="NodeFrameBackgroundColor">#40101010</Color>
-            
+
             <!-- Categories -->
             <!-- Categories -->
             <Color x:Key="ImageCategoryBackgroundColor">#5B7348</Color>
             <Color x:Key="ImageCategoryBackgroundColor">#5B7348</Color>
             <Color x:Key="StructureCategoryBackgroundColor">#735C39</Color>
             <Color x:Key="StructureCategoryBackgroundColor">#735C39</Color>
@@ -91,7 +92,7 @@
             <Color x:Key="AnimationCategoryBackgroundColor">#4D4466</Color>
             <Color x:Key="AnimationCategoryBackgroundColor">#4D4466</Color>
             <Color x:Key="EffectsCategoryBackgroundColor">#e36262</Color>
             <Color x:Key="EffectsCategoryBackgroundColor">#e36262</Color>
             <Color x:Key="MatrixCategoryBackgroundColor">#9169ff</Color>
             <Color x:Key="MatrixCategoryBackgroundColor">#9169ff</Color>
-            
+
             <Color x:Key="HorizontalSnapAxisColor">#B00022</Color>
             <Color x:Key="HorizontalSnapAxisColor">#B00022</Color>
             <Color x:Key="VerticalSnapAxisColor">#5fad65</Color>
             <Color x:Key="VerticalSnapAxisColor">#5fad65</Color>
             <Color x:Key="SnapPointPreviewColor">#68abdf</Color>
             <Color x:Key="SnapPointPreviewColor">#68abdf</Color>
@@ -104,16 +105,19 @@
             <SolidColorBrush x:Key="ThemeForegroundLowBrush" Color="gray"></SolidColorBrush>
             <SolidColorBrush x:Key="ThemeForegroundLowBrush" Color="gray"></SolidColorBrush>
 
 
             <SolidColorBrush x:Key="ThemeBackgroundBrush" Color="{StaticResource ThemeBackgroundColor}" />
             <SolidColorBrush x:Key="ThemeBackgroundBrush" Color="{StaticResource ThemeBackgroundColor}" />
-            <SolidColorBrush x:Key="ThemeBackgroundTranslucentBrush" Color="{StaticResource ThemeBackgroundTranslucentColor}"/>
+            <SolidColorBrush x:Key="ThemeBackgroundTranslucentBrush"
+                             Color="{StaticResource ThemeBackgroundTranslucentColor}" />
             <SolidColorBrush x:Key="ThemeBackgroundBrush1" Color="{StaticResource ThemeBackgroundColor1}" />
             <SolidColorBrush x:Key="ThemeBackgroundBrush1" Color="{StaticResource ThemeBackgroundColor1}" />
             <SolidColorBrush x:Key="ThemeBackgroundBrush2" Color="{StaticResource ThemeBackgroundColor2}" />
             <SolidColorBrush x:Key="ThemeBackgroundBrush2" Color="{StaticResource ThemeBackgroundColor2}" />
             <SolidColorBrush x:Key="ThemeAccentLowBrush" Color="{StaticResource AccentLowColor}" />
             <SolidColorBrush x:Key="ThemeAccentLowBrush" Color="{StaticResource AccentLowColor}" />
             <SolidColorBrush x:Key="ThemeAccentBrush" Color="{StaticResource AccentColor}" />
             <SolidColorBrush x:Key="ThemeAccentBrush" Color="{StaticResource AccentColor}" />
             <SolidColorBrush x:Key="ThemeAccentHighBrush" Color="{StaticResource AccentHighColor}" />
             <SolidColorBrush x:Key="ThemeAccentHighBrush" Color="{StaticResource AccentHighColor}" />
             <SolidColorBrush x:Key="ThemeAccent2Brush" Color="{StaticResource ThemeAccent2Color}" />
             <SolidColorBrush x:Key="ThemeAccent2Brush" Color="{StaticResource ThemeAccent2Color}" />
+            <SolidColorBrush x:Key="ThemeAccent3Brush" Color="{StaticResource ThemeAccent3Color}" />
 
 
             <SolidColorBrush x:Key="ThemeForegroundBrush" Color="{StaticResource ThemeForegroundColor}" />
             <SolidColorBrush x:Key="ThemeForegroundBrush" Color="{StaticResource ThemeForegroundColor}" />
-            <SolidColorBrush x:Key="ThemeForegroundSecondaryBrush" Color="{StaticResource ThemeForegroundSecondaryColor}" />
+            <SolidColorBrush x:Key="ThemeForegroundSecondaryBrush"
+                             Color="{StaticResource ThemeForegroundSecondaryColor}" />
 
 
             <SolidColorBrush x:Key="ThemeControlLowBrush" Color="{StaticResource ThemeControlLowColor}" />
             <SolidColorBrush x:Key="ThemeControlLowBrush" Color="{StaticResource ThemeControlLowColor}" />
             <SolidColorBrush x:Key="ThemeControlMidBrush" Color="{StaticResource ThemeControlMidColor}" />
             <SolidColorBrush x:Key="ThemeControlMidBrush" Color="{StaticResource ThemeControlMidColor}" />
@@ -125,76 +129,92 @@
             <SolidColorBrush x:Key="ThemeBorderMidBrush" Color="{StaticResource ThemeBorderMidColor}" />
             <SolidColorBrush x:Key="ThemeBorderMidBrush" Color="{StaticResource ThemeBorderMidColor}" />
             <SolidColorBrush x:Key="ThemeBorderHighBrush" Color="{StaticResource ThemeBorderHighColor}" />
             <SolidColorBrush x:Key="ThemeBorderHighBrush" Color="{StaticResource ThemeBorderHighColor}" />
 
 
-            <SolidColorBrush x:Key="NotificationCardBackgroundBrush" Color="{StaticResource NotificationCardBackgroundColor}" />
+            <SolidColorBrush x:Key="NotificationCardBackgroundBrush"
+                             Color="{StaticResource NotificationCardBackgroundColor}" />
 
 
             <SolidColorBrush x:Key="ErrorBrush" Color="{StaticResource ErrorColor}" />
             <SolidColorBrush x:Key="ErrorBrush" Color="{StaticResource ErrorColor}" />
             <SolidColorBrush x:Key="ErrorOnDarkBrush" Color="{StaticResource ErrorOnDarkColor}" />
             <SolidColorBrush x:Key="ErrorOnDarkBrush" Color="{StaticResource ErrorOnDarkColor}" />
-            <SolidColorBrush x:Key="GlyphBrush" Color="{StaticResource GlyphColor}"/>
-            <SolidColorBrush x:Key="ThumbBrush" Color="{StaticResource ThumbColor}"/>
-            
-            <SolidColorBrush x:Key="SelectionFillBrush" Color="{StaticResource SelectionFillColor}"/>
-            
+            <SolidColorBrush x:Key="GlyphBrush" Color="{StaticResource GlyphColor}" />
+            <SolidColorBrush x:Key="ThumbBrush" Color="{StaticResource ThumbColor}" />
+
+            <SolidColorBrush x:Key="SelectionFillBrush" Color="{StaticResource SelectionFillColor}" />
+
             <!--                           -->
             <!--                           -->
             <!--     <| Node Brushes |>    -->
             <!--     <| Node Brushes |>    -->
             <!--                           -->
             <!--                           -->
-            
+
             <!-- Sockets -->
             <!-- Sockets -->
-            <SolidColorBrush x:Key="DefaultSocketBrush" Color="{StaticResource DefaultSocketColor}"/>
-            <SolidColorBrush x:Key="TextureSocketBrush" Color="{StaticResource ImageSocketColor}"/>
-            <SolidColorBrush x:Key="PainterSocketBrush" Color="{StaticResource PainterSocketColor}"/>
-            <SolidColorBrush x:Key="FilterSocketBrush" Color="{StaticResource FilterSocketColor}"/>
-            <SolidColorBrush x:Key="TextureSamplerSocketBrush" Color="{StaticResource ImageSocketColor}"/>
-            <SolidColorBrush x:Key="BooleanSocketBrush" Color="{StaticResource BoolSocketColor}"/>
-            <SolidColorBrush x:Key="SingleSocketBrush" Color="{StaticResource FloatSocketColor}"/>
-            <SolidColorBrush x:Key="DoubleSocketBrush" Color="{StaticResource DoubleSocketColor}"/>
-            <SolidColorBrush x:Key="Float1SocketBrush" Color="{StaticResource DoubleSocketColor}"/>
-            <SolidColorBrush x:Key="ColorSocketBrush" Color="{StaticResource ColorSocketColor}"/>
-            <SolidColorBrush x:Key="PaintableSocketBrush" Color="{StaticResource PaintableSocketColor}"/>
-            <SolidColorBrush x:Key="Half4SocketBrush" Color="{StaticResource ColorSocketColor}"/>
-            <SolidColorBrush x:Key="VecDSocketBrush" Color="{StaticResource VecDSocketColor}"/>
-            <SolidColorBrush x:Key="Vec3DSocketBrush" Color="{StaticResource Vec3DSocketColor}"/>
-            <SolidColorBrush x:Key="Float2SocketBrush" Color="{StaticResource VecDSocketColor}"/>
-            <SolidColorBrush x:Key="VecISocketBrush" Color="{StaticResource VecISocketColor}"/>
-            <SolidColorBrush x:Key="Int2SocketBrush" Color="{StaticResource VecISocketColor}"/>
-            <SolidColorBrush x:Key="Int32SocketBrush" Color="{StaticResource IntSocketColor}"/>
-            <SolidColorBrush x:Key="Int1SocketBrush" Color="{StaticResource IntSocketColor}"/>
-            <SolidColorBrush x:Key="StringSocketBrush" Color="{StaticResource StringSocketColor}"/>
-            <SolidColorBrush x:Key="Matrix3X3SocketBrush" Color="{StaticResource Matrix3X3SocketColor}"/>
-
-            <ConicGradientBrush x:Key="ShapeVectorDataSocketBrush" GradientStops="{StaticResource ShapeDataSocketGradient}"/>
-            <SolidColorBrush x:Key="EllipseVectorDataSocketBrush" Color="{StaticResource EllipseDataSocketColor}"/>
-            <SolidColorBrush x:Key="PointsVectorDataSocketBrush" Color="{StaticResource PointsDataSocketColor}"/>
-            <SolidColorBrush x:Key="TextVectorDataSocketBrush" Color="{StaticResource TextDataSocketColor}"/>
-            
+            <SolidColorBrush x:Key="DefaultSocketBrush" Color="{StaticResource DefaultSocketColor}" />
+            <SolidColorBrush x:Key="TextureSocketBrush" Color="{StaticResource ImageSocketColor}" />
+            <SolidColorBrush x:Key="PainterSocketBrush" Color="{StaticResource PainterSocketColor}" />
+            <SolidColorBrush x:Key="FilterSocketBrush" Color="{StaticResource FilterSocketColor}" />
+            <SolidColorBrush x:Key="TextureSamplerSocketBrush" Color="{StaticResource ImageSocketColor}" />
+            <SolidColorBrush x:Key="BooleanSocketBrush" Color="{StaticResource BoolSocketColor}" />
+            <SolidColorBrush x:Key="SingleSocketBrush" Color="{StaticResource FloatSocketColor}" />
+            <SolidColorBrush x:Key="DoubleSocketBrush" Color="{StaticResource DoubleSocketColor}" />
+            <SolidColorBrush x:Key="Float1SocketBrush" Color="{StaticResource DoubleSocketColor}" />
+            <SolidColorBrush x:Key="ColorSocketBrush" Color="{StaticResource ColorSocketColor}" />
+            <SolidColorBrush x:Key="PaintableSocketBrush" Color="{StaticResource PaintableSocketColor}" />
+            <SolidColorBrush x:Key="Half4SocketBrush" Color="{StaticResource ColorSocketColor}" />
+            <SolidColorBrush x:Key="VecDSocketBrush" Color="{StaticResource VecDSocketColor}" />
+            <SolidColorBrush x:Key="Vec3DSocketBrush" Color="{StaticResource Vec3DSocketColor}" />
+            <SolidColorBrush x:Key="Float2SocketBrush" Color="{StaticResource VecDSocketColor}" />
+            <SolidColorBrush x:Key="VecISocketBrush" Color="{StaticResource VecISocketColor}" />
+            <SolidColorBrush x:Key="Int2SocketBrush" Color="{StaticResource VecISocketColor}" />
+            <SolidColorBrush x:Key="Int32SocketBrush" Color="{StaticResource IntSocketColor}" />
+            <SolidColorBrush x:Key="Int1SocketBrush" Color="{StaticResource IntSocketColor}" />
+            <SolidColorBrush x:Key="StringSocketBrush" Color="{StaticResource StringSocketColor}" />
+            <SolidColorBrush x:Key="Matrix3X3SocketBrush" Color="{StaticResource Matrix3X3SocketColor}" />
+
+            <ConicGradientBrush x:Key="ShapeVectorDataSocketBrush"
+                                GradientStops="{StaticResource ShapeDataSocketGradient}" />
+            <SolidColorBrush x:Key="EllipseVectorDataSocketBrush" Color="{StaticResource EllipseDataSocketColor}" />
+            <SolidColorBrush x:Key="PointsVectorDataSocketBrush" Color="{StaticResource PointsDataSocketColor}" />
+            <SolidColorBrush x:Key="TextVectorDataSocketBrush" Color="{StaticResource TextDataSocketColor}" />
+
             <!-- Zones & Frames -->
             <!-- Zones & Frames -->
-            <SolidColorBrush x:Key="PixiEditorModifyImageLeftBorderBrush" Color="{StaticResource PixiEditorModifyImageBorderColor}"/>
-            <SolidColorBrush x:Key="PixiEditorModifyImageRightBorderBrush" Color="{StaticResource PixiEditorModifyImageBorderColor}"/>
-            <SolidColorBrush x:Key="PixiEditorModifyImageZoneBorderBrush" Color="{StaticResource PixiEditorModifyImageBorderColor}"/>
-            <SolidColorBrush x:Key="PixiEditorModifyImageZoneBackgroundBrush" Color="{StaticResource PixiEditorModifyImageNodeBackgroundColor}"/>
-            
-            <SolidColorBrush x:Key="PixiEditorColorEvaluatorLeftBorderBrush" Color="{StaticResource PixiEditorColorEvaluatorBorderColor}"/>
-            <SolidColorBrush x:Key="PixiEditorColorEvaluatorRightBorderBrush" Color="{StaticResource PixiEditorColorEvaluatorBorderColor}"/>
-            <SolidColorBrush x:Key="PixiEditorColorEvaluatorZoneBorderBrush" Color="{StaticResource PixiEditorColorEvaluatorBorderColor}"/>
-            <SolidColorBrush x:Key="PixiEditorColorEvaluatorZoneBackgroundBrush" Color="{StaticResource PixiEditorColorEvaluatorNodeBackgroundColor}"/>
-
-            <SolidColorBrush x:Key="NodeFrameBorderBrush" Color="{StaticResource NodeFrameBorderColor}"/>
-            <SolidColorBrush x:Key="NodeFrameBackgroundBrush" Color="{StaticResource NodeFrameBackgroundColor}"/>
-            
+            <SolidColorBrush x:Key="PixiEditorModifyImageLeftBorderBrush"
+                             Color="{StaticResource PixiEditorModifyImageBorderColor}" />
+            <SolidColorBrush x:Key="PixiEditorModifyImageRightBorderBrush"
+                             Color="{StaticResource PixiEditorModifyImageBorderColor}" />
+            <SolidColorBrush x:Key="PixiEditorModifyImageZoneBorderBrush"
+                             Color="{StaticResource PixiEditorModifyImageBorderColor}" />
+            <SolidColorBrush x:Key="PixiEditorModifyImageZoneBackgroundBrush"
+                             Color="{StaticResource PixiEditorModifyImageNodeBackgroundColor}" />
+
+            <SolidColorBrush x:Key="PixiEditorColorEvaluatorLeftBorderBrush"
+                             Color="{StaticResource PixiEditorColorEvaluatorBorderColor}" />
+            <SolidColorBrush x:Key="PixiEditorColorEvaluatorRightBorderBrush"
+                             Color="{StaticResource PixiEditorColorEvaluatorBorderColor}" />
+            <SolidColorBrush x:Key="PixiEditorColorEvaluatorZoneBorderBrush"
+                             Color="{StaticResource PixiEditorColorEvaluatorBorderColor}" />
+            <SolidColorBrush x:Key="PixiEditorColorEvaluatorZoneBackgroundBrush"
+                             Color="{StaticResource PixiEditorColorEvaluatorNodeBackgroundColor}" />
+
+            <SolidColorBrush x:Key="NodeFrameBorderBrush" Color="{StaticResource NodeFrameBorderColor}" />
+            <SolidColorBrush x:Key="NodeFrameBackgroundBrush" Color="{StaticResource NodeFrameBackgroundColor}" />
+
             <!-- Categories -->
             <!-- Categories -->
             <SolidColorBrush x:Key="ImageCategoryBackgroundBrush" Color="{StaticResource ImageCategoryBackgroundColor}" />
             <SolidColorBrush x:Key="ImageCategoryBackgroundBrush" Color="{StaticResource ImageCategoryBackgroundColor}" />
-            <SolidColorBrush x:Key="StructureCategoryBackgroundBrush" Color="{StaticResource StructureCategoryBackgroundColor}" />
-            <SolidColorBrush x:Key="FiltersCategoryBackgroundBrush" Color="{StaticResource FiltersCategoryBackgroundColor}" />
+            <SolidColorBrush x:Key="StructureCategoryBackgroundBrush"
+                             Color="{StaticResource StructureCategoryBackgroundColor}" />
+            <SolidColorBrush x:Key="FiltersCategoryBackgroundBrush"
+                             Color="{StaticResource FiltersCategoryBackgroundColor}" />
             <SolidColorBrush x:Key="ShapeCategoryBackgroundBrush" Color="{StaticResource ShapeCategoryBackgroundColor}" />
             <SolidColorBrush x:Key="ShapeCategoryBackgroundBrush" Color="{StaticResource ShapeCategoryBackgroundColor}" />
-            <SolidColorBrush x:Key="NumbersCategoryBackgroundBrush" Color="{StaticResource NumbersCategoryBackgroundColor}" />
+            <SolidColorBrush x:Key="NumbersCategoryBackgroundBrush"
+                             Color="{StaticResource NumbersCategoryBackgroundColor}" />
             <SolidColorBrush x:Key="ColorCategoryBackgroundBrush" Color="{StaticResource ColorCategoryBackgroundColor}" />
             <SolidColorBrush x:Key="ColorCategoryBackgroundBrush" Color="{StaticResource ColorCategoryBackgroundColor}" />
-            <SolidColorBrush x:Key="AnimationCategoryBackgroundBrush" Color="{StaticResource AnimationCategoryBackgroundColor}" />
-            <SolidColorBrush x:Key="EffectsCategoryBackgroundBrush" Color="{StaticResource EffectsCategoryBackgroundColor}" />
-            <SolidColorBrush x:Key="MatrixCategoryBackgroundBrush" Color="{StaticResource MatrixCategoryBackgroundColor}" />
-
-            <SolidColorBrush x:Key="HorizontalSnapAxisBrush" Color="{StaticResource HorizontalSnapAxisColor}"/>
-            <SolidColorBrush x:Key="VerticalSnapAxisBrush" Color="{StaticResource VerticalSnapAxisColor}"/>
-            <SolidColorBrush x:Key="SnapPointPreviewBrush" Color="{StaticResource SnapPointPreviewColor}"/>
-            
+            <SolidColorBrush x:Key="AnimationCategoryBackgroundBrush"
+                             Color="{StaticResource AnimationCategoryBackgroundColor}" />
+            <SolidColorBrush x:Key="EffectsCategoryBackgroundBrush"
+                             Color="{StaticResource EffectsCategoryBackgroundColor}" />
+            <SolidColorBrush x:Key="MatrixCategoryBackgroundBrush"
+                             Color="{StaticResource MatrixCategoryBackgroundColor}" />
+
+            <SolidColorBrush x:Key="HorizontalSnapAxisBrush" Color="{StaticResource HorizontalSnapAxisColor}" />
+            <SolidColorBrush x:Key="VerticalSnapAxisBrush" Color="{StaticResource VerticalSnapAxisColor}" />
+            <SolidColorBrush x:Key="SnapPointPreviewBrush" Color="{StaticResource SnapPointPreviewColor}" />
+
             <CornerRadius x:Key="ControlCornerRadius">5</CornerRadius>
             <CornerRadius x:Key="ControlCornerRadius">5</CornerRadius>
             <CornerRadius x:Key="ControlCornerRadiusTop">5, 5, 0, 0</CornerRadius>
             <CornerRadius x:Key="ControlCornerRadiusTop">5, 5, 0, 0</CornerRadius>
             <system:Double x:Key="ControlCornerRadiusValue">5</system:Double>
             <system:Double x:Key="ControlCornerRadiusValue">5</system:Double>
@@ -202,14 +222,15 @@
             <system:Double x:Key="ScrollBarThickness">12</system:Double>
             <system:Double x:Key="ScrollBarThickness">12</system:Double>
             <system:Double x:Key="ScrollBarThumbThickness">8</system:Double>
             <system:Double x:Key="ScrollBarThumbThickness">8</system:Double>
 
 
-            <SolidColorBrush x:Key="DockApplicationAccentBrushLow" Color="{DynamicResource AccentLowColor}"/>
-            <SolidColorBrush x:Key="DockApplicationAccentBrushMed" Color="{DynamicResource AccentColor}"/>
-            <SolidColorBrush x:Key="DockApplicationAccentBrushHigh" Color="{DynamicResource AccentHighColor}"/>
-            <SolidColorBrush x:Key="DockApplicationAccentForegroundBrush" Color="{DynamicResource ThemeForegroundColor}"/>
-            <SolidColorBrush x:Key="DockApplicationAccentBrushIndicator" Color="{DynamicResource AccentColor}"/>
+            <SolidColorBrush x:Key="DockApplicationAccentBrushLow" Color="{DynamicResource AccentLowColor}" />
+            <SolidColorBrush x:Key="DockApplicationAccentBrushMed" Color="{DynamicResource AccentColor}" />
+            <SolidColorBrush x:Key="DockApplicationAccentBrushHigh" Color="{DynamicResource AccentHighColor}" />
+            <SolidColorBrush x:Key="DockApplicationAccentForegroundBrush"
+                             Color="{DynamicResource ThemeForegroundColor}" />
+            <SolidColorBrush x:Key="DockApplicationAccentBrushIndicator" Color="{DynamicResource AccentColor}" />
 
 
-            <SolidColorBrush x:Key="UnsavedDotBrush" Color="{DynamicResource UnsavedDotColor}"/>
-            <SolidColorBrush x:Key="AutosaveDotBrush" Color="{DynamicResource AutosaveDotColor}"/>
+            <SolidColorBrush x:Key="UnsavedDotBrush" Color="{DynamicResource UnsavedDotColor}" />
+            <SolidColorBrush x:Key="AutosaveDotBrush" Color="{DynamicResource AutosaveDotColor}" />
 
 
             <SolidColorBrush x:Key="DockThemeBorderLowBrush" Color="{DynamicResource ThemeBorderMidColor}" />
             <SolidColorBrush x:Key="DockThemeBorderLowBrush" Color="{DynamicResource ThemeBorderMidColor}" />
             <SolidColorBrush x:Key="DockThemeBackgroundBrush" Color="{DynamicResource ThemeBackgroundColor}" />
             <SolidColorBrush x:Key="DockThemeBackgroundBrush" Color="{DynamicResource ThemeBackgroundColor}" />

+ 20 - 6
src/PixiEditor.UpdateInstaller.Exe/Program.cs

@@ -4,6 +4,17 @@ using PixiEditor.UpdateInstaller.ViewModels;
 
 
 UpdateController controller = new UpdateController();
 UpdateController controller = new UpdateController();
 StringBuilder log = new StringBuilder();
 StringBuilder log = new StringBuilder();
+bool startAfterUpdate = false;
+
+foreach (string arg in args)
+{
+    if (arg == "--startOnSuccess")
+    {
+        startAfterUpdate = true;
+        log.AppendLine($"{DateTime.Now}: Found --startOnSuccess argument, will start PixiEditor after update.");
+        break;
+    }
+}
 
 
 try
 try
 {
 {
@@ -24,13 +35,16 @@ finally
     }
     }
     catch
     catch
     {
     {
-       // probably permissions or disk full, the best we can do is to ignore this 
+        // probably permissions or disk full, the best we can do is to ignore this
     }
     }
-    
-    var files = Directory.GetFiles(controller.UpdateDirectory, "PixiEditor.exe");
-    if (files.Length > 0)
+
+    if (startAfterUpdate)
     {
     {
-        string pixiEditorExecutablePath = files[0];
-        Process.Start(pixiEditorExecutablePath);
+        var files = Directory.GetFiles(controller.UpdateDirectory, "PixiEditor.exe");
+        if (files.Length > 0)
+        {
+            string pixiEditorExecutablePath = files[0];
+            Process.Start(pixiEditorExecutablePath);
+        }
     }
     }
 }
 }

+ 13 - 3
src/PixiEditor.UpdateModule/UpdateInstaller.cs

@@ -24,7 +24,7 @@ public class UpdateInstaller
 
 
     public void Install(StringBuilder log)
     public void Install(StringBuilder log)
     {
     {
-        var processes = Process.GetProcessesByName("PixiEditor");
+        var processes = Process.GetProcessesByName("PixiEditor.Desktop");
         log.AppendLine($"Found {processes.Length} PixiEditor processes running.");
         log.AppendLine($"Found {processes.Length} PixiEditor processes running.");
         if (processes.Length > 0)
         if (processes.Length > 0)
         {
         {
@@ -40,8 +40,18 @@ public class UpdateInstaller
         log.AppendLine("Files extracted");
         log.AppendLine("Files extracted");
         string dirWithFiles = Directory.GetDirectories(UpdateFilesPath)[0];
         string dirWithFiles = Directory.GetDirectories(UpdateFilesPath)[0];
         log.AppendLine($"Copying files from {dirWithFiles} to {TargetDirectory}");
         log.AppendLine($"Copying files from {dirWithFiles} to {TargetDirectory}");
-        
-        CopyFilesToDestination(dirWithFiles, log);
+
+        try
+        {
+            CopyFilesToDestination(dirWithFiles, log);
+        }
+        catch (IOException ex)
+        {
+            log.AppendLine($"Error copying files: {ex.Message}. Retrying in 1 second...");
+            System.Threading.Thread.Sleep(1000);
+            CopyFilesToDestination(dirWithFiles, log);
+        }
+
         log.AppendLine("Files copied");
         log.AppendLine("Files copied");
         log.AppendLine("Deleting archive and update files");
         log.AppendLine("Deleting archive and update files");
         
         

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

@@ -7,17 +7,18 @@ namespace PixiEditor.Windows;
 
 
 public class WindowsProcessUtility : IProcessUtility
 public class WindowsProcessUtility : IProcessUtility
 {
 {
-    public Process RunAsAdmin(string path)
+    public Process RunAsAdmin(string path, string args)
     {
     {
-        return RunAsAdmin(path, true);
+        return RunAsAdmin(path, args, true);
     }
     }
 
 
-    public Process RunAsAdmin(string path, bool createWindow)
+    public Process RunAsAdmin(string path, string args, bool createWindow)
     {
     {
         ProcessStartInfo startInfo = new ProcessStartInfo
         ProcessStartInfo startInfo = new ProcessStartInfo
         {
         {
             FileName = path,
             FileName = path,
             Verb = "runas",
             Verb = "runas",
+            Arguments = args,
             UseShellExecute = createWindow,
             UseShellExecute = createWindow,
             CreateNoWindow = !createWindow,
             CreateNoWindow = !createWindow,
             RedirectStandardOutput = !createWindow,
             RedirectStandardOutput = !createWindow,

+ 12 - 4
src/PixiEditor/Data/Localization/Languages/en.json

@@ -55,9 +55,7 @@
   "APPLY_TRANSFORM": "Apply transform",
   "APPLY_TRANSFORM": "Apply transform",
   "INCREASE_TOOL_SIZE": "Increase tool size",
   "INCREASE_TOOL_SIZE": "Increase tool size",
   "DECREASE_TOOL_SIZE": "Decrease tool size",
   "DECREASE_TOOL_SIZE": "Decrease tool size",
-  "TO_INSTALL_UPDATE": "to install update {0}",
-  "DOWNLOADING_UPDATE": "Downloading update...",
-  "UPDATE_READY": "Update is ready to be installed. Do you want to install it now?",
+ "UPDATE_READY": "Update is ready to be installed. Do you want to install it now?",
   "NEW_UPDATE": "New update",
   "NEW_UPDATE": "New update",
   "COULD_NOT_UPDATE_WITHOUT_ADMIN": "Couldn't update without admin privileges. Please run PixiEditor as administrator.",
   "COULD_NOT_UPDATE_WITHOUT_ADMIN": "Couldn't update without admin privileges. Please run PixiEditor as administrator.",
   "INSUFFICIENT_PERMISSIONS": "Insufficient permissions",
   "INSUFFICIENT_PERMISSIONS": "Insufficient permissions",
@@ -989,5 +987,15 @@
   "ERR_RENDERING_FAILED": "Rendering failed",
   "ERR_RENDERING_FAILED": "Rendering failed",
   "ENABLE_ANALYTICS": "Send anonymous analytics",
   "ENABLE_ANALYTICS": "Send anonymous analytics",
   "ANALYTICS_INFO": "We collect anonymous usage data to improve PixiEditor. No personal data is collected.",
   "ANALYTICS_INFO": "We collect anonymous usage data to improve PixiEditor. No personal data is collected.",
-  "LANGUAGE_INFO": "All translations are community-driven. Join our Discord server for more information."
+  "LANGUAGE_INFO": "All translations are community-driven. Join our Discord server for more information.",
+  "UP_TO_DATE_UNKNOWN": "Couldn't check for updates",
+  "UP_TO_DATE": "PixiEditor is up to date",
+  "UPDATE_AVAILABLE": "Update {0} is available",
+  "CHECKING_UPDATES": "Checking for updates...",
+  "UPDATE_FAILED_DOWNLOAD": "Failed to download the update",
+  "UPDATE_READY_TO_INSTALL": "Update is ready. Switch to {0}?",
+  "SWITCH_TO_NEW_VERSION": "Switch",
+  "DOWNLOAD_UPDATE": "Download",
+  "DOWNLOADING_UPDATE": "Downloading update...",
+  "CHECKING_FOR_UPDATES": "Checking for updates..."
 }
 }

+ 4 - 4
src/PixiEditor/Helpers/ProcessHelper.cs

@@ -5,9 +5,9 @@ namespace PixiEditor.Helpers;
 
 
 internal static class ProcessHelper
 internal static class ProcessHelper
 {
 {
-    public static Process RunAsAdmin(string path)
+    public static Process RunAsAdmin(string path, string? args = null)
     {
     {
-        return IOperatingSystem.Current.ProcessUtility.RunAsAdmin(path);
+        return IOperatingSystem.Current.ProcessUtility.RunAsAdmin(path, args);
     }
     }
 
 
     public static bool IsRunningAsAdministrator()
     public static bool IsRunningAsAdministrator()
@@ -15,8 +15,8 @@ internal static class ProcessHelper
         return IOperatingSystem.Current.ProcessUtility.IsRunningAsAdministrator();
         return IOperatingSystem.Current.ProcessUtility.IsRunningAsAdministrator();
     }
     }
 
 
-    public static void RunAsAdmin(string updaterPath, bool showWindow)
+    public static void RunAsAdmin(string path, string? args, bool showWindow)
     {
     {
-        IOperatingSystem.Current.ProcessUtility.RunAsAdmin(updaterPath, showWindow);
+        IOperatingSystem.Current.ProcessUtility.RunAsAdmin(path, args, showWindow);
     }
     }
 }
 }

+ 8 - 1
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/DrawableShapeToolExecutor.cs

@@ -170,7 +170,14 @@ internal abstract class DrawableShapeToolExecutor<T> : SimpleShapeToolExecutor w
     private ShapeData ShapeDataFromCorners(ShapeCorners corners)
     private ShapeData ShapeDataFromCorners(ShapeCorners corners)
     {
     {
         var rect = RectD.FromCenterAndSize(corners.RectCenter, corners.RectSize);
         var rect = RectD.FromCenterAndSize(corners.RectCenter, corners.RectSize);
-        ShapeData shapeData = new ShapeData(rect.Center, rect.Size, corners.RectRotation, (float)StrokeWidth,
+        double cornerRadius = 0;
+        if (toolViewModel is ICornerRadiusTool cornerRadiusTool)
+        {
+            cornerRadius = cornerRadiusTool.CornerRadius;
+        }
+
+        ShapeData shapeData = new ShapeData(rect.Center, rect.Size, cornerRadius, corners.RectRotation,
+            (float)StrokeWidth,
             StrokePaintable,
             StrokePaintable,
             FillPaintable) { AntiAliasing = toolbar.AntiAliasing };
             FillPaintable) { AntiAliasing = toolbar.AntiAliasing };
         return shapeData;
         return shapeData;

+ 2 - 2
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/RasterRectangleToolExecutor.cs

@@ -26,7 +26,7 @@ internal class RasterRectangleToolExecutor : DrawableShapeToolExecutor<IRasterRe
         lastRect = (RectD)rect;
         lastRect = (RectD)rect;
         lastRadians = rotationRad;
         lastRadians = rotationRad;
 
 
-        lastData = new ShapeData(rect.Center, rect.Size, rotationRad, (float)StrokeWidth, StrokePaintable, FillPaintable)
+        lastData = new ShapeData(rect.Center, rect.Size, toolViewModel.CornerRadius, rotationRad, (float)StrokeWidth, StrokePaintable, FillPaintable)
         {
         {
             AntiAliasing = toolbar.AntiAliasing
             AntiAliasing = toolbar.AntiAliasing
         };
         };
@@ -43,7 +43,7 @@ internal class RasterRectangleToolExecutor : DrawableShapeToolExecutor<IRasterRe
 
 
     protected override IAction SettingsChangedAction(string name, object value)
     protected override IAction SettingsChangedAction(string name, object value)
     {
     {
-        lastData = new ShapeData(lastData.Center, lastData.Size, lastRadians, (float)StrokeWidth, StrokePaintable, FillPaintable)
+        lastData = new ShapeData(lastData.Center, lastData.Size, toolViewModel.CornerRadius, lastRadians, (float)StrokeWidth, StrokePaintable, FillPaintable)
         {
         {
             AntiAliasing = toolbar.AntiAliasing
             AntiAliasing = toolbar.AntiAliasing
         };
         };

+ 6 - 0
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorRectangleToolExecutor.cs

@@ -44,6 +44,8 @@ internal class VectorRectangleToolExecutor : DrawableShapeToolExecutor<IVectorRe
         firstCenter = rectData.Center;
         firstCenter = rectData.Center;
         firstSize = rectData.Size;
         firstSize = rectData.Size;
         lastMatrix = rectData.TransformationMatrix;
         lastMatrix = rectData.TransformationMatrix;
+        toolViewModel.CornerRadius = (float)rectData.CornerRadius;
+
         return true;
         return true;
     }
     }
 
 
@@ -79,6 +81,7 @@ internal class VectorRectangleToolExecutor : DrawableShapeToolExecutor<IVectorRe
         RectangleVectorData data = new RectangleVectorData(firstCenter, firstSize)
         RectangleVectorData data = new RectangleVectorData(firstCenter, firstSize)
         {
         {
             Stroke = StrokePaintable, FillPaintable = FillPaintable, StrokeWidth = (float)StrokeWidth,
             Stroke = StrokePaintable, FillPaintable = FillPaintable, StrokeWidth = (float)StrokeWidth,
+            CornerRadius = toolViewModel.CornerRadius,
         };
         };
 
 
         lastRect = rect;
         lastRect = rect;
@@ -94,6 +97,7 @@ internal class VectorRectangleToolExecutor : DrawableShapeToolExecutor<IVectorRe
             nameof(IFillableShapeToolbar.Fill) => VectorShapeChangeType.Fill,
             nameof(IFillableShapeToolbar.Fill) => VectorShapeChangeType.Fill,
             nameof(IFillableShapeToolbar.FillBrush) => VectorShapeChangeType.Fill,
             nameof(IFillableShapeToolbar.FillBrush) => VectorShapeChangeType.Fill,
             nameof(IShapeToolbar.StrokeBrush) => VectorShapeChangeType.Stroke,
             nameof(IShapeToolbar.StrokeBrush) => VectorShapeChangeType.Stroke,
+            "Radius" => VectorShapeChangeType.GeometryData,
             nameof(IShapeToolbar.ToolSize) => VectorShapeChangeType.Stroke,
             nameof(IShapeToolbar.ToolSize) => VectorShapeChangeType.Stroke,
             nameof(IShapeToolbar.AntiAliasing) => VectorShapeChangeType.OtherVisuals,
             nameof(IShapeToolbar.AntiAliasing) => VectorShapeChangeType.OtherVisuals,
             "FillAndStroke" => VectorShapeChangeType.Fill | VectorShapeChangeType.Stroke,
             "FillAndStroke" => VectorShapeChangeType.Fill | VectorShapeChangeType.Stroke,
@@ -105,6 +109,7 @@ internal class VectorRectangleToolExecutor : DrawableShapeToolExecutor<IVectorRe
                 Stroke = StrokePaintable,
                 Stroke = StrokePaintable,
                 FillPaintable = FillPaintable,
                 FillPaintable = FillPaintable,
                 StrokeWidth = (float)StrokeWidth,
                 StrokeWidth = (float)StrokeWidth,
+                CornerRadius = toolViewModel.CornerRadius,
                 TransformationMatrix = lastMatrix
                 TransformationMatrix = lastMatrix
             }, changeType);
             }, changeType);
     }
     }
@@ -141,6 +146,7 @@ internal class VectorRectangleToolExecutor : DrawableShapeToolExecutor<IVectorRe
             Stroke = data.Stroke,
             Stroke = data.Stroke,
             FillPaintable = data.FillPaintable,
             FillPaintable = data.FillPaintable,
             StrokeWidth = data.StrokeWidth,
             StrokeWidth = data.StrokeWidth,
+            CornerRadius = data.CornerRadius,
             TransformationMatrix = matrix
             TransformationMatrix = matrix
         };
         };
 
 

+ 8 - 0
src/PixiEditor/Models/Handlers/Tools/ICornerRadiusTool.cs

@@ -0,0 +1,8 @@
+using Drawie.Numerics;
+
+namespace PixiEditor.Models.Handlers.Tools;
+
+public interface ICornerRadiusTool
+{
+    public float CornerRadius { get; set; }
+}

+ 4 - 2
src/PixiEditor/Models/Handlers/Tools/IRasterRectangleToolHandler.cs

@@ -1,5 +1,7 @@
-namespace PixiEditor.Models.Handlers.Tools;
+using Drawie.Numerics;
 
 
-internal interface IRasterRectangleToolHandler : IShapeToolHandler
+namespace PixiEditor.Models.Handlers.Tools;
+
+internal interface IRasterRectangleToolHandler : IShapeToolHandler, ICornerRadiusTool
 {
 {
 }
 }

+ 4 - 2
src/PixiEditor/Models/Handlers/Tools/IVectorRectangleToolHandler.cs

@@ -1,5 +1,7 @@
-namespace PixiEditor.Models.Handlers.Tools;
+using Drawie.Numerics;
 
 
-internal interface IVectorRectangleToolHandler : IShapeToolHandler
+namespace PixiEditor.Models.Handlers.Tools;
+
+internal interface IVectorRectangleToolHandler : IShapeToolHandler, ICornerRadiusTool
 {
 {
 }
 }

+ 12 - 1
src/PixiEditor/Models/IO/CustomDocumentFormats/SvgDocumentBuilder.cs

@@ -333,9 +333,20 @@ internal class SvgDocumentBuilder : IDocumentBuilder
 
 
     private RectangleVectorData AddRect(SvgRectangle element)
     private RectangleVectorData AddRect(SvgRectangle element)
     {
     {
+        double rx = element.Rx.Unit?.PixelsValue ?? 0;
+        double ry = element.Ry.Unit?.PixelsValue ?? 0;
+        double width = element.Width.Unit?.PixelsValue ?? 0;
+        double height = element.Height.Unit?.PixelsValue ?? 0;
+
+        double shortestAxis = Math.Min(width, height);
+
+        double cornerRadius = Math.Max(rx, ry);
+        double cornerRadiusPercent = cornerRadius / (shortestAxis / 2f);
+
         return new RectangleVectorData(
         return new RectangleVectorData(
             element.X.Unit?.PixelsValue ?? 0, element.Y.Unit?.PixelsValue ?? 0,
             element.X.Unit?.PixelsValue ?? 0, element.Y.Unit?.PixelsValue ?? 0,
-            element.Width.Unit?.PixelsValue ?? 0, element.Height.Unit?.PixelsValue ?? 0);
+            width, height)
+            { CornerRadius = cornerRadiusPercent };
     }
     }
 
 
     private TextVectorData AddText(SvgText element)
     private TextVectorData AddText(SvgText element)

+ 10 - 1
src/PixiEditor/ViewModels/Menu/MenuBarViewModel.cs

@@ -12,6 +12,7 @@ using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Extensions.UI;
 using PixiEditor.Extensions.UI;
 using PixiEditor.Models.Commands;
 using PixiEditor.Models.Commands;
 using PixiEditor.OperatingSystem;
 using PixiEditor.OperatingSystem;
+using PixiEditor.ViewModels.SubViewModels;
 using PixiEditor.ViewModels.SubViewModels.AdditionalContent;
 using PixiEditor.ViewModels.SubViewModels.AdditionalContent;
 using Command = PixiEditor.Models.Commands.Commands.Command;
 using Command = PixiEditor.Models.Commands.Commands.Command;
 using Commands_Command = PixiEditor.Models.Commands.Commands.Command;
 using Commands_Command = PixiEditor.Models.Commands.Commands.Command;
@@ -22,6 +23,7 @@ namespace PixiEditor.ViewModels.Menu;
 internal class MenuBarViewModel : PixiObservableObject
 internal class MenuBarViewModel : PixiObservableObject
 {
 {
     private AdditionalContentViewModel additionalContentViewModel;
     private AdditionalContentViewModel additionalContentViewModel;
+    private UpdateViewModel updateViewModel;
 
 
     public AdditionalContentViewModel AdditionalContentSubViewModel
     public AdditionalContentViewModel AdditionalContentSubViewModel
     {
     {
@@ -29,6 +31,12 @@ internal class MenuBarViewModel : PixiObservableObject
         set => SetProperty(ref additionalContentViewModel, value);
         set => SetProperty(ref additionalContentViewModel, value);
     }
     }
 
 
+    public UpdateViewModel UpdateViewModel
+    {
+        get => updateViewModel;
+        set => SetProperty(ref updateViewModel, value);
+    }
+
     public ObservableCollection<MenuItem>? MenuEntries { get; set; }
     public ObservableCollection<MenuItem>? MenuEntries { get; set; }
     public NativeMenu? NativeMenu { get; private set; }
     public NativeMenu? NativeMenu { get; private set; }
 
 
@@ -47,9 +55,10 @@ internal class MenuBarViewModel : PixiObservableObject
         { "DEBUG", 1000 },
         { "DEBUG", 1000 },
     };
     };
 
 
-    public MenuBarViewModel(AdditionalContentViewModel? additionalContentSubViewModel)
+    public MenuBarViewModel(AdditionalContentViewModel? additionalContentSubViewModel, UpdateViewModel? updateViewModel)
     {
     {
         AdditionalContentSubViewModel = additionalContentSubViewModel;
         AdditionalContentSubViewModel = additionalContentSubViewModel;
+        UpdateViewModel = updateViewModel;
     }
     }
 
 
     public void Init(IServiceProvider serviceProvider, CommandController controller)
     public void Init(IServiceProvider serviceProvider, CommandController controller)

+ 159 - 126
src/PixiEditor/ViewModels/SubViewModels/UpdateViewModel.cs

@@ -7,6 +7,8 @@ using System.Text;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using Avalonia;
 using Avalonia;
 using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Threading;
+using CommunityToolkit.Mvvm.Input;
 using PixiEditor.Views.Dialogs;
 using PixiEditor.Views.Dialogs;
 using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Extensions.CommonApi.UserPreferences.Settings.PixiEditor;
 using PixiEditor.Extensions.CommonApi.UserPreferences.Settings.PixiEditor;
@@ -23,12 +25,27 @@ namespace PixiEditor.ViewModels.SubViewModels;
 internal class UpdateViewModel : SubViewModel<ViewModelMain>
 internal class UpdateViewModel : SubViewModel<ViewModelMain>
 {
 {
     public const int MaxRetryCount = 3;
     public const int MaxRetryCount = 3;
-    private bool updateReadyToInstall = false;
-
     public UpdateChecker UpdateChecker { get; set; }
     public UpdateChecker UpdateChecker { get; set; }
 
 
     public List<UpdateChannel> UpdateChannels { get; } = new List<UpdateChannel>();
     public List<UpdateChannel> UpdateChannels { get; } = new List<UpdateChannel>();
 
 
+    private UpdateState _updateState = UpdateState.Checking;
+
+    public UpdateState UpdateState
+    {
+        get => _updateState;
+        set
+        {
+            _updateState = value;
+            OnPropertyChanged(nameof(UpdateState));
+            OnPropertyChanged(nameof(IsUpdateAvailable));
+            OnPropertyChanged(nameof(UpdateReadyToInstall));
+            OnPropertyChanged(nameof(IsDownloading));
+            OnPropertyChanged(nameof(IsUpToDate));
+            OnPropertyChanged(nameof(UpdateStateString));
+        }
+    }
+
     private string versionText;
     private string versionText;
 
 
     public string VersionText
     public string VersionText
@@ -41,26 +58,70 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
         }
         }
     }
     }
 
 
-    public bool UpdateReadyToInstall
+
+    public string UpdateStateString
     {
     {
-        get => updateReadyToInstall;
-        set
+        get
         {
         {
-            updateReadyToInstall = value;
-            OnPropertyChanged(nameof(UpdateReadyToInstall));
-            if (value)
+            if (!SelfUpdatingAvailable)
+                return string.Empty;
+            switch (_updateState)
             {
             {
-                VersionText =
-                    new LocalizedString("TO_INSTALL_UPDATE",
-                        UpdateChecker.LatestReleaseInfo.TagName); // Button shows "Restart" before this text
+                case UpdateState.UnableToCheck:
+                    return new LocalizedString("UP_TO_DATE_UNKNOWN");
+                case UpdateState.Checking:
+                    return new LocalizedString("CHECKING_FOR_UPDATES");
+                case UpdateState.FailedDownload:
+                    return new LocalizedString("UPDATE_FAILED_DOWNLOAD");
+                case UpdateState.ReadyToInstall:
+                    return new LocalizedString("UPDATE_READY_TO_INSTALL", UpdateChecker.LatestReleaseInfo.TagName);
+                case UpdateState.Downloading:
+                    return new LocalizedString("DOWNLOADING_UPDATE");
+                case UpdateState.UpdateAvailable:
+                    return new LocalizedString("UPDATE_AVAILABLE", UpdateChecker.LatestReleaseInfo.TagName);
+                case UpdateState.UpToDate:
+                    return new LocalizedString("UP_TO_DATE");
+                default:
+                    return new LocalizedString("UP_TO_DATE_UNKNOWN");
             }
             }
         }
         }
     }
     }
 
 
+    public bool IsUpdateAvailable
+    {
+        get => _updateState == UpdateState.UpdateAvailable;
+    }
+
+    public bool UpdateReadyToInstall
+    {
+        get => _updateState == UpdateState.ReadyToInstall;
+    }
+
+    public bool IsDownloading
+    {
+        get => _updateState == UpdateState.Downloading;
+    }
+
+    public bool IsUpToDate
+    {
+        get => _updateState == UpdateState.UpToDate;
+    }
+
+    public bool SelfUpdatingAvailable =>
+#if UPDATE
+        PixiEditorSettings.Update.CheckUpdatesOnStartup.Value && OsSupported();
+#else
+        false;
+#endif
+
+    public AsyncRelayCommand DownloadCommand => new AsyncRelayCommand(Download);
+    public RelayCommand InstallCommand => new RelayCommand(Install);
+
     public UpdateViewModel(ViewModelMain owner)
     public UpdateViewModel(ViewModelMain owner)
         : base(owner)
         : base(owner)
     {
     {
         Owner.OnStartupEvent += Owner_OnStartupEvent;
         Owner.OnStartupEvent += Owner_OnStartupEvent;
+        Owner.OnClose += Owner_OnClose;
         PixiEditorSettings.Update.UpdateChannel.ValueChanged += (_, value) =>
         PixiEditorSettings.Update.UpdateChannel.ValueChanged += (_, value) =>
         {
         {
             string prevChannel = UpdateChecker.Channel.ApiUrl;
             string prevChannel = UpdateChecker.Channel.ApiUrl;
@@ -69,83 +130,111 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
             {
             {
                 ConditionalUPDATE();
                 ConditionalUPDATE();
             }
             }
+
+            OnPropertyChanged(nameof(UpdateStateString));
+            OnPropertyChanged(nameof(SelfUpdatingAvailable));
         };
         };
         InitUpdateChecker();
         InitUpdateChecker();
     }
     }
 
 
-    public async Task<bool> CheckForUpdate()
+    public async Task CheckForUpdate()
     {
     {
         if (!IOperatingSystem.Current.IsWindows)
         if (!IOperatingSystem.Current.IsWindows)
         {
         {
-            return false;
+            return;
         }
         }
 
 
         bool updateAvailable = await UpdateChecker.CheckUpdateAvailable();
         bool updateAvailable = await UpdateChecker.CheckUpdateAvailable();
         if (!UpdateChecker.LatestReleaseInfo.WasDataFetchSuccessful ||
         if (!UpdateChecker.LatestReleaseInfo.WasDataFetchSuccessful ||
             string.IsNullOrEmpty(UpdateChecker.LatestReleaseInfo.TagName))
             string.IsNullOrEmpty(UpdateChecker.LatestReleaseInfo.TagName))
         {
         {
-            return false;
+            UpdateState = UpdateState.UnableToCheck;
+            return;
         }
         }
 
 
+        UpdateState = updateAvailable ? UpdateState.UpdateAvailable : UpdateState.UpToDate;
+    }
+
+    private void Owner_OnClose()
+    {
+        if (UpdateState == UpdateState.ReadyToInstall)
+        {
+            Install(false);
+        }
+    }
+
+    public async Task Download()
+    {
         bool updateCompatible = await UpdateChecker.IsUpdateCompatible();
         bool updateCompatible = await UpdateChecker.IsUpdateCompatible();
-        bool autoUpdateFailed = CheckAutoupdateFailed();
-        bool updateFileDoesNotExists = !File.Exists(
-            Path.Join(UpdateDownloader.DownloadLocation, $"update-{UpdateChecker.LatestReleaseInfo.TagName}.zip"));
+        bool updateFileDoesNotExists = !AutoUpdateFileExists();
+        bool updateExeDoesNotExists = !UpdateInstallerFileExists();
 
 
-        bool updateExeDoesNotExists = !File.Exists(
-            Path.Join(UpdateDownloader.DownloadLocation, $"update-{UpdateChecker.LatestReleaseInfo.TagName}.exe"));
-        if (updateAvailable && (updateFileDoesNotExists && updateExeDoesNotExists) || autoUpdateFailed)
+        if (!updateExeDoesNotExists || !updateFileDoesNotExists)
+        {
+            UpdateState = UpdateState.ReadyToInstall;
+            return;
+        }
+
+        if ((updateFileDoesNotExists && updateExeDoesNotExists))
         {
         {
-            UpdateReadyToInstall = false;
-            VersionText = new LocalizedString("DOWNLOADING_UPDATE");
             try
             try
             {
             {
-                if (updateCompatible && !autoUpdateFailed)
+                UpdateState = UpdateState.Downloading;
+                if (updateCompatible)
                 {
                 {
                     await UpdateDownloader.DownloadReleaseZip(UpdateChecker.LatestReleaseInfo);
                     await UpdateDownloader.DownloadReleaseZip(UpdateChecker.LatestReleaseInfo);
                 }
                 }
                 else
                 else
                 {
                 {
                     await UpdateDownloader.DownloadInstaller(UpdateChecker.LatestReleaseInfo);
                     await UpdateDownloader.DownloadInstaller(UpdateChecker.LatestReleaseInfo);
-                    if (autoUpdateFailed)
-                    {
-                        RemoveZipIfExists();
-                    }
                 }
                 }
 
 
-                UpdateReadyToInstall = true;
+                UpdateState = UpdateState.ReadyToInstall;
             }
             }
             catch (IOException ex)
             catch (IOException ex)
             {
             {
-                NoticeDialog.Show("FAILED_DOWNLOADING", "FAILED_DOWNLOADING_TITLE");
-                return false;
+                UpdateState = UpdateState.FailedDownload;
             }
             }
             catch (TaskCanceledException ex)
             catch (TaskCanceledException ex)
             {
             {
-                return false;
+                UpdateState = UpdateState.UpdateAvailable;
+            }
+            catch (Exception ex)
+            {
+                UpdateState = UpdateState.FailedDownload;
             }
             }
-
-            return true;
         }
         }
+    }
 
 
-        return false;
+    private bool AutoUpdateFileExists()
+    {
+        string path = Path.Join(UpdateDownloader.DownloadLocation,
+            $"update-{UpdateChecker.LatestReleaseInfo.TagName}.zip");
+        return File.Exists(path);
     }
     }
 
 
-    private async void Install()
+    private bool UpdateInstallerFileExists()
     {
     {
-#if RELEASE || DEVRELEASE
-        if (!PixiEditorSettings.Update.CheckUpdatesOnStartup.Value)
-        {
-            return;
-        }
+        string path = Path.Join(UpdateDownloader.DownloadLocation,
+            $"update-{UpdateChecker.LatestReleaseInfo.TagName}.exe");
+        return File.Exists(path);
+    }
+
+
+    private void Install()
+    {
+        Install(true);
+    }
 
 
+    private void Install(bool startAfterUpdate)
+    {
+#if RELEASE || DEVRELEASE
         string dir = AppDomain.CurrentDomain.BaseDirectory;
         string dir = AppDomain.CurrentDomain.BaseDirectory;
 
 
         UpdateDownloader.CreateTempDirectory();
         UpdateDownloader.CreateTempDirectory();
         if (UpdateChecker.LatestReleaseInfo == null ||
         if (UpdateChecker.LatestReleaseInfo == null ||
             string.IsNullOrEmpty(UpdateChecker.LatestReleaseInfo.TagName)) return;
             string.IsNullOrEmpty(UpdateChecker.LatestReleaseInfo.TagName)) return;
-        bool updateFileExists = File.Exists(
-            Path.Join(UpdateDownloader.DownloadLocation, $"update-{UpdateChecker.LatestReleaseInfo.TagName}.zip"));
+        bool updateFileExists = AutoUpdateFileExists();
         string exePath = Path.Join(UpdateDownloader.DownloadLocation,
         string exePath = Path.Join(UpdateDownloader.DownloadLocation,
             $"update-{UpdateChecker.LatestReleaseInfo.TagName}.exe");
             $"update-{UpdateChecker.LatestReleaseInfo.TagName}.exe");
 
 
@@ -163,33 +252,14 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
         if (!updateFileExists && !updateExeExists)
         if (!updateFileExists && !updateExeExists)
         {
         {
             EnsureUpdateFilesDeleted();
             EnsureUpdateFilesDeleted();
-            return;
-        }
-
-        ViewModelMain.Current.UpdateSubViewModel.UpdateReadyToInstall = true;
-        if (!UpdateInfoExists())
-        {
-            CreateUpdateInfo(UpdateChecker.LatestReleaseInfo.TagName);
-            return;
-        }
-
-        string[] info = IncrementUpdateInfo();
-
-        if (!UpdateInfoValid(UpdateChecker.LatestReleaseInfo.TagName, info))
-        {
-            File.Delete(Path.Join(Paths.TempFilesPath, "updateInfo.txt"));
-            CreateUpdateInfo(UpdateChecker.LatestReleaseInfo.TagName);
-            return;
-        }
-
-        if (!CanInstallUpdate(UpdateChecker.LatestReleaseInfo.TagName, info) && !updateExeExists)
-        {
+            UpdateState = UpdateState.Checking;
+            Dispatcher.UIThread.InvokeAsync(async () => await CheckForUpdate());
             return;
             return;
         }
         }
 
 
         if (updateFileExists && File.Exists(updaterPath))
         if (updateFileExists && File.Exists(updaterPath))
         {
         {
-            InstallHeadless(updaterPath);
+            InstallHeadless(updaterPath, startAfterUpdate);
         }
         }
         else if (updateExeExists)
         else if (updateExeExists)
         {
         {
@@ -198,11 +268,11 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
 #endif
 #endif
     }
     }
 
 
-    private static void InstallHeadless(string updaterPath)
+    private static void InstallHeadless(string updaterPath, bool startAfterUpdate)
     {
     {
         try
         try
         {
         {
-            ProcessHelper.RunAsAdmin(updaterPath, false);
+            ProcessHelper.RunAsAdmin(updaterPath, startAfterUpdate ? "--startOnSuccess" : null, false);
             Shutdown();
             Shutdown();
         }
         }
         catch (Win32Exception)
         catch (Win32Exception)
@@ -232,7 +302,7 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
     {
     {
         try
         try
         {
         {
-            IOperatingSystem.Current.ProcessUtility.RunAsAdmin(updateExeFile);
+            IOperatingSystem.Current.ProcessUtility.RunAsAdmin(updateExeFile, null);
             Shutdown();
             Shutdown();
         }
         }
         catch (Win32Exception)
         catch (Win32Exception)
@@ -275,6 +345,15 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
             try
             try
             {
             {
                 await CheckForUpdate();
                 await CheckForUpdate();
+                if (UpdateState == UpdateState.UpdateAvailable)
+                {
+                    bool updateFileExists = AutoUpdateFileExists() || UpdateInstallerFileExists();
+                    if (updateFileExists)
+                    {
+                        UpdateState = UpdateState.ReadyToInstall;
+                    }
+                }
+
                 if (UpdateChecker.LatestReleaseInfo != null && UpdateChecker.LatestReleaseInfo.TagName ==
                 if (UpdateChecker.LatestReleaseInfo != null && UpdateChecker.LatestReleaseInfo.TagName ==
                     VersionHelpers.GetCurrentAssemblyVersionString())
                     VersionHelpers.GetCurrentAssemblyVersionString())
                 {
                 {
@@ -283,15 +362,13 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
             }
             }
             catch (System.Net.Http.HttpRequestException)
             catch (System.Net.Http.HttpRequestException)
             {
             {
-                NoticeDialog.Show("COULD_NOT_CHECK_FOR_UPDATES", "UPDATE_CHECK_FAILED");
+                UpdateState = UpdateState.UnableToCheck;
             }
             }
             catch (Exception e)
             catch (Exception e)
             {
             {
+                UpdateState = UpdateState.UnableToCheck;
                 CrashHelper.SendExceptionInfoAsync(e);
                 CrashHelper.SendExceptionInfoAsync(e);
-                NoticeDialog.Show("COULD_NOT_CHECK_FOR_UPDATES", "UPDATE_CHECK_FAILED");
             }
             }
-
-            Install();
         }
         }
     }
     }
 
 
@@ -300,30 +377,6 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
         return IOperatingSystem.Current.IsWindows;
         return IOperatingSystem.Current.IsWindows;
     }
     }
 
 
-    private bool UpdateInfoExists()
-    {
-        return File.Exists(Path.Join(Paths.TempFilesPath, "updateInfo.txt"));
-    }
-
-    private void CreateUpdateInfo(string targetVersion)
-    {
-        StringBuilder sb = new StringBuilder();
-        sb.AppendLine(targetVersion);
-        sb.AppendLine("0");
-        File.WriteAllText(Path.Join(Paths.TempFilesPath, "updateInfo.txt"), sb.ToString());
-    }
-
-    private string[] IncrementUpdateInfo()
-    {
-        string[] lines = File.ReadAllLines(Path.Join(Paths.TempFilesPath, "updateInfo.txt"));
-        int.TryParse(lines[1], out int count);
-        count++;
-        lines[1] = count.ToString();
-        File.WriteAllLines(Path.Join(Paths.TempFilesPath, "updateInfo.txt"), lines);
-
-        return lines;
-    }
-
     private void EnsureUpdateFilesDeleted()
     private void EnsureUpdateFilesDeleted()
     {
     {
         string path = Path.Combine(Paths.TempFilesPath, "updateInfo.txt");
         string path = Path.Combine(Paths.TempFilesPath, "updateInfo.txt");
@@ -333,37 +386,6 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
         }
         }
     }
     }
 
 
-    private bool CanInstallUpdate(string forVersion, string[] lines)
-    {
-        if (lines.Length != 2) return false;
-
-        if (lines[0] != forVersion) return false;
-
-        return int.TryParse(lines[1], out int count) && count < MaxRetryCount;
-    }
-
-    private bool UpdateInfoValid(string forVersion, string[] lines)
-    {
-        if (lines.Length != 2) return false;
-
-        if (lines[0] != forVersion) return false;
-
-        return int.TryParse(lines[1], out _);
-    }
-
-    private bool CheckAutoupdateFailed()
-    {
-        string path = Path.Combine(Paths.TempFilesPath, "updateInfo.txt");
-        if (!File.Exists(path)) return false;
-
-        string[] lines = File.ReadAllLines(path);
-        if (lines.Length != 2) return false;
-
-        if (!int.TryParse(lines[1], out int count)) return false;
-
-        return count >= MaxRetryCount;
-    }
-
     private void RemoveZipIfExists()
     private void RemoveZipIfExists()
     {
     {
         string zipPath = Path.Join(UpdateDownloader.DownloadLocation,
         string zipPath = Path.Join(UpdateDownloader.DownloadLocation,
@@ -397,3 +419,14 @@ internal class UpdateViewModel : SubViewModel<ViewModelMain>
         return selectedChannel;
         return selectedChannel;
     }
     }
 }
 }
+
+public enum UpdateState
+{
+    Checking,
+    UnableToCheck,
+    UpdateAvailable,
+    Downloading,
+    FailedDownload,
+    ReadyToInstall,
+    UpToDate,
+}

+ 0 - 2
src/PixiEditor/ViewModels/Tools/ShapeTool.cs

@@ -15,7 +15,6 @@ internal abstract class ShapeTool : ToolViewModel, IShapeToolHandler
     public override bool IsErasable => true;
     public override bool IsErasable => true;
     public bool DrawEven { get; protected set; }
     public bool DrawEven { get; protected set; }
     public bool DrawFromCenter { get; protected set; }
     public bool DrawFromCenter { get; protected set; }
-    
 
 
     public ShapeTool()
     public ShapeTool()
     {
     {
@@ -23,7 +22,6 @@ internal abstract class ShapeTool : ToolViewModel, IShapeToolHandler
         Toolbar = new FillableShapeToolbar();
         Toolbar = new FillableShapeToolbar();
     }
     }
 
 
-
     protected override void OnDeselecting(bool transient)
     protected override void OnDeselecting(bool transient)
     {
     {
         if (!transient)
         if (!transient)

+ 1 - 0
src/PixiEditor/ViewModels/Tools/ToolSettings/Settings/BoolSettingViewModel.cs

@@ -19,5 +19,6 @@ internal sealed class BoolSettingViewModel : Setting<bool>
     {
     {
         Label = label;
         Label = label;
         Value = isChecked;
         Value = isChecked;
+        IsLabelVisible = false;
     }
     }
 }
 }

+ 3 - 1
src/PixiEditor/ViewModels/Tools/ToolSettings/Settings/Setting.cs

@@ -104,10 +104,12 @@ internal abstract class Setting : ObservableObject
     public string Tooltip { get; set; }
     public string Tooltip { get; set; }
 
 
     public bool HasLabel => !string.IsNullOrEmpty(Label);
     public bool HasLabel => !string.IsNullOrEmpty(Label);
+    public bool IsBuiltInLabelVisible => HasLabel && IsLabelVisible;
     public bool HasIcon => !string.IsNullOrEmpty(Icon);
     public bool HasIcon => !string.IsNullOrEmpty(Icon);
-
     public bool AllowIconLabel { get; protected set; } = true;
     public bool AllowIconLabel { get; protected set; } = true;
 
 
+    public bool IsLabelVisible { get; set; } = true;
+
     public object UserValue
     public object UserValue
     {
     {
         get => _value;
         get => _value;

+ 2 - 2
src/PixiEditor/ViewModels/Tools/ToolSettings/Settings/SizeSettingViewModel.cs

@@ -14,12 +14,12 @@ internal sealed class SizeSettingViewModel : Setting<double>
     private int decimalPlaces;
     private int decimalPlaces;
     private string unit = "px";
     private string unit = "px";
     
     
-    public SizeSettingViewModel(string name, string label = null, double min = 1, double max = double.PositiveInfinity,
+    public SizeSettingViewModel(string name, string label = null, double defaultValue = 1, double min = 1, double max = double.PositiveInfinity,
         int decimalPlaces = 0, string unit = "px")
         int decimalPlaces = 0, string unit = "px")
         : base(name)
         : base(name)
     {
     {
         Label = label;
         Label = label;
-        Value = 1;
+        Value = defaultValue;
 
 
         this.min = min;
         this.min = min;
         this.max = max;
         this.max = max;

+ 3 - 1
src/PixiEditor/ViewModels/Tools/ToolSettings/Toolbars/SettingAttributes.cs

@@ -80,7 +80,9 @@ public static class Settings
     /// </summary>
     /// </summary>
     public class SizeAttribute : SettingsAttribute
     public class SizeAttribute : SettingsAttribute
     {
     {
-        public SizeAttribute(string labelKey) : base(labelKey) { }
+        public SizeAttribute(string labelKey, double defaultValue = 1) : base(labelKey, defaultValue) { }
+
+        public double Min { get; set; } = 1;
     }
     }
 
 
     /// <summary>
     /// <summary>

+ 1 - 1
src/PixiEditor/ViewModels/Tools/ToolSettings/Toolbars/ShapeToolbar.cs

@@ -54,7 +54,7 @@ internal class ShapeToolbar : Toolbar, IShapeToolbar
 
 
     public ShapeToolbar()
     public ShapeToolbar()
     {
     {
-        AddSetting(new SizeSettingViewModel(nameof(ToolSize), "STROKE_WIDTH", 0, decimalPlaces: 2));
+        AddSetting(new SizeSettingViewModel(nameof(ToolSize), "STROKE_WIDTH", 0, min: 0, decimalPlaces: 2));
         AddSetting(new BoolSettingViewModel(nameof(AntiAliasing), "ANTI_ALIASING_LABEL")
         AddSetting(new BoolSettingViewModel(nameof(AntiAliasing), "ANTI_ALIASING_LABEL")
         {
         {
             IsExposed = false, Value = false
             IsExposed = false, Value = false

+ 1 - 1
src/PixiEditor/ViewModels/Tools/ToolSettings/Toolbars/ToolbarFactory.cs

@@ -65,7 +65,7 @@ internal static class ToolbarFactory
                 percentAttribute.Min, percentAttribute.Max),
                 percentAttribute.Min, percentAttribute.Max),
             Settings.FloatAttribute floatAttribute => new FloatSettingViewModel(name, (float)(attribute.DefaultValue ?? 0f), label,
             Settings.FloatAttribute floatAttribute => new FloatSettingViewModel(name, (float)(attribute.DefaultValue ?? 0f), label,
                 floatAttribute.Min, floatAttribute.Max),
                 floatAttribute.Min, floatAttribute.Max),
-            Settings.SizeAttribute => new SizeSettingViewModel(name, label),
+            Settings.SizeAttribute sa => new SizeSettingViewModel(name, label, (double)(sa.DefaultValue ?? 0f), sa.Min),
             _ => throw new NotImplementedException(
             _ => throw new NotImplementedException(
                 $"SettingsAttribute of type '{attribute.GetType().FullName}' has not been implemented")
                 $"SettingsAttribute of type '{attribute.GetType().FullName}' has not been implemented")
         };
         };

+ 22 - 5
src/PixiEditor/ViewModels/Tools/Tools/RasterRectangleToolViewModel.cs

@@ -7,6 +7,7 @@ using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Handlers.Tools;
 using PixiEditor.Models.Handlers.Tools;
 using Drawie.Numerics;
 using Drawie.Numerics;
 using PixiEditor.UI.Common.Fonts;
 using PixiEditor.UI.Common.Fonts;
+using PixiEditor.ViewModels.Tools.ToolSettings.Toolbars;
 
 
 namespace PixiEditor.ViewModels.Tools.Tools;
 namespace PixiEditor.ViewModels.Tools.Tools;
 
 
@@ -15,11 +16,6 @@ internal class RasterRectangleToolViewModel : ShapeTool, IRasterRectangleToolHan
 {
 {
     private string defaultActionDisplay = "RECTANGLE_TOOL_ACTION_DISPLAY_DEFAULT";
     private string defaultActionDisplay = "RECTANGLE_TOOL_ACTION_DISPLAY_DEFAULT";
 
 
-    public RasterRectangleToolViewModel()
-    {
-        ActionDisplay = defaultActionDisplay;
-    }
-
     public override string ToolNameLocalizationKey => "RECTANGLE_TOOL";
     public override string ToolNameLocalizationKey => "RECTANGLE_TOOL";
     public override Type[]? SupportedLayerTypes { get; } = { typeof(IRasterLayerHandler) };
     public override Type[]? SupportedLayerTypes { get; } = { typeof(IRasterLayerHandler) };
     public override LocalizedString Tooltip => new LocalizedString("RECTANGLE_TOOL_TOOLTIP", Shortcut);
     public override LocalizedString Tooltip => new LocalizedString("RECTANGLE_TOOL_TOOLTIP", Shortcut);
@@ -30,6 +26,27 @@ internal class RasterRectangleToolViewModel : ShapeTool, IRasterRectangleToolHan
 
 
     public override Type LayerTypeToCreateOnEmptyUse { get; } = typeof(ImageLayerNode);
     public override Type LayerTypeToCreateOnEmptyUse { get; } = typeof(ImageLayerNode);
 
 
+
+    [Settings.Percent("RADIUS", 0, ExposedByDefault = true, Min = 0)]
+    public float CornerRadius
+    {
+        get
+        {
+            return GetValue<float>();
+        }
+        set
+        {
+            SetValue(value);
+        }
+    }
+
+
+    public RasterRectangleToolViewModel()
+    {
+        ActionDisplay = defaultActionDisplay;
+        Toolbar = ToolbarFactory.Create<RasterRectangleToolViewModel, FillableShapeToolbar>(this);
+    }
+
     public override void KeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown, Key argsKey)
     public override void KeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown, Key argsKey)
     {
     {
         DrawFromCenter = ctrlIsDown;
         DrawFromCenter = ctrlIsDown;

+ 22 - 6
src/PixiEditor/ViewModels/Tools/Tools/VectorRectangleToolViewModel.cs

@@ -9,6 +9,7 @@ using PixiEditor.Models.Handlers;
 using PixiEditor.Models.Handlers.Tools;
 using PixiEditor.Models.Handlers.Tools;
 using Drawie.Numerics;
 using Drawie.Numerics;
 using PixiEditor.UI.Common.Fonts;
 using PixiEditor.UI.Common.Fonts;
+using PixiEditor.ViewModels.Tools.ToolSettings.Toolbars;
 
 
 namespace PixiEditor.ViewModels.Tools.Tools;
 namespace PixiEditor.ViewModels.Tools.Tools;
 
 
@@ -20,12 +21,6 @@ internal class VectorRectangleToolViewModel : ShapeTool, IVectorRectangleToolHan
     private string defaultActionDisplay = "RECTANGLE_TOOL_ACTION_DISPLAY_DEFAULT";
     private string defaultActionDisplay = "RECTANGLE_TOOL_ACTION_DISPLAY_DEFAULT";
     public override string ToolNameLocalizationKey => "RECTANGLE_TOOL";
     public override string ToolNameLocalizationKey => "RECTANGLE_TOOL";
     public override bool IsErasable => false;
     public override bool IsErasable => false;
-
-    public VectorRectangleToolViewModel()
-    {
-        ActionDisplay = defaultActionDisplay;
-    }
-
     public override Type[]? SupportedLayerTypes { get; } = [];
     public override Type[]? SupportedLayerTypes { get; } = [];
     public override LocalizedString Tooltip => new LocalizedString("RECTANGLE_TOOL_TOOLTIP", Shortcut);
     public override LocalizedString Tooltip => new LocalizedString("RECTANGLE_TOOL_TOOLTIP", Shortcut);
 
 
@@ -34,6 +29,27 @@ internal class VectorRectangleToolViewModel : ShapeTool, IVectorRectangleToolHan
     public override Type LayerTypeToCreateOnEmptyUse { get; } = typeof(VectorLayerNode);
     public override Type LayerTypeToCreateOnEmptyUse { get; } = typeof(VectorLayerNode);
     public string? DefaultNewLayerName { get; } = new LocalizedString(NewLayerKey);
     public string? DefaultNewLayerName { get; } = new LocalizedString(NewLayerKey);
 
 
+    private VecD cornerRadius = new VecD(0, 0);
+
+    [Settings.Percent("RADIUS", 0, ExposedByDefault = true, Min = 0)]
+    public float CornerRadius
+    {
+        get
+        {
+            return GetValue<float>();
+        }
+        set
+        {
+            SetValue(value);
+        }
+    }
+
+    public VectorRectangleToolViewModel()
+    {
+        ActionDisplay = defaultActionDisplay;
+        Toolbar = ToolbarFactory.Create<VectorRectangleToolViewModel, FillableShapeToolbar>(this);
+    }
+
     public override void KeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown, Key argsKey)
     public override void KeyChanged(bool ctrlIsDown, bool shiftIsDown, bool altIsDown, Key argsKey)
     {
     {
         DrawFromCenter = ctrlIsDown;
         DrawFromCenter = ctrlIsDown;

+ 4 - 2
src/PixiEditor/ViewModels/ViewModelMain.cs

@@ -33,7 +33,7 @@ internal partial class ViewModelMain : ViewModelBase, ICommandsHandler
     public static ViewModelMain Current { get; set; }
     public static ViewModelMain Current { get; set; }
     public IServiceProvider Services { get; private set; }
     public IServiceProvider Services { get; private set; }
 
 
-    public Action CloseAction { get; set; }
+    public event Action OnClose;
     public event Action OnStartupEvent;
     public event Action OnStartupEvent;
     public FileViewModel FileSubViewModel { get; set; }
     public FileViewModel FileSubViewModel { get; set; }
     public UpdateViewModel UpdateSubViewModel { get; set; }
     public UpdateViewModel UpdateSubViewModel { get; set; }
@@ -147,7 +147,7 @@ internal partial class ViewModelMain : ViewModelBase, ICommandsHandler
         RegistrySubViewModel = services.GetService<RegistryViewModel>();
         RegistrySubViewModel = services.GetService<RegistryViewModel>();
 
 
         AdditionalContentSubViewModel = services.GetService<AdditionalContentViewModel>();
         AdditionalContentSubViewModel = services.GetService<AdditionalContentViewModel>();
-        MenuBarViewModel = new MenuBarViewModel(AdditionalContentSubViewModel);
+        MenuBarViewModel = new MenuBarViewModel(AdditionalContentSubViewModel, UpdateSubViewModel);
 
 
         CommandController.Init(services);
         CommandController.Init(services);
         LayoutSubViewModel.LayoutManager.InitLayout(this);
         LayoutSubViewModel.LayoutManager.InitLayout(this);
@@ -197,6 +197,8 @@ internal partial class ViewModelMain : ViewModelBase, ICommandsHandler
             {
             {
                 await analytics.StopAsync();
                 await analytics.StopAsync();
             }
             }
+
+            OnClose?.Invoke();
         }
         }
     }
     }
 
 

+ 1 - 5
src/PixiEditor/Views/Main/ActionDisplayBar.axaml

@@ -55,15 +55,11 @@
             VerticalAlignment="Center"
             VerticalAlignment="Center"
             Grid.Column="2"
             Grid.Column="2"
             Orientation="Horizontal">
             Orientation="Horizontal">
-            <Button
-                IsVisible="{Binding DataContext.UpdateSubViewModel.UpdateReadyToInstall, FallbackValue=False, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}"
-                Background="{DynamicResource ThemeAccentBrush}"
-                Command="{xaml:Command PixiEditor.RestartToUpdate}" ui:Translator.Key="RESTART" />
             <TextBlock
             <TextBlock
                 VerticalAlignment="Center"
                 VerticalAlignment="Center"
                 Padding="10, 0"
                 Padding="10, 0"
                 HorizontalAlignment="Right"
                 HorizontalAlignment="Right"
-                Foreground="White"
+                Foreground="{DynamicResource ThemeForegroundBrush}"
                 FontSize="14"
                 FontSize="14"
                 Text="{Binding DataContext.UpdateSubViewModel.VersionText, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}" />
                 Text="{Binding DataContext.UpdateSubViewModel.VersionText, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}" />
         </StackPanel>
         </StackPanel>

+ 73 - 5
src/PixiEditor/Views/Main/MainTitleBar.axaml

@@ -29,9 +29,9 @@
                 </Border>
                 </Border>
             </dialogs:DialogTitleBar.AdditionalElement>
             </dialogs:DialogTitleBar.AdditionalElement>
         </dialogs:DialogTitleBar>
         </dialogs:DialogTitleBar>
-        <Svg DockPanel.Dock="Left" HorizontalAlignment="Left" Path="/Images/PixiEditorLogo.svg"
-             Width="20" Height="20">
-            <Svg.Margin>
+        <Panel DockPanel.Dock="Left"
+               HorizontalAlignment="Left" Width="20" Height="20">
+            <Panel.Margin>
                 <OnPlatform>
                 <OnPlatform>
                     <OnPlatform.macOS>
                     <OnPlatform.macOS>
                         <Thickness>75, 0, 0, 0</Thickness>
                         <Thickness>75, 0, 0, 0</Thickness>
@@ -40,8 +40,75 @@
                         <Thickness>10, 0, 0, 0</Thickness>
                         <Thickness>10, 0, 0, 0</Thickness>
                     </OnPlatform.Default>
                     </OnPlatform.Default>
                 </OnPlatform>
                 </OnPlatform>
-            </Svg.Margin>
-        </Svg>
+            </Panel.Margin>
+            <ToggleButton Name="LogoButton" Padding="0" BorderThickness="0">
+                <ToggleButton.Background>
+                    <VisualBrush>
+                        <VisualBrush.Visual>
+                            <Svg Path="/Images/PixiEditorLogo.svg" />
+                        </VisualBrush.Visual>
+                    </VisualBrush>
+                </ToggleButton.Background>
+                <ToggleButton.Styles>
+                    <Style Selector="FlyoutPresenter.arrow">
+                        <Setter Property="Cursor" Value="Arrow" />
+                    </Style>
+                </ToggleButton.Styles>
+                <ToggleButton.Flyout>
+                    <Flyout Placement="BottomEdgeAlignedLeft" FlyoutPresenterClasses="arrow">
+                        <Flyout.Content>
+                            <StackPanel Spacing="5" Margin="5">
+                                <TextBlock Text="PixiEditor" Classes="h3" />
+                                <TextBlock Text="{Binding UpdateViewModel.VersionText}" />
+                                <TextBlock IsVisible="{Binding UpdateViewModel.SelfUpdatingAvailable}"
+                                           Text="{Binding UpdateViewModel.UpdateStateString}" />
+                                <Button ui:Translator.Key="DOWNLOAD_UPDATE"
+                                        Background="{DynamicResource ThemeAccentBrush}"
+                                        Command="{Binding UpdateViewModel.DownloadCommand}"
+                                        IsVisible="{Binding UpdateViewModel.IsUpdateAvailable}" />
+                                <Button ui:Translator.Key="SWITCH_TO_NEW_VERSION"
+                                        Background="{DynamicResource ThemeAccentBrush}"
+                                        Command="{Binding UpdateViewModel.InstallCommand}"
+                                        IsVisible="{Binding UpdateViewModel.UpdateReadyToInstall}" />
+                            </StackPanel>
+                        </Flyout.Content>
+                    </Flyout>
+                </ToggleButton.Flyout>
+            </ToggleButton>
+            <Panel ClipToBounds="False" Margin="10, 10, 0, 0"
+                   IsVisible="{Binding UpdateViewModel.SelfUpdatingAvailable}"
+                   IsHitTestVisible="False" VerticalAlignment="Bottom" HorizontalAlignment="Right">
+                <TextBlock Text="!" IsVisible="{Binding UpdateViewModel.IsUpdateAvailable}"
+                           ClipToBounds="False"
+                           FontWeight="SemiBold"
+                           Foreground="Yellow" FontSize="18" />
+                <TextBlock Text="{DynamicResource icon-settings}"
+                           ClipToBounds="False"
+                            Width="16" Height="16"
+                           IsVisible="{Binding UpdateViewModel.IsDownloading}"
+                           Classes="pixi-icon" Foreground="DarkGray" FontSize="16">
+                    <TextBlock.Styles>
+                        <Style Selector="TextBlock">
+                            <Style.Animations>
+                                <Animation Duration="0:0:5" IterationCount="Infinite">
+                                    <KeyFrame Cue="0%">
+                                        <Setter Property="RotateTransform.Angle" Value="0.0" />
+                                    </KeyFrame>
+                                    <KeyFrame Cue="100%">
+                                        <Setter Property="RotateTransform.Angle" Value="360" />
+                                    </KeyFrame>
+                                </Animation>
+                            </Style.Animations>
+                        </Style>
+                    </TextBlock.Styles>
+                </TextBlock>
+                <Ellipse
+                    ClipToBounds="False"
+                    Fill="{DynamicResource ThemeAccent3Brush}" IsVisible="{Binding UpdateViewModel.UpdateReadyToInstall}"
+                    Width="8" Height="8" />
+            </Panel>
+        </Panel>
+
         <StackPanel Orientation="Horizontal">
         <StackPanel Orientation="Horizontal">
             <StackPanel.Margin>
             <StackPanel.Margin>
                 <OnPlatform>
                 <OnPlatform>
@@ -86,6 +153,7 @@
             </Border>
             </Border>
         </StackPanel>
         </StackPanel>
         <main:MiniAnimationPlayer
         <main:MiniAnimationPlayer
+            Name="MiniPlayer"
             ActiveFrame="{Binding ActiveDocument.AnimationDataViewModel.ActiveFrameBindable, Source={viewModels:MainVM DocumentManagerSVM}, Mode=TwoWay}"
             ActiveFrame="{Binding ActiveDocument.AnimationDataViewModel.ActiveFrameBindable, Source={viewModels:MainVM DocumentManagerSVM}, Mode=TwoWay}"
             FramesCount="{Binding ActiveDocument.AnimationDataViewModel.FramesCount, Source={viewModels:MainVM DocumentManagerSVM}}"
             FramesCount="{Binding ActiveDocument.AnimationDataViewModel.FramesCount, Source={viewModels:MainVM DocumentManagerSVM}}"
             IsPlaying="{Binding ActiveDocument.AnimationDataViewModel.IsPlayingBindable, Source={viewModels:MainVM DocumentManagerSVM}, Mode=TwoWay}"
             IsPlaying="{Binding ActiveDocument.AnimationDataViewModel.IsPlayingBindable, Source={viewModels:MainVM DocumentManagerSVM}, Mode=TwoWay}"

+ 20 - 2
src/PixiEditor/Views/Main/MainTitleBar.axaml.cs

@@ -1,14 +1,17 @@
 using Avalonia;
 using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Controls;
 using Avalonia.Input;
 using Avalonia.Input;
+using Avalonia.Interactivity;
 using Avalonia.Markup.Xaml;
 using Avalonia.Markup.Xaml;
 using PixiEditor.OperatingSystem;
 using PixiEditor.OperatingSystem;
 using PixiEditor.ViewModels.Menu;
 using PixiEditor.ViewModels.Menu;
 
 
 namespace PixiEditor.Views.Main;
 namespace PixiEditor.Views.Main;
 
 
-public partial class MainTitleBar : UserControl {
-    
+public partial class MainTitleBar : UserControl
+{
+
+    private MiniAnimationPlayer miniPlayer;
     public MainTitleBar()
     public MainTitleBar()
     {
     {
         InitializeComponent();
         InitializeComponent();
@@ -19,6 +22,21 @@ public partial class MainTitleBar : UserControl {
         AvaloniaXamlLoader.Load(this);
         AvaloniaXamlLoader.Load(this);
     }
     }
 
 
+    protected override void OnLoaded(RoutedEventArgs e)
+    {
+        base.OnLoaded(e);
+        miniPlayer = this.FindControl<MiniAnimationPlayer>("MiniPlayer");
+    }
+
+    protected override void OnSizeChanged(SizeChangedEventArgs e)
+    {
+        base.OnSizeChanged(e);
+        if (miniPlayer != null)
+        {
+            miniPlayer.IsVisible = e.NewSize.Width > 1165;
+        }
+    }
+
     protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
     protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
     {
     {
         base.OnAttachedToVisualTree(e);
         base.OnAttachedToVisualTree(e);

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

@@ -69,7 +69,7 @@
                         <StackPanel IsVisible="{Binding IsExposed}" Orientation="Horizontal"
                         <StackPanel IsVisible="{Binding IsExposed}" Orientation="Horizontal"
                                     VerticalAlignment="Center" Margin="5,0,5,0">
                                     VerticalAlignment="Center" Margin="5,0,5,0">
                             <Label
                             <Label
-                                IsVisible="{Binding HasLabel}" VerticalAlignment="Center"
+                                IsVisible="{Binding IsBuiltInLabelVisible}" VerticalAlignment="Center"
                                 Foreground="{DynamicResource ThemeForegroundBrush}"
                                 Foreground="{DynamicResource ThemeForegroundBrush}"
                                 ui:Translator.TooltipKey="{Binding Tooltip}"
                                 ui:Translator.TooltipKey="{Binding Tooltip}"
                                 ui:Translator.Key="{Binding Label.Key}" />
                                 ui:Translator.Key="{Binding Label.Key}" />

+ 1 - 2
src/PixiEditor/Views/Nodes/Properties/BooleanPropertyView.axaml

@@ -8,7 +8,6 @@
                              mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
                              mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
                              x:Class="PixiEditor.Views.Nodes.Properties.BooleanPropertyView">
                              x:Class="PixiEditor.Views.Nodes.Properties.BooleanPropertyView">
     <StackPanel Orientation="Horizontal" HorizontalAlignment="{Binding IsInput, Converter={converters:BoolToValueConverter FalseValue='Right', TrueValue='Stretch'}}">
     <StackPanel Orientation="Horizontal" HorizontalAlignment="{Binding IsInput, Converter={converters:BoolToValueConverter FalseValue='Right', TrueValue='Stretch'}}">
-        <CheckBox Margin="0,0,4,0" IsVisible="{Binding ShowInputField}" IsChecked="{Binding Value}"/>
-        <TextBlock VerticalAlignment="Center" ui:Translator.Key="{Binding DisplayName}"/>
+        <CheckBox ui:Translator.Key="{Binding DisplayName}" Margin="0,0,4,0" IsVisible="{Binding ShowInputField}" IsChecked="{Binding Value}"/>
     </StackPanel>
     </StackPanel>
 </properties:NodePropertyView>
 </properties:NodePropertyView>

+ 1 - 1
src/PixiEditor/Views/Nodes/Properties/GenericEnumPropertyView.axaml

@@ -16,7 +16,7 @@
     <Grid
     <Grid
         HorizontalAlignment="{Binding IsInput, Converter={converters:BoolToValueConverter FalseValue='Right', TrueValue='Stretch'}}">
         HorizontalAlignment="{Binding IsInput, Converter={converters:BoolToValueConverter FalseValue='Right', TrueValue='Stretch'}}">
         <TextBlock VerticalAlignment="Center" ui:Translator.Key="{Binding DisplayName}" />
         <TextBlock VerticalAlignment="Center" ui:Translator.Key="{Binding DisplayName}" />
-        <ComboBox HorizontalAlignment="Right" MinWidth="100" IsVisible="{Binding ShowInputField}"
+        <ComboBox PointerPressed="InputElement_OnPointerPressed" HorizontalAlignment="Right" MinWidth="100" IsVisible="{Binding ShowInputField}"
                   SelectedIndex="{Binding SelectedIndex, Mode=TwoWay}" ItemsSource="{Binding Values}">
                   SelectedIndex="{Binding SelectedIndex, Mode=TwoWay}" ItemsSource="{Binding Values}">
             <ComboBox.ItemTemplate>
             <ComboBox.ItemTemplate>
                 <DataTemplate>
                 <DataTemplate>

+ 6 - 0
src/PixiEditor/Views/Nodes/Properties/GenericEnumPropertyView.axaml.cs

@@ -1,5 +1,6 @@
 using Avalonia;
 using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Controls;
+using Avalonia.Input;
 using Avalonia.Markup.Xaml;
 using Avalonia.Markup.Xaml;
 
 
 namespace PixiEditor.Views.Nodes.Properties;
 namespace PixiEditor.Views.Nodes.Properties;
@@ -10,4 +11,9 @@ public partial class GenericEnumPropertyView : NodePropertyView
     {
     {
         InitializeComponent();
         InitializeComponent();
     }
     }
+
+    private void InputElement_OnPointerPressed(object? sender, PointerPressedEventArgs e)
+    {
+        e.Handled = true;
+    }
 }
 }

+ 3 - 1
src/PixiEditor/Views/Tools/ToolSettings/Settings/BoolSettingView.axaml

@@ -12,7 +12,9 @@
     </Design.DataContext>
     </Design.DataContext>
 
 
     <Grid>
     <Grid>
-        <CheckBox VerticalAlignment="Center" Focusable="False" IsChecked="{Binding Value, Mode=TwoWay}">
+        <CheckBox VerticalAlignment="Center" Focusable="False"
+                  ui:Translator.Key="{Binding Label}"
+                  IsChecked="{Binding Value, Mode=TwoWay}">
            <CheckBox.IsVisible>
            <CheckBox.IsVisible>
                <MultiBinding Converter="{converters:AllTrueConverter}">
                <MultiBinding Converter="{converters:AllTrueConverter}">
                    <Binding Path="HasLabel"/>
                    <Binding Path="HasLabel"/>

+ 28 - 21
tests/ChunkyImageLibTest/ChunkyImageTests.cs

@@ -9,6 +9,7 @@ using Drawie.Skia;
 using Xunit;
 using Xunit;
 
 
 namespace ChunkyImageLibTest;
 namespace ChunkyImageLibTest;
+
 public class ChunkyImageTests
 public class ChunkyImageTests
 {
 {
     public ChunkyImageTests()
     public ChunkyImageTests()
@@ -17,31 +18,37 @@ public class ChunkyImageTests
         {
         {
             DrawingBackendApi.SetupBackend(new SkiaDrawingBackend(), null);
             DrawingBackendApi.SetupBackend(new SkiaDrawingBackend(), null);
         }
         }
-        catch { }
+        catch
+        {
+        }
     }
     }
 
 
     [Fact]
     [Fact]
     public void Dispose_ComplexImage_ReturnsAllChunks()
     public void Dispose_ComplexImage_ReturnsAllChunks()
     {
     {
-        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));
+        ChunkyImage image = new ChunkyImage(new VecI(ChunkyImage.FullChunkSize, ChunkyImage.FullChunkSize),
+            ColorSpace.CreateSrgb());
+        image.EnqueueDrawRectangle(new(new(5, 5), new(80, 80), 0, 0, 2, Colors.AliceBlue, Colors.Snow));
         using (Chunk target = Chunk.Create(ColorSpace.CreateSrgb()))
         using (Chunk target = Chunk.Create(ColorSpace.CreateSrgb()))
         {
         {
             image.DrawMostUpToDateChunkOn(new(0, 0), ChunkResolution.Full, target.Surface.DrawingSurface, VecI.Zero);
             image.DrawMostUpToDateChunkOn(new(0, 0), ChunkResolution.Full, target.Surface.DrawingSurface, VecI.Zero);
             image.CancelChanges();
             image.CancelChanges();
             image.EnqueueResize(new(ChunkyImage.FullChunkSize * 4, ChunkyImage.FullChunkSize * 4));
             image.EnqueueResize(new(ChunkyImage.FullChunkSize * 4, ChunkyImage.FullChunkSize * 4));
-            image.EnqueueDrawRectangle(new(VecD.Zero, image.CommittedSize, 0, 2, Colors.AliceBlue, Colors.Snow, BlendMode.Multiply));
+            image.EnqueueDrawRectangle(new(VecD.Zero, image.CommittedSize, 0, 0, 2, Colors.AliceBlue, Colors.Snow,
+                BlendMode.Multiply));
             image.CommitChanges();
             image.CommitChanges();
             image.SetBlendMode(BlendMode.Overlay);
             image.SetBlendMode(BlendMode.Overlay);
-            image.EnqueueDrawRectangle(new(VecD.Zero, image.CommittedSize, 0, 2, Colors.AliceBlue, Colors.Snow, BlendMode.Multiply));
-            image.EnqueueDrawRectangle(new(VecD.Zero, image.CommittedSize, 0, 2, Colors.AliceBlue, Colors.Snow));
+            image.EnqueueDrawRectangle(new(VecD.Zero, image.CommittedSize, 0, 0, 2, Colors.AliceBlue, Colors.Snow,
+                BlendMode.Multiply));
+            image.EnqueueDrawRectangle(new(VecD.Zero, image.CommittedSize, 0, 0, 2, Colors.AliceBlue, Colors.Snow));
             image.CommitChanges();
             image.CommitChanges();
             image.SetBlendMode(BlendMode.Screen);
             image.SetBlendMode(BlendMode.Screen);
-            image.EnqueueDrawRectangle(new(VecD.Zero, image.CommittedSize, 0, 2, Colors.AliceBlue, Colors.Snow));
+            image.EnqueueDrawRectangle(new(VecD.Zero, image.CommittedSize, 0, 0, 2, Colors.AliceBlue, Colors.Snow));
             image.CancelChanges();
             image.CancelChanges();
             image.SetBlendMode(BlendMode.SrcOver);
             image.SetBlendMode(BlendMode.SrcOver);
-            image.EnqueueDrawRectangle(new(VecD.Zero, image.CommittedSize, 0, 2, Colors.AliceBlue, Colors.Snow));
+            image.EnqueueDrawRectangle(new(VecD.Zero, image.CommittedSize, 0, 0, 2, Colors.AliceBlue, Colors.Snow));
         }
         }
+
         image.Dispose();
         image.Dispose();
 
 
         Assert.Equal(0, Chunk.ChunkCounter);
         Assert.Equal(0, Chunk.ChunkCounter);
@@ -53,7 +60,7 @@ public class ChunkyImageTests
         const int chunkSize = ChunkyImage.FullChunkSize;
         const int chunkSize = ChunkyImage.FullChunkSize;
         ChunkyImage image = new ChunkyImage(new VecI(chunkSize * 2), ColorSpace.CreateSrgb());
         ChunkyImage image = new ChunkyImage(new VecI(chunkSize * 2), ColorSpace.CreateSrgb());
         image.EnqueueDrawRectangle
         image.EnqueueDrawRectangle
-            (new ShapeData(new VecD(chunkSize), new VecD(chunkSize * 2), 0, 0, Colors.Transparent, Colors.Red));
+            (new ShapeData(new VecD(chunkSize), new VecD(chunkSize * 2), 0, 0, 0, Colors.Transparent, Colors.Red));
         image.CommitChanges();
         image.CommitChanges();
         Assert.Equal(Colors.Red, image.GetCommittedPixel(new VecI(chunkSize + chunkSize / 2)));
         Assert.Equal(Colors.Red, image.GetCommittedPixel(new VecI(chunkSize + chunkSize / 2)));
         image.Dispose();
         image.Dispose();
@@ -66,25 +73,25 @@ public class ChunkyImageTests
         const int chunkSize = ChunkyImage.FullChunkSize;
         const int chunkSize = ChunkyImage.FullChunkSize;
         ChunkyImage image = new ChunkyImage(new VecI(chunkSize * 2), ColorSpace.CreateSrgb());
         ChunkyImage image = new ChunkyImage(new VecI(chunkSize * 2), ColorSpace.CreateSrgb());
         image.EnqueueDrawRectangle
         image.EnqueueDrawRectangle
-            (new ShapeData(new VecD(chunkSize), new VecD(chunkSize * 2), 0, 0, Colors.Transparent, Colors.Red));
+            (new ShapeData(new VecD(chunkSize), new VecD(chunkSize * 2), 0, 0, 0, Colors.Transparent, Colors.Red));
         Assert.Equal(Colors.Red, image.GetMostUpToDatePixel(new VecI(chunkSize + chunkSize / 2)));
         Assert.Equal(Colors.Red, image.GetMostUpToDatePixel(new VecI(chunkSize + chunkSize / 2)));
         image.Dispose();
         image.Dispose();
         Assert.Equal(0, Chunk.ChunkCounter);
         Assert.Equal(0, Chunk.ChunkCounter);
     }
     }
-    
+
     [Fact]
     [Fact]
     public void GetMostUpToDatePixel_BlendModeSrcOver_ReturnsCorrectPixel()
     public void GetMostUpToDatePixel_BlendModeSrcOver_ReturnsCorrectPixel()
     {
     {
         const int chunkSize = ChunkyImage.FullChunkSize;
         const int chunkSize = ChunkyImage.FullChunkSize;
         ChunkyImage image = new ChunkyImage(new VecI(chunkSize * 2), ColorSpace.CreateSrgb());
         ChunkyImage image = new ChunkyImage(new VecI(chunkSize * 2), ColorSpace.CreateSrgb());
         image.EnqueueDrawRectangle
         image.EnqueueDrawRectangle
-            (new ShapeData(new VecD(chunkSize), new VecD(chunkSize * 2), 0, 0, Colors.Transparent, Colors.Red));
+            (new ShapeData(new VecD(chunkSize), new VecD(chunkSize * 2), 0, 0, 0, Colors.Transparent, Colors.Red));
         image.CommitChanges();
         image.CommitChanges();
         image.SetBlendMode(BlendMode.SrcOver);
         image.SetBlendMode(BlendMode.SrcOver);
         image.EnqueueDrawRectangle(new ShapeData(
         image.EnqueueDrawRectangle(new ShapeData(
             new VecD(chunkSize),
             new VecD(chunkSize),
             new VecD(chunkSize * 2),
             new VecD(chunkSize * 2),
-            0, 
+            0, 0,
             0,
             0,
             Colors.Transparent,
             Colors.Transparent,
             new Color(0, 255, 0, 128)));
             new Color(0, 255, 0, 128)));
@@ -99,15 +106,15 @@ public class ChunkyImageTests
         const int chunkSize = ChunkyImage.FullChunkSize;
         const int chunkSize = ChunkyImage.FullChunkSize;
         using ChunkyImage image = new(new VecI(chunkSize), ColorSpace.CreateSrgb());
         using ChunkyImage image = new(new VecI(chunkSize), ColorSpace.CreateSrgb());
         image.EnqueueDrawRectangle(new ShapeData(
         image.EnqueueDrawRectangle(new ShapeData(
-                VecD.Zero,
-                new VecD(chunkSize * 10),
-                0,
-                0,
-                Colors.Transparent,
-                Colors.Red));
+            VecD.Zero,
+            new VecD(chunkSize * 10),
+            0, 0,
+            0,
+            Colors.Transparent,
+            Colors.Red));
         image.CommitChanges();
         image.CommitChanges();
         Assert.Collection(
         Assert.Collection(
-            image.FindAllChunks(), 
+            image.FindAllChunks(),
             elem => Assert.Equal(VecI.Zero, elem));
             elem => Assert.Equal(VecI.Zero, elem));
     }
     }
-}
+}

+ 8 - 7
tests/ChunkyImageLibTest/RectangleOperationTests.cs

@@ -28,7 +28,7 @@ public class RectangleOperationTests
     public void FindAffectedArea_SmallStrokeOnly_FindsCorrectChunks()
     public void FindAffectedArea_SmallStrokeOnly_FindsCorrectChunks()
     {
     {
         var (x, y, w, h) = (chunkSize / 2, chunkSize / 2, chunkSize, chunkSize);
         var (x, y, w, h) = (chunkSize / 2, chunkSize / 2, chunkSize, chunkSize);
-        RectangleOperation operation = new(new(new(x, y), new(w, h), 0, 1, Colors.Black, Colors.Transparent));
+        RectangleOperation operation = new(new(new(x, y), new(w, h), 0, 0, 1, Colors.Black, Colors.Transparent));
 
 
         HashSet<VecI> expected = new() { new(0, 0) };
         HashSet<VecI> expected = new() { new(0, 0) };
         var actual = operation.FindAffectedArea(new(chunkSize)).Chunks;
         var actual = operation.FindAffectedArea(new(chunkSize)).Chunks;
@@ -40,7 +40,7 @@ public class RectangleOperationTests
     public void FindAffectedArea_2by2StrokeOnly_FindsCorrectChunks()
     public void FindAffectedArea_2by2StrokeOnly_FindsCorrectChunks()
     {
     {
         var (x, y, w, h) = (0, 0, chunkSize * 2, chunkSize * 2);
         var (x, y, w, h) = (0, 0, chunkSize * 2, chunkSize * 2);
-        RectangleOperation operation = new(new(new(x, y), new(w, h), 0, 1, Colors.Black, Colors.Transparent));
+        RectangleOperation operation = new(new(new(x, y), new(w, h), 0, 0, 1, Colors.Black, Colors.Transparent));
 
 
         HashSet<VecI> expected = new() { new(-1, -1), new(0, -1), new(-1, 0), new(0, 0) };
         HashSet<VecI> expected = new() { new(-1, -1), new(0, -1), new(-1, 0), new(0, 0) };
         var actual = operation.FindAffectedArea(new(chunkSize)).Chunks;
         var actual = operation.FindAffectedArea(new(chunkSize)).Chunks;
@@ -52,7 +52,7 @@ public class RectangleOperationTests
     public void FindAffectedArea_3x3PositiveStrokeOnly_FindsCorrectChunks()
     public void FindAffectedArea_3x3PositiveStrokeOnly_FindsCorrectChunks()
     {
     {
         var (x, y, w, h) = (2 * chunkSize + chunkSize / 2, 2 * chunkSize + chunkSize / 2, chunkSize * 2, chunkSize * 2);
         var (x, y, w, h) = (2 * chunkSize + chunkSize / 2, 2 * chunkSize + chunkSize / 2, chunkSize * 2, chunkSize * 2);
-        RectangleOperation operation = new(new(new(x, y), new(w, h), 0, 1, Colors.Black, Colors.Transparent));
+        RectangleOperation operation = new(new(new(x, y), new(w, h), 0, 0, 1, Colors.Black, Colors.Transparent));
 
 
         HashSet<VecI> expected = new()
         HashSet<VecI> expected = new()
         {
         {
@@ -70,7 +70,7 @@ public class RectangleOperationTests
     {
     {
         var (x, y, w, h) = (-chunkSize * 2 - chunkSize / 2, -chunkSize * 2 - chunkSize / 2, chunkSize * 2,
         var (x, y, w, h) = (-chunkSize * 2 - chunkSize / 2, -chunkSize * 2 - chunkSize / 2, chunkSize * 2,
             chunkSize * 2);
             chunkSize * 2);
-        RectangleOperation operation = new(new(new(x, y), new(w, h), 0, 1, Colors.Black, Colors.Transparent));
+        RectangleOperation operation = new(new(new(x, y), new(w, h), 0, 0, 1, Colors.Black, Colors.Transparent));
 
 
         HashSet<VecI> expected = new()
         HashSet<VecI> expected = new()
         {
         {
@@ -87,7 +87,7 @@ public class RectangleOperationTests
     public void FindAffectedArea_3x3PositiveFilled_FindsCorrectChunks()
     public void FindAffectedArea_3x3PositiveFilled_FindsCorrectChunks()
     {
     {
         var (x, y, w, h) = (2 * chunkSize + chunkSize / 2, 2 * chunkSize + chunkSize / 2, chunkSize * 2, chunkSize * 2);
         var (x, y, w, h) = (2 * chunkSize + chunkSize / 2, 2 * chunkSize + chunkSize / 2, chunkSize * 2, chunkSize * 2);
-        RectangleOperation operation = new(new(new(x, y), new(w, h), 0, 1, Colors.Black, Colors.White));
+        RectangleOperation operation = new(new(new(x, y), new(w, h), 0, 0, 1, Colors.Black, Colors.White));
 
 
         HashSet<VecI> expected = new()
         HashSet<VecI> expected = new()
         {
         {
@@ -104,7 +104,8 @@ public class RectangleOperationTests
     public void FindAffectedArea_ThickPositiveStroke_FindsCorrectChunks()
     public void FindAffectedArea_ThickPositiveStroke_FindsCorrectChunks()
     {
     {
         var (x, y, w, h) = (2 * chunkSize + chunkSize / 2, 2 * chunkSize + chunkSize / 2, chunkSize * 4, chunkSize * 4);
         var (x, y, w, h) = (2 * chunkSize + chunkSize / 2, 2 * chunkSize + chunkSize / 2, chunkSize * 4, chunkSize * 4);
-        RectangleOperation operation = new(new(new(x, y), new(w, h), 0, chunkSize, Colors.Black, Colors.Transparent));
+        RectangleOperation operation =
+            new(new(new(x, y), new(w, h), 0, 0, chunkSize, Colors.Black, Colors.Transparent));
 
 
         HashSet<VecI> expected = new()
         HashSet<VecI> expected = new()
         {
         {
@@ -123,7 +124,7 @@ public class RectangleOperationTests
     public void FindAffectedArea_SmallButThick_FindsCorrectChunks()
     public void FindAffectedArea_SmallButThick_FindsCorrectChunks()
     {
     {
         var (x, y, w, h) = (chunkSize / 2f - 0.5, chunkSize / 2f - 0.5, 1, 1);
         var (x, y, w, h) = (chunkSize / 2f - 0.5, chunkSize / 2f - 0.5, 1, 1);
-        RectangleOperation operation = new(new(new(x, y), new(w, h), 0, chunkSize, Colors.Black, Colors.White));
+        RectangleOperation operation = new(new(new(x, y), new(w, h), 0, 0, chunkSize, Colors.Black, Colors.White));
 
 
         HashSet<VecI> expected = new() { new(0, 0) };
         HashSet<VecI> expected = new() { new(0, 0) };
         var actual = operation.FindAffectedArea(new(chunkSize)).Chunks;
         var actual = operation.FindAffectedArea(new(chunkSize)).Chunks;