Bläddra i källkod

Merge branch 'master' into rectangle-node

Jakub Ciemała 1 månad sedan
förälder
incheckning
2a4d5b0e92

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit 539a77bffc3ec75ae99d101434280c176fea80df
+Subproject commit 0145d4583f4ac2e20a4658e451e841a9f8397a9e

+ 23 - 2
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ShaderNode.cs

@@ -32,11 +32,26 @@ public class ShaderNode : RenderNode, IRenderInput, ICustomShaderNode
     protected override bool ExecuteOnlyOnCacheChange => true;
     protected override CacheTriggerFlags CacheTrigger => CacheTriggerFlags.All;
 
+    private string defaultShaderCode = """
+                                       // Below is a list of built-in special uniforms that are automatically added by PixiEditor.
+                                       // Any other uniform will be added as a Node input
+                                       
+                                       uniform vec2 iResolution; // The resolution of current render output. It is usually a document size.
+                                       uniform float iNormalizedTime; // The normalized time of the current frame, from 0 to 1.
+                                       uniform int iFrame; // The current frame number.
+                                       uniform shader iImage; // The Background input of the node, alternatively you can use "Background" uniform.
+                                       
+                                       half4 main(float2 uv)
+                                       {
+                                           return half4(1, 1, 1, 1);
+                                       }
+                                       """;
+
     public ShaderNode()
     {
         Background = CreateRenderInput("Background", "BACKGROUND");
         ColorSpace = CreateInput("ColorSpace", "COLOR_SPACE", ColorSpaceType.Inherit);
-        ShaderCode = CreateInput("ShaderCode", "SHADER_CODE", "")
+        ShaderCode = CreateInput("ShaderCode", "SHADER_CODE", defaultShaderCode)
             .WithRules(validator => validator.Custom(ValidateShaderCode))
             .NonOverridenChanged(RegenerateUniformInputs);
 
@@ -118,6 +133,7 @@ public class ShaderNode : RenderNode, IRenderInput, ICustomShaderNode
         lastImageShader = snapshot.ToShader();
 
         uniforms.Add("iImage", new Uniform("iImage", lastImageShader));
+        uniforms.Add("Background", new Uniform("Background", lastImageShader));
 
         snapshot.Dispose();
         //texture.Dispose();
@@ -220,6 +236,11 @@ public class ShaderNode : RenderNode, IRenderInput, ICustomShaderNode
         var uniforms = declarations;
 
         var nonExistingUniforms = uniformInputs.Keys.Where(x => uniforms.All(y => y.Name != x)).ToList();
+        if(nonExistingUniforms.Contains("Background"))
+        {
+            nonExistingUniforms.Remove("Background");
+        }
+
         foreach (var nonExistingUniform in nonExistingUniforms)
         {
             RemoveInputProperty(uniformInputs[nonExistingUniform].prop);
@@ -388,7 +409,7 @@ public class ShaderNode : RenderNode, IRenderInput, ICustomShaderNode
 
     private bool IsBuiltInUniform(string name)
     {
-        return name is "iResolution" or "iNormalizedTime" or "iFrame" or "iImage";
+        return name is "iResolution" or "iNormalizedTime" or "iFrame" or "iImage" or "Background";
     }
 
     private ValidatorResult ValidateShaderCode(object? value)

+ 22 - 8
src/PixiEditor.ChangeableDocument/Changes/Drawing/CombineStructureMembersOnto_Change.cs

@@ -4,6 +4,8 @@ using PixiEditor.ChangeableDocument.Changes.Structure;
 using PixiEditor.ChangeableDocument.Rendering;
 using Drawie.Backend.Core;
 using Drawie.Backend.Core.Bridge;
+using Drawie.Backend.Core.ColorsImpl;
+using Drawie.Backend.Core.ColorsImpl.Paintables;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
@@ -144,6 +146,11 @@ internal class CombineStructureMembersOnto_Change : Change
 
         var ordererd = OrderLayers(layersToCombine, target);
 
+        if (ordererd.Count == 0)
+        {
+            return changes;
+        }
+
         foreach (var guid in ordererd)
         {
             var layer = target.FindMemberOrThrow<StructureNode>(guid);
@@ -215,6 +222,11 @@ internal class CombineStructureMembersOnto_Change : Change
             }
             else
             {
+                if (targetPath == null)
+                {
+                    targetPath = new VectorPath();
+                }
+
                 targetPath.AddPath(path, vectorNode.EmbeddedShapeData.TransformationMatrix, AddPathMode.Append);
                 path.Dispose();
             }
@@ -227,10 +239,10 @@ internal class CombineStructureMembersOnto_Change : Change
             ShapeVectorData shape = clone as ShapeVectorData;
             data = new PathVectorData(targetPath)
             {
-                Stroke = shape.Stroke,
-                FillPaintable = shape.FillPaintable,
-                StrokeWidth = shape.StrokeWidth,
-                Fill = shape.Fill,
+                Stroke = shape?.Stroke,
+                FillPaintable = shape?.FillPaintable,
+                StrokeWidth = shape?.StrokeWidth ?? 1,
+                Fill = shape?.Fill ?? true,
                 TransformationMatrix = Matrix3X3.Identity
             };
         }
@@ -248,9 +260,9 @@ internal class CombineStructureMembersOnto_Change : Change
 
     private AffectedArea RasterMerge(Document target, StructureNode targetLayer, int frame)
     {
-        if(targetLayer is not ImageLayerNode)
+        if (targetLayer is not ImageLayerNode)
             throw new InvalidOperationException("Target layer is not a raster layer");
-        
+
         var toDrawOnImage = ((ImageLayerNode)targetLayer).GetLayerImageAtFrame(frame);
         toDrawOnImage.EnqueueClear();
 
@@ -261,7 +273,8 @@ internal class CombineStructureMembersOnto_Change : Change
         AffectedArea affArea = new();
         DrawingBackendApi.Current.RenderingDispatcher.Invoke(() =>
         {
-            renderer.RenderLayers(tempTexture.DrawingSurface, layersToCombine, frame, ChunkResolution.Full, target.Size);
+            renderer.RenderLayers(tempTexture.DrawingSurface, layersToCombine, frame, ChunkResolution.Full,
+                target.Size);
 
             toDrawOnImage.EnqueueDrawTexture(VecI.Zero, tempTexture);
 
@@ -288,7 +301,8 @@ internal class CombineStructureMembersOnto_Change : Change
         return ordered.Reverse().ToHashSet();
     }
 
-    private void AddMissingKeyFrame(StructureNode targetLayer, int frame, StructureNode layer, List<IChangeInfo> changes,
+    private void AddMissingKeyFrame(StructureNode targetLayer, int frame, StructureNode layer,
+        List<IChangeInfo> changes,
         Document target)
     {
         bool hasKeyframe = targetLayer.KeyFrames.Any(x => x.IsInFrame(frame));

+ 32 - 7
src/PixiEditor.ChangeableDocument/Changes/NodeGraph/NodeOperations.cs

@@ -72,9 +72,11 @@ public static class NodeOperations
         return node;
     }
 
-    public static List<IChangeInfo> AppendMember(Node parent, Node toAppend, out Dictionary<Guid, VecD> originalPositions)
+    public static List<IChangeInfo> AppendMember(Node parent, Node toAppend,
+        out Dictionary<Guid, VecD> originalPositions)
     {
-        InputProperty<Painter?>? parentInput = parent.GetInputProperty(OutputNode.InputPropertyName) as InputProperty<Painter?>;
+        InputProperty<Painter?>? parentInput =
+            parent.GetInputProperty(OutputNode.InputPropertyName) as InputProperty<Painter?>;
         if (parentInput == null)
         {
             throw new InvalidOperationException("Parent node does not have an input property for appending members.");
@@ -86,18 +88,21 @@ public static class NodeOperations
             throw new InvalidOperationException("Node to append does not have an output property named 'Output'.");
         }
 
-        InputProperty<Painter>? toAddInput = toAppend.GetInputProperty(OutputNode.InputPropertyName) as InputProperty<Painter>;
+        InputProperty<Painter>? toAddInput =
+            toAppend.GetInputProperty(OutputNode.InputPropertyName) as InputProperty<Painter>;
 
         if (toAddInput == null)
         {
-            throw new InvalidOperationException("Node to append does not have an input property for appending members.");
+            throw new InvalidOperationException(
+                "Node to append does not have an input property for appending members.");
         }
 
         Guid memberId = toAppend.Id;
 
         List<IChangeInfo> changes = AppendMember(parentInput, toAddOutput, toAddInput, memberId);
 
-        var adjustedPositions = AdjustPositionsAfterAppend(toAppend, parent, parentInput.Connection?.Node as Node ?? null, out originalPositions);
+        var adjustedPositions = AdjustPositionsAfterAppend(toAppend, parent,
+            parentInput.Connection?.Node as Node ?? null, out originalPositions);
 
         changes.AddRange(adjustedPositions);
         return changes;
@@ -215,8 +220,8 @@ public static class NodeOperations
                 toMove.Position = pos;
                 toMove.Position = new VecD(toMove.Position.X, y);
                 changes.Add(new NodePosition_ChangeInfo(toMove.Id, toMove.Position));
-                
-                if(aNode == appendedTo) return false;
+
+                if (aNode == appendedTo) return false;
             }
 
             return true;
@@ -270,15 +275,35 @@ public static class NodeOperations
     public static List<IChangeInfo> ConnectStructureNodeProperties(ConnectionsData originalConnections, Node node,
         IReadOnlyNodeGraph graph)
     {
+        if (node == null || originalConnections == null || graph == null)
+        {
+            return new List<IChangeInfo>();
+        }
+
         List<IChangeInfo> changes = new();
         foreach (var connections in originalConnections.originalOutputConnections)
         {
             PropertyConnection outputConnection = connections.Key;
+            if (outputConnection == null)
+                continue;
+
             IOutputProperty outputProp = node.GetOutputProperty(outputConnection.PropertyName);
+
+            if (outputProp == null)
+            {
+                continue;
+            }
+
             foreach (var connection in connections.Value)
             {
                 var inputNode = graph.AllNodes.FirstOrDefault(x => x.Id == connection.NodeId);
+                if (inputNode is null)
+                    continue;
+
                 IInputProperty property = inputNode.GetInputProperty(connection.PropertyName);
+                if (property is null)
+                    continue;
+
                 outputProp.ConnectTo(property);
                 changes.Add(new ConnectProperty_ChangeInfo(node.Id, property.Node.Id, outputProp.InternalPropertyName,
                     property.InternalPropertyName));

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

@@ -21,7 +21,7 @@ internal sealed class ApplyMask_Change : Change
     public override bool InitializeAndValidate(Document target)
     {
         var member = target.FindMember(structureMemberGuid);
-        bool isValid = member is not (null or FolderNode) && member.EmbeddedMask is not null;
+        bool isValid = member is ImageLayerNode && member.EmbeddedMask is not null;
 
         return isValid;
     }

+ 27 - 3
src/PixiEditor.Desktop/Program.cs

@@ -1,8 +1,10 @@
 using System;
+using System.Linq;
 using Avalonia;
 using Avalonia.Logging;
 using Drawie.Interop.Avalonia;
 using Drawie.Interop.VulkanAvalonia;
+using PixiEditor.Helpers;
 
 namespace PixiEditor.Desktop;
 
@@ -17,16 +19,37 @@ public class Program
 
     // Avalonia configuration, don't remove; also used by visual designer.
     public static AppBuilder BuildAvaloniaApp()
-        => AppBuilder.Configure<App>()
+    {
+        bool openGlPreferred = false;
+        try
+        {
+            openGlPreferred = string.Equals(RenderApiPreferenceManager.TryReadRenderApiPreference(), "opengl",
+                StringComparison.OrdinalIgnoreCase);
+
+            if (!openGlPreferred)
+            {
+                var cmdArgs = Environment.GetCommandLineArgs();
+                if (cmdArgs is { Length: > 0 })
+                {
+                    openGlPreferred = cmdArgs.Any(arg =>
+                        string.Equals(arg, "--opengl", StringComparison.OrdinalIgnoreCase));
+                }
+            }
+        }
+        catch (Exception ex)
+        {
+        }
+
+        return AppBuilder.Configure<App>()
             .UsePlatformDetect()
             .With(new Win32PlatformOptions()
             {
-                RenderingMode = new Win32RenderingMode[] { Win32RenderingMode.Vulkan, Win32RenderingMode.Wgl },
+                RenderingMode = openGlPreferred ? [ Win32RenderingMode.Wgl, Win32RenderingMode.Vulkan] : [ Win32RenderingMode.Vulkan, Win32RenderingMode.Wgl],
                 OverlayPopups = true,
             })
             .With(new X11PlatformOptions()
             {
-                RenderingMode = new X11RenderingMode[] { X11RenderingMode.Vulkan, X11RenderingMode.Glx },
+                RenderingMode = openGlPreferred ? [ X11RenderingMode.Glx, X11RenderingMode.Vulkan] : [ X11RenderingMode.Vulkan, X11RenderingMode.Glx],
                 OverlayPopups = true,
             })
             .With(new SkiaOptions()
@@ -38,4 +61,5 @@ public class Program
             .LogToTrace(LogEventLevel.Verbose, "Vulkan")
 #endif
             .LogToTrace();
+    }
 }

+ 5 - 0
src/PixiEditor.Linux/LinuxOperatingSystem.cs

@@ -43,6 +43,11 @@ public sealed class LinuxOperatingSystem : IOperatingSystem
         return true;
     }
 
+    public string[] GetAvailableRenderers()
+    {
+        return ["Vulkan", "OpenGL"];
+    }
+
     public void HandleActivatedWithFile(FileActivatedEventArgs fileActivatedEventArgs)
     {
         // TODO: Check if this is executed on Linux at all

+ 5 - 0
src/PixiEditor.MacOs/MacOperatingSystem.cs

@@ -39,4 +39,9 @@ public sealed class MacOperatingSystem : IOperatingSystem
     {
         return true;
     }
+
+    public string[] GetAvailableRenderers()
+    {
+        return ["OpenGL"];
+    }
 }

+ 2 - 0
src/PixiEditor.OperatingSystem/IOperatingSystem.cs

@@ -36,5 +36,7 @@ public interface IOperatingSystem
 
     public bool HandleNewInstance(Dispatcher? dispatcher, Action<string, bool> openInExistingAction,
         IApplicationLifetime lifetime);
+
+    public string[] GetAvailableRenderers();
 }
 

+ 5 - 0
src/PixiEditor.Windows/WindowsOperatingSystem.cs

@@ -92,6 +92,11 @@ public sealed class WindowsOperatingSystem : IOperatingSystem
         return false;
     }
 
+    public string[] GetAvailableRenderers()
+    {
+        return ["Vulkan", "OpenGL"];
+    }
+
     public void HandleActivatedWithFile(FileActivatedEventArgs fileActivatedEventArgs) { }
 
     public void HandleActivatedWithUri(ProtocolActivatedEventArgs openUriEventArgs) { }

+ 5 - 1
src/PixiEditor/Data/Localization/Languages/en.json

@@ -1124,5 +1124,9 @@
   "NORMALIZE_COORDINATES": "Normalize Coordinates",
   "TRANSFORMED_POSITION": "Transformed Position",
   "ACCOUNT_PROVIDER_NOT_AVAILABLE": "This build of PixiEditor does not support accounts. Use the official build from pixieditor.net to manage your account.",
-  "STEAM_OFFLINE": "Cannot validate the account. Steam is offline. Make sure Steam client is running and you are logged in."
+  "STEAM_OFFLINE": "Cannot validate the account. Steam is offline. Make sure Steam client is running and you are logged in.",
+  "ERROR_GPU_RESOURCES_CREATION": "Failed to create resources: Try updating your GPU drivers or try setting different rendering api in settings. \nError: '{0}'",
+  "ERROR_SAVING_PREFERENCES_DESC": "Failed to save preferences with error: '{0}'. Please check if you have write permissions to the PixiEditor data folder.",
+  "ERROR_SAVING_PREFERENCES": "Failed to save preferences",
+  "PREFERRED_RENDERER": "Preferred Render Api"
 }

+ 10 - 4
src/PixiEditor/Helpers/Nodes/NodeAbbreviation.cs

@@ -7,7 +7,7 @@ public static class NodeAbbreviation
 {
     private static readonly SearchValues<char> SearchFor = SearchValues.Create(['.']);
 
-    
+
     public static bool IsAbbreviation(string value, out string? lastValue)
     {
         var span = value.AsSpan();
@@ -23,9 +23,14 @@ public static class NodeAbbreviation
         lastValue = span[(i + 1)..].ToString();
         return true;
     }
-    
+
     public static List<NodeTypeInfo>? FromString(string value, ICollection<NodeTypeInfo> allNodes)
     {
+        if (string.IsNullOrEmpty(value))
+        {
+            return null;
+        }
+
         var span = value.AsSpan();
 
         string lookFor = value;
@@ -33,7 +38,7 @@ public static class NodeAbbreviation
         {
             return [allNodes.FirstOrDefault(SearchComparer)];
         }
-        
+
         var list = new List<NodeTypeInfo>();
 
         var enumerator = new PartEnumerator(span, SearchFor);
@@ -47,7 +52,8 @@ public static class NodeAbbreviation
         }
 
         bool SearchComparer(NodeTypeInfo x) =>
-            x.FinalPickerName.Value.Replace(" ", "").Contains(lookFor.Replace(" ", ""), StringComparison.OrdinalIgnoreCase);
+            x.FinalPickerName.Value.Replace(" ", "")
+                .Contains(lookFor.Replace(" ", ""), StringComparison.OrdinalIgnoreCase);
 
         return list;
     }

+ 41 - 0
src/PixiEditor/Helpers/RenderApiPreferenceManager.cs

@@ -0,0 +1,41 @@
+namespace PixiEditor.Helpers;
+
+public static class RenderApiPreferenceManager
+{
+    public static string? FirstReadApiPreference { get; } = TryReadRenderApiPreference() ?? null;
+    public static string? TryReadRenderApiPreference()
+    {
+        try
+        {
+            using var stream =
+                new FileStream(
+                    Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
+                        "PixiEditor",
+                        "render_api.config"), FileMode.Open, FileAccess.Read, FileShare.Read);
+            using var reader = new StreamReader(stream);
+            string? renderApi = reader.ReadLine();
+            if (string.IsNullOrEmpty(renderApi))
+            {
+                return null;
+            }
+
+            return renderApi;
+        }
+        catch (Exception)
+        {
+            return null;
+        }
+    }
+
+    public static void UpdateRenderApiPreference(string renderApi)
+    {
+        using var stream =
+            new FileStream(
+                Path.Combine(
+                    Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
+                    "PixiEditor",
+                    "render_api.config"), FileMode.Create, FileAccess.Write, FileShare.None);
+        using var writer = new StreamWriter(stream);
+        writer.WriteLine(renderApi);
+    }
+}

+ 14 - 2
src/PixiEditor/Models/Preferences/PreferencesSettings.cs

@@ -2,6 +2,9 @@
 using Newtonsoft.Json;
 using Newtonsoft.Json.Linq;
 using PixiEditor.Extensions.CommonApi.UserPreferences;
+using PixiEditor.Helpers;
+using PixiEditor.Models.Dialogs;
+using PixiEditor.UI.Common.Localization;
 
 namespace PixiEditor.Models.Preferences;
 
@@ -89,8 +92,17 @@ internal class PreferencesSettings : IPreferences
             Init();
         }
 
-        File.WriteAllText(PathToRoamingUserPreferences, JsonConvert.SerializeObject(Preferences));
-        File.WriteAllText(PathToLocalPreferences, JsonConvert.SerializeObject(LocalPreferences));
+        try
+        {
+            File.WriteAllText(PathToRoamingUserPreferences, JsonConvert.SerializeObject(Preferences));
+            File.WriteAllText(PathToLocalPreferences, JsonConvert.SerializeObject(LocalPreferences));
+        }
+        catch (Exception ex)
+        {
+            NoticeDialog.Show(
+                new LocalizedString("ERROR_SAVING_PREFERENCES_DESC", ex.Message),
+                "ERROR_SAVING_PREFERENCES");
+        }
     }
 
     public Dictionary<string, List<Action<string, object>>> Callbacks { get; set; } = new Dictionary<string, List<Action<string, object>>>();

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

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

+ 3 - 2
src/PixiEditor/ViewModels/Document/AnimationDataViewModel.cs

@@ -466,12 +466,13 @@ internal class AnimationDataViewModel : ObservableObject, IAnimationHandler
 
     public int GetFirstVisibleFrame()
     {
-        return keyFrames.Count > 0 ? keyFrames.Where(x => x.IsVisible).Min(x => x.StartFrameBindable) : 1;
+        return keyFrames.Count > 0 && keyFrames.Any(x => x.IsVisible)
+            ? keyFrames.Where(x => x.IsVisible).Min(x => x.StartFrameBindable) : 1;
     }
 
     public int GetLastVisibleFrame()
     {
-        return keyFrames.Count > 0
+        return keyFrames.Count > 0 && keyFrames.Any(x => x.IsVisible)
             ? keyFrames.Where(x => x.IsVisible).Max(x => x.StartFrameBindable + x.DurationBindable)
             : DefaultEndFrameBindable;
     }

+ 16 - 1
src/PixiEditor/ViewModels/SubViewModels/LayersViewModel.cs

@@ -375,6 +375,15 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
     public bool ActiveMemberHasMask() =>
         Owner.DocumentManagerSubViewModel.ActiveDocument?.SelectedStructureMember?.HasMaskBindable ?? false;
 
+    [Evaluator.CanExecute("PixiEditor.Layer.ActiveLayerHasApplyableMask",
+        nameof(ViewModelMain.DocumentManagerSubViewModel.ActiveDocument),
+        nameof(ViewModelMain.DocumentManagerSubViewModel.ActiveDocument.SelectedStructureMember),
+        nameof(ViewModelMain.DocumentManagerSubViewModel.ActiveDocument.SelectedStructureMember.HasMaskBindable))]
+
+    public bool ActiveMemberHasApplyableMask() =>
+        (Owner.DocumentManagerSubViewModel.ActiveDocument?.SelectedStructureMember?.HasMaskBindable ?? false)
+        && Owner.DocumentManagerSubViewModel.ActiveDocument?.SelectedStructureMember is IRasterLayerHandler;
+
     [Evaluator.CanExecute("PixiEditor.Layer.ActiveLayerHasNoMask",
         nameof(ViewModelMain.DocumentManagerSubViewModel.ActiveDocument),
         nameof(ViewModelMain.DocumentManagerSubViewModel.ActiveDocument.SelectedStructureMember),
@@ -419,7 +428,7 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
     }
 
     [Command.Basic("PixiEditor.Layer.ApplyMask", "APPLY_MASK", "APPLY_MASK",
-        CanExecute = "PixiEditor.Layer.ActiveLayerHasMask", AnalyticsTrack = true)]
+        CanExecute = "PixiEditor.Layer.ActiveLayerHasApplyableMask", AnalyticsTrack = true)]
     public void ApplyMask()
     {
         var doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
@@ -548,6 +557,12 @@ internal class LayersViewModel : SubViewModel<ViewModelMain>
             NoticeDialog.Show(title: "ERROR", message: e.Message);
             return;
         }
+        catch (Exception e)
+        {
+            CrashHelper.SendExceptionInfo(e);
+            NoticeDialog.Show(title: "ERROR", message: e.Message);
+            return;
+        }
 
         byte[] bytes = bitmap.ToByteArray();
 

+ 57 - 3
src/PixiEditor/ViewModels/UserPreferences/Settings/GeneralSettings.cs

@@ -1,26 +1,40 @@
 using System.Collections.Generic;
 using System.Globalization;
 using System.Linq;
+using Drawie.Backend.Core.Bridge;
 using PixiEditor.Extensions.CommonApi.UserPreferences;
+using PixiEditor.Helpers;
+using PixiEditor.Models.Dialogs;
+using PixiEditor.Models.IO;
+using PixiEditor.OperatingSystem;
 using PixiEditor.UI.Common.Localization;
 
 namespace PixiEditor.ViewModels.UserPreferences.Settings;
 
 internal class GeneralSettings : SettingsGroup
 {
+    private string? selectedRenderApi = RenderApiPreferenceManager.TryReadRenderApiPreference();
+
+    private List<string>? availableRenderApis = IOperatingSystem.Current?.GetAvailableRenderers()?.ToList() ??
+                                                new List<string>();
+
     private LanguageData? selectedLanguage = ILocalizationProvider.Current?.SelectedLanguage;
+
     private List<LanguageData>? availableLanguages = ILocalizationProvider.Current?.LocalizationData.Languages
         .OrderByDescending(x => x == ILocalizationProvider.Current.FollowSystem)
-        .ThenByDescending(x => CultureInfo.CurrentUICulture.TwoLetterISOLanguageName == x.Code || CultureInfo.InstalledUICulture.TwoLetterISOLanguageName == x.Code)
+        .ThenByDescending(x =>
+            CultureInfo.CurrentUICulture.TwoLetterISOLanguageName == x.Code ||
+            CultureInfo.InstalledUICulture.TwoLetterISOLanguageName == x.Code)
         .ThenBy(x => x.Name).ToList();
 
     private bool isDebugModeEnabled = GetPreference(nameof(IsDebugModeEnabled), false);
+
     public bool IsDebugModeEnabled
     {
         get => isDebugModeEnabled;
         set => RaiseAndUpdatePreference(ref isDebugModeEnabled, value);
     }
-    
+
     public List<LanguageData>? AvailableLanguages
     {
         get => availableLanguages;
@@ -40,10 +54,50 @@ internal class GeneralSettings : SettingsGroup
         }
     }
 
-    private bool isAnalyticsEnabled = GetPreference(PreferencesConstants.AnalyticsEnabled, PreferencesConstants.AnalyticsEnabledDefault);
+    private bool isAnalyticsEnabled =
+        GetPreference(PreferencesConstants.AnalyticsEnabled, PreferencesConstants.AnalyticsEnabledDefault);
+
     public bool AnalyticsEnabled
     {
         get => isAnalyticsEnabled;
         set => RaiseAndUpdatePreference(ref isAnalyticsEnabled, value);
     }
+
+    public List<string> AvailableRenderApis
+    {
+        get
+        {
+            if (availableRenderApis == null || availableRenderApis.Count == 0)
+            {
+                availableRenderApis = new List<string>(IOperatingSystem.Current?.GetAvailableRenderers() ?? []);
+            }
+
+            return availableRenderApis;
+        }
+    }
+
+    public string SelectedRenderApi
+    {
+        get => selectedRenderApi ?? AvailableRenderApis.FirstOrDefault() ?? string.Empty;
+        set
+        {
+            if (SetProperty(ref selectedRenderApi, value))
+            {
+                try
+                {
+                    RenderApiPreferenceManager.UpdateRenderApiPreference(value);
+                    OnPropertyChanged(nameof(RenderApiChangePending));
+                }
+                catch (Exception ex)
+                {
+                    NoticeDialog.Show(
+                        new LocalizedString("ERROR_SAVING_PREFERENCES_DESC", ex.Message),
+                        "ERROR_SAVING_PREFERENCES");
+                }
+            }
+        }
+    }
+
+    public bool RenderApiChangePending =>
+        selectedRenderApi != RenderApiPreferenceManager.FirstReadApiPreference;
 }

+ 21 - 9
src/PixiEditor/Views/Nodes/NodeGraphView.cs

@@ -87,8 +87,9 @@ internal class NodeGraphView : Zoombox.Zoombox
         AvaloniaProperty.Register<NodeGraphView, ICommand>(
             "CreateNodeFromContextCommand");
 
-    public static readonly StyledProperty<SocketsInfo> SocketsInfoProperty = AvaloniaProperty.Register<NodeGraphView, SocketsInfo>(
-        nameof(SocketsInfo));
+    public static readonly StyledProperty<SocketsInfo> SocketsInfoProperty =
+        AvaloniaProperty.Register<NodeGraphView, SocketsInfo>(
+            nameof(SocketsInfo));
 
     public SocketsInfo SocketsInfo
     {
@@ -402,6 +403,11 @@ internal class NodeGraphView : Zoombox.Zoombox
 
     private void CreateNodeType(NodeTypeInfo nodeType)
     {
+        if (nodeType == null)
+        {
+            return;
+        }
+
         var type = nodeType.NodeType;
         if (CreateNodeCommand != null && CreateNodeCommand.CanExecute(type))
         {
@@ -589,7 +595,7 @@ internal class NodeGraphView : Zoombox.Zoombox
 
         if (e.Source is NodeView nodeView)
         {
-            UpdateConnections(/*nodeView*/);
+            UpdateConnections( /*nodeView*/);
         }
     }
 
@@ -639,7 +645,7 @@ internal class NodeGraphView : Zoombox.Zoombox
         if (e.Property == BoundsProperty)
         {
             NodeView nodeView = (NodeView)sender!;
-            UpdateConnections(/*nodeView*/);
+            UpdateConnections( /*nodeView*/);
         }
     }
 
@@ -673,7 +679,7 @@ internal class NodeGraphView : Zoombox.Zoombox
         startDragConnectionPoint = _previewConnectionLine.StartPoint;
     }
 
-    private void UpdateConnections(/*NodeView nodeView*/)
+    private void UpdateConnections( /*NodeView nodeView*/)
     {
         connectionRenderer?.InvalidateVisual();
         /*if (nodeView == null)
@@ -822,8 +828,10 @@ internal class NodeGraphView : Zoombox.Zoombox
         {
             foreach (NodeConnectionViewModel connection in e.NewItems)
             {
-                SocketsInfo.Sockets[$"i:{connection.InputNode.Id}.{connection.InputProperty.PropertyName}"] = connection.InputProperty;
-                SocketsInfo.Sockets[$"o:{connection.OutputNode.Id}.{connection.OutputProperty.PropertyName}"] = connection.OutputProperty;
+                SocketsInfo.Sockets[$"i:{connection.InputNode.Id}.{connection.InputProperty.PropertyName}"] =
+                    connection.InputProperty;
+                SocketsInfo.Sockets[$"o:{connection.OutputNode.Id}.{connection.OutputProperty.PropertyName}"] =
+                    connection.OutputProperty;
             }
         }
         else if (e.Action == NotifyCollectionChangedAction.Reset)
@@ -841,14 +849,18 @@ internal class NodeGraphView : Zoombox.Zoombox
         {
             e.OldValue.Value.Connections.CollectionChanged += nodeGraph.OnConnectionsChanged;
         }
+
         if (e.NewValue.Value != null)
         {
             e.NewValue.Value.Connections.CollectionChanged += nodeGraph.OnConnectionsChanged;
             nodeGraph.SocketsInfo.Sockets.Clear();
             foreach (var connection in e.NewValue.Value.Connections)
             {
-                nodeGraph.SocketsInfo.Sockets[$"i:{connection.InputNode.Id}.{connection.InputProperty.PropertyName}"] = connection.InputProperty;
-                nodeGraph.SocketsInfo.Sockets[$"o:{connection.OutputNode.Id}.{connection.OutputProperty.PropertyName}"] = connection.OutputProperty;
+                nodeGraph.SocketsInfo.Sockets[$"i:{connection.InputNode.Id}.{connection.InputProperty.PropertyName}"] =
+                    connection.InputProperty;
+                nodeGraph.SocketsInfo.Sockets
+                        [$"o:{connection.OutputNode.Id}.{connection.OutputProperty.PropertyName}"] =
+                    connection.OutputProperty;
             }
         }
     }

+ 1 - 1
src/PixiEditor/Views/Overlays/LineToolOverlay/LineToolOverlay.cs

@@ -174,7 +174,7 @@ internal class LineToolOverlay : Overlay
         isDraggingHandle = false;
         IsSizeBoxEnabled = false;
         
-        AddToUndoCommand.Execute((LineStart, LineEnd));
+        AddToUndoCommand?.Execute((LineStart, LineEnd));
     }
 
     protected override void ZoomChanged(double newZoom)

+ 194 - 101
src/PixiEditor/Views/Rendering/Scene.cs

@@ -1,5 +1,6 @@
 using System.Collections.ObjectModel;
 using System.Collections.Specialized;
+using System.Globalization;
 using Avalonia;
 using Avalonia.Animation;
 using Avalonia.Input;
@@ -357,18 +358,25 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
         {
             foreach (Overlay overlay in AllOverlays)
             {
-                if (!overlay.IsVisible || overlay.OverlayRenderSorting != sorting)
+                try
                 {
-                    continue;
-                }
+                    if (!overlay.IsVisible || overlay.OverlayRenderSorting != sorting)
+                    {
+                        continue;
+                    }
 
-                overlay.PointerPosition = lastMousePosition;
+                    overlay.PointerPosition = lastMousePosition;
 
-                overlay.ZoomScale = Scale;
+                    overlay.ZoomScale = Scale;
 
-                if (!overlay.CanRender()) continue;
+                    if (!overlay.CanRender()) continue;
 
-                overlay.RenderOverlay(renderSurface.Canvas, dirtyBounds);
+                    overlay.RenderOverlay(renderSurface.Canvas, dirtyBounds);
+                }
+                catch (Exception ex)
+                {
+                    CrashHelper.SendExceptionInfo(ex);
+                }
             }
         }
     }
@@ -416,161 +424,203 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
     protected override void OnPointerMoved(PointerEventArgs e)
     {
         base.OnPointerMoved(e);
-        if (AllOverlays != null)
+        try
         {
-            OverlayPointerArgs args = ConstructPointerArgs(e);
-            lastMousePosition = args.Point;
+            if (AllOverlays != null)
+            {
+                OverlayPointerArgs args = ConstructPointerArgs(e);
+                lastMousePosition = args.Point;
 
-            Cursor finalCursor = DefaultCursor;
+                Cursor finalCursor = DefaultCursor;
 
-            if (capturedOverlay != null)
-            {
-                capturedOverlay.MovePointer(args);
-                if (capturedOverlay.IsHitTestVisible)
+                if (capturedOverlay != null)
                 {
-                    finalCursor = capturedOverlay.Cursor ?? DefaultCursor;
+                    capturedOverlay.MovePointer(args);
+                    if (capturedOverlay.IsHitTestVisible)
+                    {
+                        finalCursor = capturedOverlay.Cursor ?? DefaultCursor;
+                    }
                 }
-            }
-            else
-            {
-                foreach (Overlay overlay in AllOverlays)
+                else
                 {
-                    if (!overlay.IsVisible) continue;
-
-                    if (overlay.TestHit(args.Point))
+                    foreach (Overlay overlay in AllOverlays)
                     {
-                        if (!mouseOverOverlays.Contains(overlay))
+                        if (!overlay.IsVisible) continue;
+
+                        if (overlay.TestHit(args.Point))
                         {
-                            overlay.EnterPointer(args);
-                            mouseOverOverlays.Add(overlay);
+                            if (!mouseOverOverlays.Contains(overlay))
+                            {
+                                overlay.EnterPointer(args);
+                                mouseOverOverlays.Add(overlay);
+                            }
                         }
-                    }
-                    else
-                    {
-                        if (mouseOverOverlays.Contains(overlay))
+                        else
                         {
-                            overlay.ExitPointer(args);
-                            mouseOverOverlays.Remove(overlay);
-
-                            e.Handled = args.Handled;
-                            return;
+                            if (mouseOverOverlays.Contains(overlay))
+                            {
+                                overlay.ExitPointer(args);
+                                mouseOverOverlays.Remove(overlay);
+
+                                e.Handled = args.Handled;
+                                return;
+                            }
                         }
-                    }
 
-                    overlay.MovePointer(args);
-                    if (overlay.IsHitTestVisible)
-                    {
-                        finalCursor = overlay.Cursor ?? DefaultCursor;
+                        overlay.MovePointer(args);
+                        if (overlay.IsHitTestVisible)
+                        {
+                            finalCursor = overlay.Cursor ?? DefaultCursor;
+                        }
                     }
                 }
-            }
 
-            if (Cursor.ToString() != finalCursor.ToString())
-                Cursor = finalCursor;
-            e.Handled = args.Handled;
+                if (Cursor.ToString() != finalCursor.ToString())
+                    Cursor = finalCursor;
+                e.Handled = args.Handled;
+            }
+        }
+        catch (Exception ex)
+        {
+            CrashHelper.SendExceptionInfo(ex);
         }
     }
 
     protected override void OnPointerPressed(PointerPressedEventArgs e)
     {
         base.OnPointerPressed(e);
-        if (AllOverlays != null)
+        try
         {
-            OverlayPointerArgs args = ConstructPointerArgs(e);
-            if (capturedOverlay != null)
+            if (AllOverlays != null)
             {
-                capturedOverlay?.PressPointer(args);
-            }
-            else
-            {
-                foreach (var overlay in AllOverlays)
+                OverlayPointerArgs args = ConstructPointerArgs(e);
+                if (capturedOverlay != null)
                 {
-                    if (args.Handled) break;
-                    if (!overlay.IsVisible) continue;
+                    capturedOverlay?.PressPointer(args);
+                }
+                else
+                {
+                    foreach (var overlay in AllOverlays)
+                    {
+                        if (args.Handled) break;
+                        if (!overlay.IsVisible) continue;
 
-                    if (!overlay.IsHitTestVisible || !overlay.TestHit(args.Point)) continue;
+                        if (!overlay.IsHitTestVisible || !overlay.TestHit(args.Point)) continue;
 
-                    overlay.PressPointer(args);
+                        overlay.PressPointer(args);
+                    }
                 }
-            }
 
-            e.Handled = args.Handled;
+                e.Handled = args.Handled;
+            }
+        }
+        catch (Exception ex)
+        {
+            CrashHelper.SendExceptionInfo(ex);
         }
     }
 
     protected override void OnPointerExited(PointerEventArgs e)
     {
         base.OnPointerExited(e);
-        if (AllOverlays != null)
+        try
         {
-            OverlayPointerArgs args = ConstructPointerArgs(e);
-            for (var i = 0; i < mouseOverOverlays.Count; i++)
+            if (AllOverlays != null)
             {
-                var overlay = mouseOverOverlays[i];
-                if (args.Handled) break;
-                if (!overlay.IsVisible) continue;
+                OverlayPointerArgs args = ConstructPointerArgs(e);
+                for (var i = 0; i < mouseOverOverlays.Count; i++)
+                {
+                    var overlay = mouseOverOverlays[i];
+                    if (args.Handled) break;
+                    if (!overlay.IsVisible) continue;
 
-                overlay.ExitPointer(args);
-                mouseOverOverlays.Remove(overlay);
-                i--;
-            }
+                    overlay.ExitPointer(args);
+                    mouseOverOverlays.Remove(overlay);
+                    i--;
+                }
 
-            e.Handled = args.Handled;
+                e.Handled = args.Handled;
+            }
+        }
+        catch (Exception ex)
+        {
+            CrashHelper.SendExceptionInfo(ex);
         }
     }
 
     protected override void OnPointerReleased(PointerReleasedEventArgs e)
     {
         base.OnPointerExited(e);
-        if (AllOverlays != null)
+        try
         {
-            OverlayPointerArgs args = ConstructPointerArgs(e);
-
-            if (capturedOverlay != null)
-            {
-                capturedOverlay.ReleasePointer(args);
-                capturedOverlay = null;
-            }
-            else
+            if (AllOverlays != null)
             {
-                foreach (Overlay overlay in AllOverlays)
+                OverlayPointerArgs args = ConstructPointerArgs(e);
+
+                if (capturedOverlay != null)
                 {
-                    if (args.Handled) break;
-                    if (!overlay.IsVisible) continue;
+                    capturedOverlay.ReleasePointer(args);
+                    capturedOverlay = null;
+                }
+                else
+                {
+                    foreach (Overlay overlay in AllOverlays)
+                    {
+                        if (args.Handled) break;
+                        if (!overlay.IsVisible) continue;
 
-                    if (!overlay.IsHitTestVisible || !overlay.TestHit(args.Point)) continue;
+                        if (!overlay.IsHitTestVisible || !overlay.TestHit(args.Point)) continue;
 
-                    overlay.ReleasePointer(args);
+                        overlay.ReleasePointer(args);
+                    }
                 }
             }
         }
+        catch (Exception ex)
+        {
+            CrashHelper.SendExceptionInfo(ex);
+        }
     }
 
     protected override void OnKeyDown(KeyEventArgs e)
     {
         base.OnKeyDown(e);
-        if (AllOverlays != null)
+        try
         {
-            foreach (Overlay overlay in AllOverlays)
+            if (AllOverlays != null)
             {
-                if (!overlay.IsVisible) continue;
+                foreach (Overlay overlay in AllOverlays)
+                {
+                    if (!overlay.IsVisible) continue;
 
-                overlay.KeyPressed(e);
+                    overlay.KeyPressed(e);
+                }
             }
         }
+        catch (Exception ex)
+        {
+            CrashHelper.SendExceptionInfo(ex);
+        }
     }
 
     protected override void OnKeyUp(KeyEventArgs e)
     {
         base.OnKeyUp(e);
-        if (AllOverlays != null)
+        try
         {
-            foreach (Overlay overlay in AllOverlays)
+            if (AllOverlays != null)
             {
-                if (!overlay.IsVisible) continue;
-                overlay.KeyReleased(e);
+                foreach (Overlay overlay in AllOverlays)
+                {
+                    if (!overlay.IsVisible) continue;
+                    overlay.KeyReleased(e);
+                }
             }
         }
+        catch (Exception ex)
+        {
+            CrashHelper.SendExceptionInfo(ex);
+        }
     }
 
     private OverlayPointerArgs ConstructPointerArgs(PointerEventArgs e)
@@ -596,27 +646,41 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
     protected override void OnGotFocus(GotFocusEventArgs e)
     {
         base.OnGotFocus(e);
-        if (AllOverlays != null)
+        try
         {
-            foreach (Overlay overlay in AllOverlays)
+            if (AllOverlays != null)
             {
-                if (!overlay.IsVisible) continue;
-                overlay.FocusChanged(true);
+                foreach (Overlay overlay in AllOverlays)
+                {
+                    if (!overlay.IsVisible) continue;
+                    overlay.FocusChanged(true);
+                }
             }
         }
+        catch (Exception ex)
+        {
+            CrashHelper.SendExceptionInfo(ex);
+        }
     }
 
     protected override void OnLostFocus(RoutedEventArgs e)
     {
         base.OnLostFocus(e);
-        if (AllOverlays != null)
+        try
         {
-            foreach (Overlay overlay in AllOverlays)
+            if (AllOverlays != null)
             {
-                if (!overlay.IsVisible) continue;
-                overlay.FocusChanged(false);
+                foreach (Overlay overlay in AllOverlays)
+                {
+                    if (!overlay.IsVisible) continue;
+                    overlay.FocusChanged(false);
+                }
             }
         }
+        catch (Exception ex)
+        {
+            CrashHelper.SendExceptionInfo(ex);
+        }
     }
 
     private VecD ToCanvasSpace(Point scenePosition)
@@ -704,7 +768,17 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
         }
 
         var size = new PixelSize((int)Bounds.Width, (int)Bounds.Height);
-        RenderFrame(size);
+        try
+        {
+            RenderFrame(size);
+            info = string.Empty;
+        }
+        catch (Exception e)
+        {
+            info = new LocalizedString("ERROR_GPU_RESOURCES_CREATION", e.Message);
+            CrashHelper.SendExceptionInfo(e);
+            return;
+        }
     }
 
     public void QueueNextFrame()
@@ -741,11 +815,30 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
     protected (bool success, string info) InitializeGraphicsResources(Compositor targetCompositor,
         CompositionDrawingSurface compositionDrawingSurface, ICompositionGpuInterop interop)
     {
-        resources = IDrawieInteropContext.Current.CreateResources(compositionDrawingSurface, interop);
+        try
+        {
+            resources = IDrawieInteropContext.Current.CreateResources(compositionDrawingSurface, interop);
+        }
+        catch (Exception e)
+        {
+            return (false, new LocalizedString("ERROR_GPU_RESOURCES_CREATION", e.Message));
+        }
 
         return (true, string.Empty);
     }
 
+    public override void Render(DrawingContext context)
+    {
+        if (!string.IsNullOrEmpty(info))
+        {
+            Point center = new Point(Bounds.Width / 2, Bounds.Height / 2);
+            context.DrawText(
+                new FormattedText(info, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, Typeface.Default, 12,
+                    Brushes.White),
+                center);
+        }
+    }
+
     protected async Task FreeGraphicsResources()
     {
         renderTexture?.Dispose();

+ 34 - 10
src/PixiEditor/Views/Windows/Settings/SettingsWindow.axaml

@@ -192,6 +192,30 @@
                                       IsChecked="{Binding SettingsSubViewModel.Tools.EnableSharedToolbar}"
                                       ui:Translator.Key="ENABLE_SHARED_TOOLBAR" />
 
+                            <TextBlock ui:Translator.Key="Rendering" Classes="h5" />
+
+                            <Label Target="primaryToolsetComboBox" ui:Translator.Key="PREFERRED_RENDERER"
+                                   VerticalAlignment="Center" />
+
+                            <StackPanel Orientation="Horizontal" Spacing="5">
+                                <ComboBox Classes="leftOffset" Width="200" HorizontalAlignment="Left"
+                                          ItemsSource="{Binding SettingsSubViewModel.General.AvailableRenderApis}"
+                                          SelectedItem="{Binding SettingsSubViewModel.General.SelectedRenderApi, Mode=TwoWay}">
+                                    <ComboBox.ItemTemplate>
+                                        <DataTemplate>
+                                            <TextBlock ui:Translator.Key="{Binding}" />
+                                        </DataTemplate>
+                                    </ComboBox.ItemTemplate>
+                                </ComboBox>
+                                <Button Command="{cmds:Command Name=PixiEditor.Restart}"
+                                        Background="{DynamicResource ThemeAccentBrush}"
+                                        IsVisible="{Binding SettingsSubViewModel.General.RenderApiChangePending}"
+                                        d:Content="Restart to apply changes"
+                                        ui:Translator.Key="RESTART"
+                                        HorizontalAlignment="Left"
+                                        VerticalAlignment="Center" />
+                            </StackPanel>
+
                             <TextBlock ui:Translator.Key="DEBUG" Classes="h5" />
                             <CheckBox Classes="leftOffset"
                                       IsChecked="{Binding SettingsSubViewModel.General.IsDebugModeEnabled}"
@@ -322,12 +346,12 @@
                                               ItemsSource="{Binding SettingsSubViewModel.Update.UpdateChannels}"
                                               SelectedValue="{Binding SettingsSubViewModel.Update.UpdateChannelName}" />
                                     <TextBlock Cursor="Help"
-                                           Classes="pixi-icon"
-                                           Text="{DynamicResource icon-help}"
-                                           VerticalAlignment="Center"
-                                           ToolTip.ShowDelay="0"
-                                           IsVisible="{Binding !ShowUpdateTab}"
-                                           ui:Translator.TooltipKey="UPDATE_CHANNEL_HELP_TOOLTIP" />
+                                               Classes="pixi-icon"
+                                               Text="{DynamicResource icon-help}"
+                                               VerticalAlignment="Center"
+                                               ToolTip.ShowDelay="0"
+                                               IsVisible="{Binding !ShowUpdateTab}"
+                                               ui:Translator.TooltipKey="UPDATE_CHANNEL_HELP_TOOLTIP" />
                                     <!-- ToolTipService.InitialShowDelay="0"-->
                                 </StackPanel>
                             </StackPanel>
@@ -373,12 +397,12 @@
                             <StackPanel Spacing="5" Orientation="Horizontal" Classes="leftOffset">
                                 <Label Content="X" />
                                 <controls:SizeInput MinSize="0.5" Decimals="1"
-                                                 Size="{Binding SettingsSubViewModel.Scene.CustomBackgroundScaleX, Mode=TwoWay}"
-                                                 HorizontalAlignment="Left" />
+                                                    Size="{Binding SettingsSubViewModel.Scene.CustomBackgroundScaleX, Mode=TwoWay}"
+                                                    HorizontalAlignment="Left" />
                                 <Label Content="Y" />
                                 <controls:SizeInput MinSize="0.5" Decimals="1"
-                                                 Size="{Binding SettingsSubViewModel.Scene.CustomBackgroundScaleY, Mode=TwoWay}"
-                                                 HorizontalAlignment="Left" />
+                                                    Size="{Binding SettingsSubViewModel.Scene.CustomBackgroundScaleY, Mode=TwoWay}"
+                                                    HorizontalAlignment="Left" />
                             </StackPanel>
 
                             <StackPanel Orientation="Horizontal" Spacing="5">