Преглед на файлове

Merge pull request #755 from PixiEditor/svg-import-improvements

Svg import improvements
Krzysztof Krysiński преди 5 месеца
родител
ревизия
3973359a7f
променени са 89 файла, в които са добавени 1467 реда и са изтрити 368 реда
  1. 1 1
      src/Drawie
  2. 5 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/Shapes/IReadOnlyTextData.cs
  3. 10 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/PathVectorData.cs
  4. 2 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/ShapeVectorData.cs
  5. 29 1
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/TextVectorData.cs
  6. 6 2
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/RasterizeShapeNode.cs
  7. 10 20
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/TextNode.cs
  8. 51 0
      src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/TextOnPathNode.cs
  9. 4 1
      src/PixiEditor.SVG/Elements/SvgGroup.cs
  10. 6 1
      src/PixiEditor.SVG/Elements/SvgPrimitive.cs
  11. 51 0
      src/PixiEditor.SVG/Elements/SvgText.cs
  12. 8 0
      src/PixiEditor.SVG/Enums/SvgFontStyle.cs
  13. 9 0
      src/PixiEditor.SVG/Enums/SvgFontWeight.cs
  14. 1 0
      src/PixiEditor.SVG/Features/IFillable.cs
  15. 8 0
      src/PixiEditor.SVG/Features/IOpacity.cs
  16. 154 32
      src/PixiEditor.SVG/StyleContext.cs
  17. 16 13
      src/PixiEditor.SVG/SvgDocument.cs
  18. 24 23
      src/PixiEditor.SVG/SvgElement.cs
  19. 14 12
      src/PixiEditor.SVG/SvgParser.cs
  20. 20 3
      src/PixiEditor.SVG/SvgProperty.cs
  21. 35 15
      src/PixiEditor.SVG/Units/SvgNumericUnit.cs
  22. 142 0
      src/PixiEditor.SVG/Units/SvgNumericUnits.cs
  23. 73 0
      src/PixiEditor.SVG/Units/SvgStyleUnit.cs
  24. 98 4
      src/PixiEditor.SVG/Units/SvgTransformUnit.cs
  25. 2 0
      src/PixiEditor.UI.Common/Accents/Base.axaml
  26. BIN
      src/PixiEditor.UI.Common/Fonts/PixiPerfect.ttf
  27. 1 1
      src/PixiEditor.UI.Common/Fonts/PixiPerfectIcons.axaml
  28. 0 1
      src/PixiEditor/App.axaml
  29. 9 3
      src/PixiEditor/Data/Localization/Languages/en.json
  30. 3 3
      src/PixiEditor/Helpers/DocumentViewModelBuilder.cs
  31. 1 1
      src/PixiEditor/Helpers/Extensions/PixiParserDocumentEx.cs
  32. 1 1
      src/PixiEditor/Initialization/ClassicDesktopEntry.cs
  33. 10 0
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/UpdateableChangeExecutor.cs
  34. 44 8
      src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorTextToolExecutor.cs
  35. 2 0
      src/PixiEditor/Models/Files/SvgFileType.cs
  36. 116 41
      src/PixiEditor/Models/IO/CustomDocumentFormats/SvgDocumentBuilder.cs
  37. 0 22
      src/PixiEditor/Styles/NodeIcons.axaml
  38. 1 1
      src/PixiEditor/Styles/Templates/NodePicker.axaml
  39. 85 40
      src/PixiEditor/ViewModels/Document/DocumentManagerViewModel.cs
  40. 53 1
      src/PixiEditor/ViewModels/Document/DocumentViewModel.Serialization.cs
  41. 6 6
      src/PixiEditor/ViewModels/Document/DocumentViewModel.cs
  42. 1 1
      src/PixiEditor/ViewModels/Document/Nodes/Animable/TimeNodeViewModel.cs
  43. 1 1
      src/PixiEditor/ViewModels/Document/Nodes/ColorNodeViewModel.cs
  44. 1 1
      src/PixiEditor/ViewModels/Document/Nodes/CombineSeparate/CombineChannelsNodeViewModel.cs
  45. 1 1
      src/PixiEditor/ViewModels/Document/Nodes/CombineSeparate/CombineColorNodeViewModel.cs
  46. 1 1
      src/PixiEditor/ViewModels/Document/Nodes/CombineSeparate/CombineVecDNodeViewModel.cs
  47. 1 1
      src/PixiEditor/ViewModels/Document/Nodes/CombineSeparate/CombineVecINodeViewModel.cs
  48. 1 1
      src/PixiEditor/ViewModels/Document/Nodes/CombineSeparate/SeparateChannelsNodeViewModel.cs
  49. 1 1
      src/PixiEditor/ViewModels/Document/Nodes/CombineSeparate/SeparateColorNodeViewModel.cs
  50. 1 1
      src/PixiEditor/ViewModels/Document/Nodes/CombineSeparate/SeparateVecDNodeViewModel.cs
  51. 1 1
      src/PixiEditor/ViewModels/Document/Nodes/CombineSeparate/SeparateVecINodeViewModel.cs
  52. 1 1
      src/PixiEditor/ViewModels/Document/Nodes/CreateImageNodeViewModel.cs
  53. 1 1
      src/PixiEditor/ViewModels/Document/Nodes/CustomOutputNodeViewModel.cs
  54. 1 1
      src/PixiEditor/ViewModels/Document/Nodes/FilterNodes/ApplyFilterNodeViewModel.cs
  55. 1 1
      src/PixiEditor/ViewModels/Document/Nodes/FilterNodes/BlurNodeViewModel.cs
  56. 1 1
      src/PixiEditor/ViewModels/Document/Nodes/FilterNodes/ColorMatrixFilterNodeViewModel.cs
  57. 1 1
      src/PixiEditor/ViewModels/Document/Nodes/FilterNodes/GrayscaleNodeViewModel.cs
  58. 1 1
      src/PixiEditor/ViewModels/Document/Nodes/FilterNodes/KernelFilterNodeViewModel.cs
  59. 1 1
      src/PixiEditor/ViewModels/Document/Nodes/FolderNodeViewModel.cs
  60. 1 1
      src/PixiEditor/ViewModels/Document/Nodes/ImageLayerNodeViewModel.cs
  61. 1 1
      src/PixiEditor/ViewModels/Document/Nodes/LerpColorNodeViewModel.cs
  62. 1 1
      src/PixiEditor/ViewModels/Document/Nodes/MathNodeViewModel.cs
  63. 1 1
      src/PixiEditor/ViewModels/Document/Nodes/MergeNodeViewModel.cs
  64. 1 1
      src/PixiEditor/ViewModels/Document/Nodes/ModifyImageLeftNodeViewModel.cs
  65. 1 1
      src/PixiEditor/ViewModels/Document/Nodes/NoiseNodeViewModel.cs
  66. 1 1
      src/PixiEditor/ViewModels/Document/Nodes/SampleImageNodeViewModel.cs
  67. 1 1
      src/PixiEditor/ViewModels/Document/Nodes/Shapes/DistributePointsNodeViewModel.cs
  68. 1 1
      src/PixiEditor/ViewModels/Document/Nodes/Shapes/EllipseNodeViewModel.cs
  69. 1 1
      src/PixiEditor/ViewModels/Document/Nodes/Shapes/RasterizeShapeNodeViewModel.cs
  70. 1 1
      src/PixiEditor/ViewModels/Document/Nodes/Shapes/RemoveClosePointsNodeViewModel.cs
  71. 1 1
      src/PixiEditor/ViewModels/Document/Nodes/Shapes/TextNodeViewModel.cs
  72. 9 0
      src/PixiEditor/ViewModels/Document/Nodes/Shapes/TextOnPathNodeViewModel.cs
  73. 1 1
      src/PixiEditor/ViewModels/Document/Nodes/VectorLayerNodeViewModel.cs
  74. 10 0
      src/PixiEditor/ViewModels/Nodes/Properties/FontFamilyNamePropertyViewModel.cs
  75. 29 13
      src/PixiEditor/ViewModels/SubViewModels/FileViewModel.cs
  76. 16 46
      src/PixiEditor/ViewModels/Tools/ToolSettings/Settings/FontFamilySettingViewModel.cs
  77. 2 1
      src/PixiEditor/ViewModels/Tools/Tools/TextToolViewModel.cs
  78. 6 0
      src/PixiEditor/ViewModels/Tools/Tools/VectorPathToolViewModel.cs
  79. 25 0
      src/PixiEditor/Views/Input/FontFamilyPicker.axaml
  80. 105 0
      src/PixiEditor/Views/Input/FontFamilyPicker.axaml.cs
  81. 25 0
      src/PixiEditor/Views/Nodes/Properties/FontFamilyNamePropertyView.axaml
  82. 20 0
      src/PixiEditor/Views/Nodes/Properties/FontFamilyNamePropertyView.axaml.cs
  83. 13 0
      src/PixiEditor/Views/Nodes/Properties/NodePropertyView.cs
  84. 1 1
      src/PixiEditor/Views/Nodes/Properties/StringPropertyView.axaml
  85. 20 0
      src/PixiEditor/Views/Overlays/Overlay.cs
  86. 14 0
      src/PixiEditor/Views/Overlays/TextOverlay/TextOverlay.cs
  87. 26 0
      src/PixiEditor/Views/Rendering/Scene.cs
  88. 3 15
      src/PixiEditor/Views/Tools/ToolSettings/Settings/FontFamilySettingView.axaml
  89. 1 1
      src/PixiParser

+ 1 - 1
src/Drawie

@@ -1 +1 @@
-Subproject commit 52c1a1efd9be0c5bb94199056d7abd16fea372a1
+Subproject commit dc3886c56d8df2596f01098622f18a859e2a82a4

+ 5 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Interfaces/Shapes/IReadOnlyTextData.cs

@@ -1,4 +1,5 @@
-using Drawie.Numerics;
+using Drawie.Backend.Core.Text;
+using Drawie.Numerics;
 
 namespace PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces.Shapes;
 
@@ -6,4 +7,7 @@ public interface IReadOnlyTextData
 {
     public string Text { get; }
     public VecD Position { get; }
+    public Font ConstructFont();
+    public double Spacing { get; }
+    public double MaxWidth { get; }
 }

+ 10 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/PathVectorData.cs

@@ -11,7 +11,7 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 public class PathVectorData : ShapeVectorData, IReadOnlyPathData
 {
     public VectorPath Path { get; set; }
-    public override RectD GeometryAABB => Path.TightBounds;
+    public override RectD GeometryAABB => Path?.TightBounds ?? RectD.Empty;
     public override RectD VisualAABB => GeometryAABB.Inflate(StrokeWidth / 2);
 
     public override ShapeCorners TransformationCorners =>
@@ -24,6 +24,10 @@ public class PathVectorData : ShapeVectorData, IReadOnlyPathData
     public PathVectorData(VectorPath path)
     {
         Path = path;
+        if (path == null)
+        {
+            Path = new VectorPath();
+        }
     }
 
     public override void RasterizeGeometry(Canvas canvas)
@@ -38,6 +42,11 @@ public class PathVectorData : ShapeVectorData, IReadOnlyPathData
 
     private void Rasterize(Canvas canvas, bool applyTransform)
     {
+        if(Path == null)
+        {
+            return;
+        }
+
         int num = 0;
         if (applyTransform)
         {

+ 2 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/ShapeVectorData.cs

@@ -11,7 +11,7 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 
 public abstract class ShapeVectorData : ICacheable, ICloneable, IReadOnlyShapeVectorData
 {
-    private float strokeWidth = 1;
+    private float strokeWidth = 0;
 
     public Matrix3X3 TransformationMatrix { get; set; } = Matrix3X3.Identity;
 
@@ -29,6 +29,7 @@ public abstract class ShapeVectorData : ICacheable, ICloneable, IReadOnlyShapeVe
     }
     
     public bool Fill { get; set; } = true;
+
     public abstract RectD GeometryAABB { get; }
     public abstract RectD VisualAABB { get; }
     public RectD TransformedAABB => new ShapeCorners(GeometryAABB).WithMatrix(TransformationMatrix).AABBBounds;

+ 29 - 1
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/Data/TextVectorData.cs

@@ -14,6 +14,7 @@ public class TextVectorData : ShapeVectorData, IReadOnlyTextData
     private Font font = Font.CreateDefault();
     private double? spacing = null;
     private double strokeWidth = 1;
+    private VectorPath? path;
 
     public string Text
     {
@@ -29,6 +30,7 @@ public class TextVectorData : ShapeVectorData, IReadOnlyTextData
 
     public VecD Position { get; set; }
 
+
     public double MaxWidth { get; set; } = double.MaxValue;
 
     public Font Font
@@ -61,6 +63,20 @@ public class TextVectorData : ShapeVectorData, IReadOnlyTextData
         lastBounds = richText.MeasureBounds(Font);
     }
 
+    public Font ConstructFont()
+    {
+        Font newFont = Font.FromFontFamily(Font.Family);
+        newFont.Size = Font.Size;
+        newFont.Edging = Font.Edging;
+        newFont.SubPixel = Font.SubPixel;
+        newFont.Bold = Font.Bold;
+        newFont.Italic = Font.Italic;
+
+        return newFont;
+    }
+
+    double IReadOnlyTextData.Spacing => Spacing ?? Font.Size;
+
     public double? Spacing
     {
         get => spacing;
@@ -97,12 +113,24 @@ public class TextVectorData : ShapeVectorData, IReadOnlyTextData
         new ShapeCorners(GeometryAABB).WithMatrix(TransformationMatrix);
 
     public override RectD VisualAABB => GeometryAABB;
-    public VectorPath? Path { get; set; }
+
+    public VectorPath? Path
+    {
+        get => path;
+        set
+        {
+            path = value;
+            // TODO: properly calculate bounds
+            //lastBounds = richText.MeasureBounds(Font);
+        }
+    }
+
     public FontFamilyName? MissingFontFamily { get; set; }
     public string MissingFontText { get; set; }
 
     private RichText richText;
     private RectD lastBounds;
+    private double _spacing;
 
     public TextVectorData()
     {

+ 6 - 2
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/RasterizeShapeNode.cs

@@ -12,11 +12,12 @@ namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes;
 public class RasterizeShapeNode : RenderNode
 {
     public InputProperty<ShapeVectorData> Data { get; }
-
+    public InputProperty<bool> HighDpiRendering { get; }
 
     public RasterizeShapeNode()
     {
         Data = CreateInput<ShapeVectorData>("Points", "SHAPE", null);
+        HighDpiRendering = CreateInput<bool>("High DPI Rendering", "HIGH_DPI_RENDERING", true);
     }
 
     protected override void OnPaint(RenderContext context, DrawingSurface surface)
@@ -25,11 +26,14 @@ public class RasterizeShapeNode : RenderNode
 
         if (shape == null || !shape.IsValid())
             return;
-        
+
+        AllowHighDpiRendering = HighDpiRendering.Value;
+
         shape.RasterizeTransformed(surface.Canvas);
     }
 
     public override Node CreateCopy() => new RasterizeShapeNode();
+
     public override RectD? GetPreviewBounds(int frame, string elementToRenderName = "")
     {
         return Data?.Value?.TransformedAABB;

+ 10 - 20
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/TextNode.cs

@@ -11,35 +11,32 @@ public class TextNode : ShapeNode<TextVectorData>
 {
     public InputProperty<string> Text { get; }
     public InputProperty<VecD> TextPosition { get; }
-    public InputProperty<string> FontFamily { get; }
+    public InputProperty<FontFamilyName> FontFamily { get; }
     public InputProperty<double> FontSize { get; }
-    public InputProperty<ShapeVectorData> OnPathData { get; }
-    
+
     private string lastText = "";
     private VecD lastPosition = new VecD();
-    private string lastFontFamily = "";
+    private FontFamilyName lastFontFamily = new FontFamilyName();
     private double lastFontSize = 12d;
     private VectorPath? lastPath;
 
     private TextVectorData? cachedData;
     public TextNode()
     {
-        Text = CreateInput("Text", "TEXT", "");
+        Text = CreateInput("Text", "TEXT_LABEL", "");
         TextPosition = CreateInput("Position", "POSITION", new VecD());
-        FontFamily = CreateInput("FontFamily", "FONT_FAMILY", "");
-        FontSize = CreateInput("FontSize", "FONT_SIZE", 12d);
-        OnPathData = CreateInput<ShapeVectorData>("PathToDrawOn", "ON_PATH_DATA", null);
+        FontFamily = CreateInput("FontFamily", "FONT_LABEL", new FontFamilyName());
+        FontSize = CreateInput("FontSize", "FONT_SIZE_LABEL", 12d);
     }
     
     protected override TextVectorData? GetShapeData(RenderContext context)
     {
         string text = Text.Value;
         VecD position = TextPosition.Value;
-        string fontFamily = FontFamily.Value;
+        FontFamilyName fontFamily = FontFamily.Value;
         double fontSize = FontSize.Value;
-        VectorPath? path = OnPathData.Value?.ToPath();
-        
-        if (text == lastText && position == lastPosition && fontFamily == lastFontFamily && fontSize == lastFontSize && path == lastPath)
+
+        if (text == lastText && position == lastPosition && fontFamily.Equals(lastFontFamily) && fontSize == lastFontSize)
         {
             return cachedData;
         }
@@ -48,9 +45,8 @@ public class TextNode : ShapeNode<TextVectorData>
         lastPosition = position;
         lastFontFamily = fontFamily;
         lastFontSize = fontSize;
-        lastPath = path;
 
-        Font font = Font.FromFamilyName(fontFamily);
+        Font font = Font.FromFontFamily(fontFamily);
         if(font == null)
         {
             font = Font.CreateDefault();
@@ -63,7 +59,6 @@ public class TextNode : ShapeNode<TextVectorData>
             Text = text,
             Position = position,
             Font = font,
-            Path = path,
         };
         
         return cachedData;
@@ -73,9 +68,4 @@ public class TextNode : ShapeNode<TextVectorData>
     {
         return new TextNode();
     }
-
-    public override void Dispose()
-    {
-        base.Dispose();
-    }
 }

+ 51 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/Shapes/TextOnPathNode.cs

@@ -0,0 +1,51 @@
+using Drawie.Backend.Core.Vector;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
+using PixiEditor.ChangeableDocument.Rendering;
+
+namespace PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes;
+
+[NodeInfo("TextOnPath")]
+public class TextOnPathNode : Node
+{
+    public InputProperty<TextVectorData> TextData { get; }
+    public InputProperty<ShapeVectorData> PathData { get; }
+
+    public OutputProperty<TextVectorData> Output { get; }
+
+    private VectorPath lastPath;
+
+    public TextOnPathNode()
+    {
+        TextData = CreateInput<TextVectorData>("Text", "TEXT_LABEL", null);
+        PathData = CreateInput<ShapeVectorData>("Path", "SHAPE_LABEL", null);
+
+        Output = CreateOutput<TextVectorData>("Output", "TEXT_LABEL", null);
+    }
+
+    protected override void OnExecute(RenderContext context)
+    {
+        var textData = TextData.Value;
+        var pathData = PathData.Value;
+
+        if (textData == null || pathData == null || !textData.IsValid() || !pathData.IsValid())
+        {
+            Output.Value = null;
+            return;
+        }
+
+        var cloned = (TextVectorData)textData.Clone();
+
+        lastPath?.Dispose();
+        lastPath = pathData.ToPath();
+        lastPath.Transform(pathData.TransformationMatrix);
+
+        cloned.Path = lastPath;
+
+        Output.Value = cloned;
+    }
+
+    public override Node CreateCopy()
+    {
+        return new TextOnPathNode();
+    }
+}

+ 4 - 1
src/PixiEditor.SVG/Elements/SvgGroup.cs

@@ -5,19 +5,22 @@ using PixiEditor.SVG.Units;
 
 namespace PixiEditor.SVG.Elements;
 
-public class SvgGroup() : SvgElement("g"), ITransformable, IFillable, IStrokable, IElementContainer
+public class SvgGroup() : SvgElement("g"), ITransformable, IFillable, IStrokable, IOpacity, IElementContainer
 {
     public List<SvgElement> Children { get; } = new();
     public SvgProperty<SvgTransformUnit> Transform { get; } = new("transform");
     public SvgProperty<SvgColorUnit> Fill { get; } = new("fill");
+    public SvgProperty<SvgNumericUnit> FillOpacity { get; } = new("fill-opacity");
     public SvgProperty<SvgColorUnit> Stroke { get; } = new("stroke");
     public SvgProperty<SvgNumericUnit> StrokeWidth { get; } = new("stroke-width");
     public SvgProperty<SvgEnumUnit<SvgStrokeLineCap>> StrokeLineCap { get; } = new("stroke-linecap");
     public SvgProperty<SvgEnumUnit<SvgStrokeLineJoin>> StrokeLineJoin { get; } = new("stroke-linejoin");
+    public SvgProperty<SvgNumericUnit> Opacity { get; } = new("opacity");
 
     public override void ParseData(XmlReader reader)
     {
         List<SvgProperty> properties = new List<SvgProperty>() { Transform, Fill, Stroke, StrokeWidth, StrokeLineCap, StrokeLineJoin };
         ParseAttributes(properties, reader);
     }
+
 }

+ 6 - 1
src/PixiEditor.SVG/Elements/SvgPrimitive.cs

@@ -5,10 +5,11 @@ using PixiEditor.SVG.Units;
 
 namespace PixiEditor.SVG.Elements;
 
-public abstract class SvgPrimitive(string tagName) : SvgElement(tagName), ITransformable, IFillable, IStrokable
+public abstract class SvgPrimitive(string tagName) : SvgElement(tagName), ITransformable, IFillable, IStrokable, IOpacity
 {
     public SvgProperty<SvgTransformUnit> Transform { get; } = new("transform");
     public SvgProperty<SvgColorUnit> Fill { get; } = new("fill");
+    public SvgProperty<SvgNumericUnit> FillOpacity { get; } = new("fill-opacity");
     public SvgProperty<SvgColorUnit> Stroke { get; } = new("stroke");
     public SvgProperty<SvgNumericUnit> StrokeWidth { get; } = new("stroke-width");
     
@@ -16,16 +17,20 @@ public abstract class SvgPrimitive(string tagName) : SvgElement(tagName), ITrans
     
     public SvgProperty<SvgEnumUnit<SvgStrokeLineJoin>> StrokeLineJoin { get; } = new("stroke-linejoin");
 
+    public SvgProperty<SvgNumericUnit> Opacity { get; } = new("opacity");
+
     public override void ParseData(XmlReader reader)
     {
         List<SvgProperty> properties = GetProperties().ToList();
         
         properties.Add(Transform);
         properties.Add(Fill);
+        properties.Add(FillOpacity);
         properties.Add(Stroke);
         properties.Add(StrokeWidth);
         properties.Add(StrokeLineCap);
         properties.Add(StrokeLineJoin);
+        properties.Add(Opacity);
 
         do
         {

+ 51 - 0
src/PixiEditor.SVG/Elements/SvgText.cs

@@ -0,0 +1,51 @@
+using System.Xml;
+using PixiEditor.SVG.Enums;
+using PixiEditor.SVG.Units;
+
+namespace PixiEditor.SVG.Elements;
+
+public class SvgText() : SvgPrimitive("text")
+{
+    public SvgProperty<SvgStringUnit> Text { get; } = new("");
+    public SvgProperty<SvgNumericUnit> X { get; } = new("x");
+    public SvgProperty<SvgNumericUnit> Y { get; } = new("y");
+    public SvgProperty<SvgNumericUnit> FontSize { get; } = new("font-size");
+    public SvgProperty<SvgStringUnit> FontFamily { get; } = new("font-family");
+    public SvgProperty<SvgEnumUnit<SvgFontWeight>> FontWeight { get; } = new("font-weight");
+    public SvgProperty<SvgEnumUnit<SvgFontStyle>> FontStyle { get; } = new("font-style");
+
+    public override void ParseData(XmlReader reader)
+    {
+        base.ParseData(reader);
+        Text.Unit = new SvgStringUnit(ParseContent(reader));
+    }
+
+    protected override IEnumerable<SvgProperty> GetProperties()
+    {
+        yield return X;
+        yield return Y;
+        yield return FontSize;
+        yield return FontFamily;
+        yield return FontWeight;
+        yield return FontStyle;
+    }
+
+    private string ParseContent(XmlReader reader)
+    {
+        string content = string.Empty;
+        if (reader.NodeType == XmlNodeType.None) return content;
+        while (reader.Read())
+        {
+            if (reader.NodeType == XmlNodeType.Text || reader.NodeType == XmlNodeType.CDATA)
+            {
+                content = reader.Value;
+            }
+            else if (reader is { NodeType: XmlNodeType.EndElement, Name: "text" })
+            {
+                break;
+            }
+        }
+
+        return content;
+    }
+}

+ 8 - 0
src/PixiEditor.SVG/Enums/SvgFontStyle.cs

@@ -0,0 +1,8 @@
+namespace PixiEditor.SVG.Enums;
+
+public enum SvgFontStyle
+{
+    Normal,
+    Italic,
+    Oblique,
+}

+ 9 - 0
src/PixiEditor.SVG/Enums/SvgFontWeight.cs

@@ -0,0 +1,9 @@
+namespace PixiEditor.SVG.Enums;
+
+public enum SvgFontWeight
+{
+    Lighter = 100,
+    Normal = 400,
+    Bold = 700,
+    Bolder = 900,
+}

+ 1 - 0
src/PixiEditor.SVG/Features/IFillable.cs

@@ -6,4 +6,5 @@ namespace PixiEditor.SVG.Features;
 public interface IFillable
 {
     public SvgProperty<SvgColorUnit> Fill { get; }
+    public SvgProperty<SvgNumericUnit> FillOpacity { get; }
 }

+ 8 - 0
src/PixiEditor.SVG/Features/IOpacity.cs

@@ -0,0 +1,8 @@
+using PixiEditor.SVG.Units;
+
+namespace PixiEditor.SVG.Features;
+
+public interface IOpacity
+{
+    public SvgProperty<SvgNumericUnit> Opacity { get; }
+}

+ 154 - 32
src/PixiEditor.SVG/StyleContext.cs

@@ -1,4 +1,6 @@
-using PixiEditor.SVG.Enums;
+using Drawie.Backend.Core.Numerics;
+using Drawie.Numerics;
+using PixiEditor.SVG.Enums;
 using PixiEditor.SVG.Features;
 using PixiEditor.SVG.Units;
 
@@ -9,69 +11,102 @@ public struct StyleContext
     public SvgProperty<SvgNumericUnit> StrokeWidth { get; }
     public SvgProperty<SvgColorUnit> Stroke { get; }
     public SvgProperty<SvgColorUnit> Fill { get; }
+    public SvgProperty<SvgNumericUnit> FillOpacity { get; }
     public SvgProperty<SvgTransformUnit> Transform { get; }
-    
     public SvgProperty<SvgEnumUnit<SvgStrokeLineCap>> StrokeLineCap { get; }
-    
     public SvgProperty<SvgEnumUnit<SvgStrokeLineJoin>> StrokeLineJoin { get; }
+    public SvgProperty<SvgNumericUnit> Opacity { get; }
+    public SvgProperty<SvgStyleUnit> InlineStyle { get; set; }
+    public VecD ViewboxOrigin { get; set; }
 
     public StyleContext()
     {
         StrokeWidth = new("stroke-width");
         Stroke = new("stroke");
         Fill = new("fill");
+        FillOpacity = new("fill-opacity");
+        Fill.Unit = new SvgColorUnit?(new SvgColorUnit("black"));
         Transform = new("transform");
         StrokeLineCap = new("stroke-linecap");
         StrokeLineJoin = new("stroke-linejoin");
+        Opacity = new("opacity");
+        InlineStyle = new("style");
     }
-    
+
     public StyleContext(SvgDocument document)
     {
-        StrokeWidth = document.StrokeWidth;
-        Stroke = document.Stroke;
-        Fill = document.Fill;
-        Transform = document.Transform;
-        StrokeLineCap = document.StrokeLineCap;
-        StrokeLineJoin = document.StrokeLineJoin;
+        StrokeWidth = FallbackToCssStyle(document.StrokeWidth, document.Style);
+        Stroke = FallbackToCssStyle(document.Stroke, document.Style);
+        Fill = FallbackToCssStyle(document.Fill, document.Style, new SvgColorUnit("black"));
+        FillOpacity = FallbackToCssStyle(document.FillOpacity, document.Style);
+        Transform = FallbackToCssStyle(document.Transform, document.Style, new SvgTransformUnit(Matrix3X3.Identity));
+        StrokeLineCap = FallbackToCssStyle(document.StrokeLineCap, document.Style);
+        StrokeLineJoin = FallbackToCssStyle(document.StrokeLineJoin, document.Style);
+        Opacity = FallbackToCssStyle(document.Opacity, document.Style);
+        ViewboxOrigin = new VecD(
+            document.ViewBox.Unit.HasValue ? -document.ViewBox.Unit.Value.Value.X : 0,
+            document.ViewBox.Unit.HasValue ? -document.ViewBox.Unit.Value.Value.Y : 0);
+        InlineStyle = document.Style;
     }
 
     public StyleContext WithElement(SvgElement element)
     {
         StyleContext styleContext = Copy();
 
-        if (element is ITransformable { Transform.Unit: not null } transformableElement)
+        styleContext.InlineStyle = MergeInlineStyle(element.Style, InlineStyle);
+
+        if (element is ITransformable transformableElement)
         {
-            styleContext.Transform.Unit = transformableElement.Transform.Unit;
+            if (styleContext.Transform.Unit == null)
+            {
+                styleContext.Transform.Unit =
+                    FallbackToCssStyle(transformableElement.Transform, styleContext.Transform, styleContext.InlineStyle)
+                        .Unit;
+            }
+            else
+            {
+                styleContext.Transform.Unit = new SvgTransformUnit(
+                    styleContext.Transform.Unit.Value.MatrixValue.Concat(
+                        FallbackToCssStyle(transformableElement.Transform, styleContext.InlineStyle).Unit
+                            ?.MatrixValue ??
+                        Matrix3X3.Identity));
+            }
         }
 
-        if (element is IFillable { Fill.Unit: not null } fillableElement)
+        if (element is IFillable fillableElement)
         {
-            styleContext.Fill.Unit = fillableElement.Fill.Unit;
+            styleContext.Fill.Unit = FallbackToCssStyle(fillableElement.Fill, styleContext.Fill,
+                styleContext.InlineStyle, new SvgColorUnit("black")).Unit;
+            styleContext.FillOpacity.Unit =
+                FallbackToCssStyle(fillableElement.FillOpacity, styleContext.FillOpacity, styleContext.InlineStyle)
+                    .Unit;
         }
 
         if (element is IStrokable strokableElement)
         {
-            if (strokableElement.Stroke.Unit != null)
-            {
-                styleContext.Stroke.Unit = strokableElement.Stroke.Unit;
-            }
+            styleContext.Stroke.Unit =
+                FallbackToCssStyle(strokableElement.Stroke, styleContext.Stroke, styleContext.InlineStyle).Unit;
 
-            if (strokableElement.StrokeWidth.Unit != null)
-            {
-                styleContext.StrokeWidth.Unit = strokableElement.StrokeWidth.Unit;
-            }
-            
-            if (strokableElement.StrokeLineCap.Unit != null)
-            {
-                styleContext.StrokeLineCap.Unit = strokableElement.StrokeLineCap.Unit;
-            }
-            
-            if (strokableElement.StrokeLineJoin.Unit != null)
-            {
-                styleContext.StrokeLineJoin.Unit = strokableElement.StrokeLineJoin.Unit;
-            }
+            styleContext.StrokeWidth.Unit =
+                FallbackToCssStyle(strokableElement.StrokeWidth, styleContext.StrokeWidth, styleContext.InlineStyle)
+                    .Unit;
+
+            styleContext.StrokeLineCap.Unit =
+                FallbackToCssStyle(strokableElement.StrokeLineCap, styleContext.StrokeLineCap, styleContext.InlineStyle)
+                    .Unit;
+
+            styleContext.StrokeLineJoin.Unit =
+                FallbackToCssStyle(strokableElement.StrokeLineJoin, styleContext.StrokeLineJoin,
+                    styleContext.InlineStyle).Unit;
         }
 
+        if (element is IOpacity opacityElement)
+        {
+            styleContext.Opacity.Unit =
+                FallbackToCssStyle(opacityElement.Opacity, styleContext.Opacity, styleContext.InlineStyle).Unit;
+        }
+
+
         return styleContext;
     }
 
@@ -93,11 +128,98 @@ public struct StyleContext
             styleContext.Fill.Unit = Fill.Unit;
         }
 
+        if (FillOpacity.Unit != null)
+        {
+            styleContext.FillOpacity.Unit = FillOpacity.Unit;
+        }
+
         if (Transform.Unit != null)
         {
             styleContext.Transform.Unit = Transform.Unit;
         }
 
+        if (StrokeLineCap.Unit != null)
+        {
+            styleContext.StrokeLineCap.Unit = StrokeLineCap.Unit;
+        }
+
+        if (StrokeLineJoin.Unit != null)
+        {
+            styleContext.StrokeLineJoin.Unit = StrokeLineJoin.Unit;
+        }
+
+        if (Opacity.Unit != null)
+        {
+            styleContext.Opacity.Unit = Opacity.Unit;
+        }
+
+        styleContext.ViewboxOrigin = ViewboxOrigin;
+
+        if (InlineStyle.Unit != null)
+        {
+            styleContext.InlineStyle.Unit = InlineStyle.Unit;
+        }
+
         return styleContext;
     }
+
+
+    private SvgProperty<TUnit>? FallbackToCssStyle<TUnit>(
+        SvgProperty<TUnit> property,
+        SvgProperty<SvgStyleUnit> inlineStyle, TUnit? fallback = null) where TUnit : struct, ISvgUnit
+    {
+        if (property.Unit != null)
+        {
+            return property;
+        }
+
+        SvgStyleUnit? style = inlineStyle.Unit;
+        return style?.TryGetStyleFor<SvgProperty<TUnit>, TUnit>(property.SvgName)
+               ?? (fallback.HasValue
+                   ? new SvgProperty<TUnit>(property.SvgName) { Unit = fallback.Value }
+                   : new SvgProperty<TUnit>(property.SvgName));
+    }
+
+    private SvgProperty<TUnit>? FallbackToCssStyle<TUnit>(
+        SvgProperty<TUnit> property,
+        SvgProperty<TUnit> parentStyleProperty,
+        SvgProperty<SvgStyleUnit> inlineStyle, TUnit? fallback = null) where TUnit : struct, ISvgUnit
+    {
+        if (property.Unit != null)
+        {
+            return property;
+        }
+
+        SvgStyleUnit? style = inlineStyle.Unit;
+        var styleProp = style?.TryGetStyleFor<SvgProperty<TUnit>, TUnit>(property.SvgName);
+        if (styleProp != null) return styleProp;
+        if(parentStyleProperty.Unit != null)
+        {
+            return parentStyleProperty;
+        }
+
+        return (fallback.HasValue
+            ? new SvgProperty<TUnit>(property.SvgName) { Unit = fallback.Value }
+            : new SvgProperty<TUnit>(property.SvgName));
+    }
+
+    private SvgProperty<SvgStyleUnit> MergeInlineStyle(SvgProperty<SvgStyleUnit> elementStyle,
+        SvgProperty<SvgStyleUnit> parentStyle)
+    {
+        SvgStyleUnit? elementStyleUnit = elementStyle.Unit;
+        SvgStyleUnit? parentStyleUnit = parentStyle.Unit;
+
+        if (elementStyleUnit == null)
+        {
+            return parentStyle;
+        }
+
+        if (parentStyleUnit == null)
+        {
+            return elementStyle;
+        }
+
+        SvgStyleUnit style = parentStyleUnit.Value.MergeWith(elementStyleUnit.Value);
+        return new SvgProperty<SvgStyleUnit>("style") { Unit = style };
+    }
 }

+ 16 - 13
src/PixiEditor.SVG/SvgDocument.cs

@@ -7,27 +7,28 @@ using PixiEditor.SVG.Units;
 
 namespace PixiEditor.SVG;
 
-public class SvgDocument : SvgElement, IElementContainer, ITransformable, IFillable, IStrokable
+public class SvgDocument : SvgElement, IElementContainer, ITransformable, IFillable, IStrokable, IOpacity
 {
     public string RootNamespace { get; set; } = "http://www.w3.org/2000/svg";
     public string Version { get; set; } = "1.1";
-    
+
     public SvgProperty<SvgRectUnit> ViewBox { get; } = new("viewBox");
     public SvgProperty<SvgColorUnit> Fill { get; } = new("fill");
     public SvgProperty<SvgColorUnit> Stroke { get; } = new("stroke");
     public SvgProperty<SvgNumericUnit> StrokeWidth { get; } = new("stroke-width");
-    
+
     public SvgProperty<SvgEnumUnit<SvgStrokeLineCap>> StrokeLineCap { get; } = new("stroke-linecap");
-    
+
     public SvgProperty<SvgEnumUnit<SvgStrokeLineJoin>> StrokeLineJoin { get; } = new("stroke-linejoin");
     public SvgProperty<SvgTransformUnit> Transform { get; } = new("transform");
+    public SvgProperty<SvgNumericUnit> Opacity { get; } = new("opacity");
+    public SvgProperty<SvgNumericUnit> FillOpacity { get; } = new("fill-opacity");
     public List<SvgElement> Children { get; } = new();
 
     public SvgDocument() : base("svg")
     {
-        
     }
-    
+
     public SvgDocument(RectD viewBox) : base("svg")
     {
         ViewBox.Unit = new SvgRectUnit(viewBox);
@@ -38,14 +39,16 @@ public class SvgDocument : SvgElement, IElementContainer, ITransformable, IFilla
         List<SvgProperty> properties = new()
         {
             Fill,
+            FillOpacity,
             Stroke,
             StrokeWidth,
             Transform,
             ViewBox,
             StrokeLineCap,
-            StrokeLineJoin
+            StrokeLineJoin,
+            Opacity
         };
-        
+
         ParseAttributes(properties, reader);
     }
 
@@ -102,11 +105,11 @@ public class SvgDocument : SvgElement, IElementContainer, ITransformable, IFilla
 
     private void AppendProperties(XElement? root)
     {
-        if(ViewBox.Unit != null)
+        if (ViewBox.Unit != null)
         {
             root.Add(new XAttribute("viewBox", ViewBox.Unit.Value.ToXml()));
         }
-        
+
         if (Fill.Unit != null)
         {
             root.Add(new XAttribute("fill", Fill.Unit.Value.ToXml()));
@@ -121,17 +124,17 @@ public class SvgDocument : SvgElement, IElementContainer, ITransformable, IFilla
         {
             root.Add(new XAttribute("stroke-width", StrokeWidth.Unit.Value.ToXml()));
         }
-        
+
         if (Transform.Unit != null)
         {
             root.Add(new XAttribute("transform", Transform.Unit.Value.ToXml()));
         }
-        
+
         if (StrokeLineCap.Unit != null)
         {
             root.Add(new XAttribute("stroke-linecap", StrokeLineCap.Unit.Value.ToXml()));
         }
-        
+
         if (StrokeLineJoin.Unit != null)
         {
             root.Add(new XAttribute("stroke-linejoin", StrokeLineJoin.Unit.Value.ToXml()));

+ 24 - 23
src/PixiEditor.SVG/SvgElement.cs

@@ -13,6 +13,7 @@ public class SvgElement(string tagName)
     public Dictionary<string, string> RequiredNamespaces { get; } = new();
     public string TagName { get; } = tagName;
 
+    public SvgProperty<SvgStyleUnit> Style { get; } = new("style");
 
     public XElement ToXml(XNamespace nameSpace)
     {
@@ -25,14 +26,21 @@ public class SvgElement(string tagName)
                 SvgProperty prop = (SvgProperty)property.GetValue(this);
                 if (prop?.Unit != null)
                 {
-                    if (!string.IsNullOrEmpty(prop.NamespaceName))
+                    if (string.IsNullOrEmpty(prop.SvgName))
                     {
-                        XName name = XNamespace.Get(RequiredNamespaces[prop.NamespaceName]) + prop.SvgName;
-                        element.Add(new XAttribute(name, prop.Unit.ToXml()));
+                        element.Value = prop.Unit.ToXml();
                     }
                     else
                     {
-                        element.Add(new XAttribute(prop.SvgName, prop.Unit.ToXml()));
+                        if (!string.IsNullOrEmpty(prop.NamespaceName))
+                        {
+                            XName name = XNamespace.Get(RequiredNamespaces[prop.NamespaceName]) + prop.SvgName;
+                            element.Add(new XAttribute(name, prop.Unit.ToXml()));
+                        }
+                        else
+                        {
+                            element.Add(new XAttribute(prop.SvgName, prop.Unit.ToXml()));
+                        }
                     }
                 }
             }
@@ -57,6 +65,16 @@ public class SvgElement(string tagName)
 
     protected void ParseAttributes(List<SvgProperty> properties, XmlReader reader)
     {
+        if (!properties.Contains(Id))
+        {
+            properties.Insert(0, Id);
+        }
+
+        if (!properties.Contains(Style))
+        {
+            properties.Insert(0, Style);
+        }
+
         do
         {
             SvgProperty matchingProperty = properties.FirstOrDefault(x =>
@@ -76,31 +94,14 @@ public class SvgElement(string tagName)
         }
         else
         {
-            property.Unit ??= CreateDefaultUnit(property);
+            property.Unit ??= property.CreateDefaultUnit();
             property.Unit.ValuesFromXml(reader.Value);
         }
     }
 
     private void ParseListProperty(SvgList list, XmlReader reader)
     {
-        list.Unit ??= CreateDefaultUnit(list);
+        list.Unit ??= list.CreateDefaultUnit();
         list.Unit.ValuesFromXml(reader.Value);
     }
-
-    private ISvgUnit CreateDefaultUnit(SvgProperty property)
-    {
-        var genericType = property.GetType().GetGenericArguments();
-        if (genericType.Length == 0)
-        {
-            throw new InvalidOperationException("Property does not have a generic type");
-        }
-
-        ISvgUnit unit = Activator.CreateInstance(genericType[0]) as ISvgUnit;
-        if (unit == null)
-        {
-            throw new InvalidOperationException("Could not create unit");
-        }
-
-        return unit;
-    }
 }

+ 14 - 12
src/PixiEditor.SVG/SvgParser.cs

@@ -1,4 +1,5 @@
-using System.Xml;
+using System.Globalization;
+using System.Xml;
 using System.Xml.Linq;
 using Drawie.Numerics;
 using PixiEditor.SVG.Elements;
@@ -19,7 +20,8 @@ public class SvgParser
         { "g", typeof(SvgGroup) },
         { "mask", typeof(SvgMask) },
         { "image", typeof(SvgImage) },
-        { "svg", typeof(SvgDocument) }
+        { "svg", typeof(SvgDocument) },
+        { "text", typeof(SvgText) }
     };
 
     public string Source { get; set; }
@@ -35,7 +37,7 @@ public class SvgParser
         using var reader = document.CreateReader();
 
         XmlNodeType node = reader.MoveToContent();
-        if (node != XmlNodeType.Element || reader.Name != "svg")
+        if (node != XmlNodeType.Element || reader.LocalName != "svg")
         {
             return null;
         }
@@ -92,7 +94,7 @@ public class SvgParser
 
     private SvgElement? ParseElement(XmlReader reader)
     {
-        if (wellKnownElements.TryGetValue(reader.Name, out Type elementType))
+        if (wellKnownElements.TryGetValue(reader.LocalName, out Type elementType))
         {
             SvgElement element = (SvgElement)Activator.CreateInstance(elementType);
             if (reader.MoveToFirstAttribute())
@@ -129,16 +131,16 @@ public class SvgParser
             string[] parts = viewBox.Split(' ').Where(s => !string.IsNullOrWhiteSpace(s)).ToArray();
             if (parts.Length == 4)
             {
-                finalX = double.Parse(parts[0]);
-                finalY = double.Parse(parts[1]);
-                finalWidth = double.Parse(parts[2]);
-                finalHeight = double.Parse(parts[3]);
+                finalX = double.Parse(parts[0], CultureInfo.InvariantCulture);
+                finalY = double.Parse(parts[1], CultureInfo.InvariantCulture);
+                finalWidth = double.Parse(parts[2], CultureInfo.InvariantCulture);
+                finalHeight = double.Parse(parts[3], CultureInfo.InvariantCulture);
             }
         }
 
         if (x != null)
         {
-            if (double.TryParse(x, out double xValue))
+            if (double.TryParse(x, CultureInfo.InvariantCulture, out double xValue))
             {
                 finalX = xValue;
             }
@@ -146,7 +148,7 @@ public class SvgParser
 
         if (y != null)
         {
-            if (double.TryParse(y, out double yValue))
+            if (double.TryParse(y, CultureInfo.InvariantCulture, out double yValue))
             {
                 finalY = yValue;
             }
@@ -154,7 +156,7 @@ public class SvgParser
 
         if (width != null)
         {
-            if (double.TryParse(width, out double widthValue))
+            if (double.TryParse(width, CultureInfo.InvariantCulture, out double widthValue))
             {
                 finalWidth = widthValue;
             }
@@ -162,7 +164,7 @@ public class SvgParser
 
         if (height != null)
         {
-            if (double.TryParse(height, out double heightValue))
+            if (double.TryParse(height, CultureInfo.InvariantCulture, out double heightValue))
             {
                 finalHeight = heightValue;
             }

+ 20 - 3
src/PixiEditor.SVG/SvgProperty.cs

@@ -9,7 +9,7 @@ public abstract class SvgProperty
     {
         SvgName = svgName;
     }
-    
+
     protected SvgProperty(string svgName, string? namespaceName) : this(svgName)
     {
         NamespaceName = namespaceName;
@@ -18,6 +18,23 @@ public abstract class SvgProperty
     public string? NamespaceName { get; set; }
     public string SvgName { get; set; }
     public ISvgUnit? Unit { get; set; }
+
+    public ISvgUnit? CreateDefaultUnit()
+    {
+        var genericType = this.GetType().GetGenericArguments();
+        if (genericType.Length == 0)
+        {
+            return null;
+        }
+
+        ISvgUnit unit = Activator.CreateInstance(genericType[0]) as ISvgUnit;
+        if (unit == null)
+        {
+            throw new InvalidOperationException("Could not create unit");
+        }
+
+        return unit;
+    }
 }
 
 public class SvgProperty<T> : SvgProperty where T : struct, ISvgUnit
@@ -27,11 +44,11 @@ public class SvgProperty<T> : SvgProperty where T : struct, ISvgUnit
         get => (T?)base.Unit;
         set => base.Unit = value;
     }
-    
+
     public SvgProperty(string svgName) : base(svgName)
     {
     }
-    
+
     public SvgProperty(string svgName, string? namespaceName) : base(svgName, namespaceName)
     {
     }

+ 35 - 15
src/PixiEditor.SVG/Units/SvgNumericUnit.cs

@@ -6,32 +6,51 @@ public struct SvgNumericUnit(double value, string postFix) : ISvgUnit
 {
     public string PostFix { get; set; } = postFix;
     public double Value { get; set; } = value;
+    public double? PixelsValue => ConvertTo(SvgNumericUnits.Px);
+
+    public double? ConvertTo(SvgNumericUnits other)
+    {
+        SvgNumericUnits? numericUnit = SvgNumericUnitsExtensions.TryParseUnit(PostFix);
+
+        if (numericUnit == null || !numericUnit.Value.IsSizeUnit() || !numericUnit.Value.IsAbsoluteUnit())
+        {
+            return null;
+        }
+
+        double? pixelsValue = SvgNumericConverter.ToPixels(Value, numericUnit.Value);
+        if (pixelsValue == null)
+        {
+            return null;
+        }
+
+        return SvgNumericConverter.FromPixels(pixelsValue.Value, other);
+    }
 
     public static SvgNumericUnit FromUserUnits(double value)
     {
         return new SvgNumericUnit(value, string.Empty);
     }
-    
+
     public static SvgNumericUnit FromPixels(double value)
     {
         return new SvgNumericUnit(value, "px");
     }
-    
+
     public static SvgNumericUnit FromInches(double value)
     {
         return new SvgNumericUnit(value, "in");
     }
-    
+
     public static SvgNumericUnit FromCentimeters(double value)
     {
         return new SvgNumericUnit(value, "cm");
     }
-    
+
     public static SvgNumericUnit FromMillimeters(double value)
     {
         return new SvgNumericUnit(value, "mm");
     }
-    
+
     public static SvgNumericUnit FromPercent(double value)
     {
         return new SvgNumericUnit(value, "%");
@@ -46,7 +65,7 @@ public struct SvgNumericUnit(double value, string postFix) : ISvgUnit
     public void ValuesFromXml(string readerValue)
     {
         string? extractedPostFix = ExtractPostFix(readerValue);
-        
+
         if (extractedPostFix == null)
         {
             if (double.TryParse(readerValue, NumberStyles.Any, CultureInfo.InvariantCulture, out double result))
@@ -65,7 +84,7 @@ public struct SvgNumericUnit(double value, string postFix) : ISvgUnit
             }
         }
     }
-    
+
     private string? ExtractPostFix(string readerValue)
     {
         if (readerValue.Length == 0)
@@ -74,21 +93,22 @@ public struct SvgNumericUnit(double value, string postFix) : ISvgUnit
         }
 
         int postFixStartIndex = readerValue.Length;
-        
+
+        if (char.IsDigit(readerValue[^1]))
+        {
+            return null;
+        }
+
         for (int i = readerValue.Length - 1; i >= 0; i--)
         {
-            if (!char.IsDigit(readerValue[i]))
+            if (char.IsDigit(readerValue[i]))
             {
                 postFixStartIndex = i + 1;
                 break;
             }
         }
-        
-        if (postFixStartIndex == readerValue.Length)
-        {
-            return null;
-        }
-        
+
+
         return readerValue.Substring(postFixStartIndex);
     }
 }

+ 142 - 0
src/PixiEditor.SVG/Units/SvgNumericUnits.cs

@@ -0,0 +1,142 @@
+namespace PixiEditor.SVG.Units;
+
+public enum SvgNumericUnits
+{
+    Px,
+    In,
+    Cm,
+    Mm,
+    Pt,
+    Pc,
+    Em,
+    Ex,
+    Ch,
+    Rem,
+    Vw,
+    Vh,
+    Vmin,
+    Vmax,
+    Percent,
+    Deg,
+    Rad,
+    Grad,
+    Turn,
+    S,
+    Ms,
+    Min,
+    H,
+    Mmss,
+    Hhmmss,
+}
+
+public static class SvgNumericConverter
+{
+    public static double? ToPixels(double value, SvgNumericUnits unit)
+    {
+        if (!unit.IsAbsoluteUnit() && !unit.IsSizeUnit()) return null;
+
+        return unit switch
+        {
+            SvgNumericUnits.Px => value,
+            SvgNumericUnits.In => value * 96,
+            SvgNumericUnits.Cm => value * 37.795,
+            SvgNumericUnits.Mm => value * 3.7795,
+            SvgNumericUnits.Pt => value * 1.3333,
+            SvgNumericUnits.Pc => value * 16,
+            _ => null,
+        };
+    }
+
+    public static double? FromPixels(double pixelsValue, SvgNumericUnits other)
+    {
+        if (!other.IsAbsoluteUnit() && !other.IsSizeUnit()) return null;
+
+        return other switch
+        {
+            SvgNumericUnits.Px => pixelsValue,
+            SvgNumericUnits.In => pixelsValue / 96,
+            SvgNumericUnits.Cm => pixelsValue / 37.795,
+            SvgNumericUnits.Mm => pixelsValue / 3.7795,
+            SvgNumericUnits.Pt => pixelsValue / 1.3333,
+            SvgNumericUnits.Pc => pixelsValue / 16,
+            _ => null,
+        };
+    }
+}
+
+public static class SvgNumericUnitsExtensions
+{
+    public static bool IsSizeUnit(this SvgNumericUnits unit)
+    {
+        return unit switch
+        {
+            SvgNumericUnits.Px => true,
+            SvgNumericUnits.In => true,
+            SvgNumericUnits.Cm => true,
+            SvgNumericUnits.Mm => true,
+            SvgNumericUnits.Pt => true,
+            SvgNumericUnits.Pc => true,
+            SvgNumericUnits.Em => true,
+            SvgNumericUnits.Ex => true,
+            SvgNumericUnits.Ch => true,
+            SvgNumericUnits.Rem => true,
+            SvgNumericUnits.Vw => true,
+            SvgNumericUnits.Vh => true,
+            SvgNumericUnits.Vmin => true,
+            SvgNumericUnits.Vmax => true,
+            SvgNumericUnits.Percent => true,
+            _ => false,
+        };
+    }
+
+    public static bool IsAbsoluteUnit(this SvgNumericUnits unit)
+    {
+        return unit switch
+        {
+            SvgNumericUnits.Px => true,
+            SvgNumericUnits.In => true,
+            SvgNumericUnits.Cm => true,
+            SvgNumericUnits.Mm => true,
+            SvgNumericUnits.Pt => true,
+            SvgNumericUnits.Pc => true,
+            SvgNumericUnits.Rad => true,
+            SvgNumericUnits.Deg => true,
+            SvgNumericUnits.Grad => true,
+            _ => false,
+        };
+    }
+
+    public static SvgNumericUnits? TryParseUnit(string postFix)
+    {
+        if (string.IsNullOrWhiteSpace(postFix)) return SvgNumericUnits.Px;
+        return postFix.ToLower().Trim() switch
+        {
+            "px" => SvgNumericUnits.Px,
+            "in" => SvgNumericUnits.In,
+            "cm" => SvgNumericUnits.Cm,
+            "mm" => SvgNumericUnits.Mm,
+            "pt" => SvgNumericUnits.Pt,
+            "pc" => SvgNumericUnits.Pc,
+            "em" => SvgNumericUnits.Em,
+            "ex" => SvgNumericUnits.Ex,
+            "ch" => SvgNumericUnits.Ch,
+            "rem" => SvgNumericUnits.Rem,
+            "vw" => SvgNumericUnits.Vw,
+            "vh" => SvgNumericUnits.Vh,
+            "vmin" => SvgNumericUnits.Vmin,
+            "vmax" => SvgNumericUnits.Vmax,
+            "%" => SvgNumericUnits.Percent,
+            "deg" => SvgNumericUnits.Deg,
+            "rad" => SvgNumericUnits.Rad,
+            "grad" => SvgNumericUnits.Grad,
+            "turn" => SvgNumericUnits.Turn,
+            "s" => SvgNumericUnits.S,
+            "ms" => SvgNumericUnits.Ms,
+            "min" => SvgNumericUnits.Min,
+            "h" => SvgNumericUnits.H,
+            "mm:ss" => SvgNumericUnits.Mmss,
+            "hh:mm:ss" => SvgNumericUnits.Hhmmss,
+            _ => null,
+        };
+    }
+}

+ 73 - 0
src/PixiEditor.SVG/Units/SvgStyleUnit.cs

@@ -0,0 +1,73 @@
+namespace PixiEditor.SVG.Units;
+
+public struct SvgStyleUnit : ISvgUnit
+{
+    private Dictionary<string, string> inlineDefinedProperties;
+    private string value;
+
+    public SvgStyleUnit(string inlineStyle)
+    {
+        Value = inlineStyle;
+    }
+
+    public string Value
+    {
+        get => value;
+        set
+        {
+            this.value = value;
+            inlineDefinedProperties = new Dictionary<string, string>();
+
+            if (string.IsNullOrEmpty(value))
+            {
+                return;
+            }
+
+            string[] properties = value.Split(';');
+            foreach (string property in properties)
+            {
+                string[] keyValue = property.Split(':');
+                if (keyValue.Length == 2)
+                {
+                    inlineDefinedProperties.Add(keyValue[0].Trim(), keyValue[1].Trim());
+                }
+            }
+        }
+    }
+
+    public string ToXml()
+    {
+        return Value;
+    }
+
+    public void ValuesFromXml(string readerValue)
+    {
+        Value = readerValue;
+    }
+
+    public TProp TryGetStyleFor<TProp, TUnit>(string property) where TProp : SvgProperty<TUnit> where TUnit : struct, ISvgUnit
+    {
+        if (inlineDefinedProperties.TryGetValue(property, out var definedProperty))
+        {
+            TProp prop = (TProp)Activator.CreateInstance(typeof(TProp), property);
+            var unit = (TUnit)prop.CreateDefaultUnit();
+            unit.ValuesFromXml(definedProperty);
+            prop.Unit = unit;
+
+            return prop;
+        }
+
+        return null;
+    }
+
+    public SvgStyleUnit MergeWith(SvgStyleUnit elementStyleUnit)
+    {
+        Dictionary<string, string> props = new(inlineDefinedProperties);
+        foreach (var inlineDefined in elementStyleUnit.inlineDefinedProperties)
+        {
+            props[inlineDefined.Key] = inlineDefined.Value;
+        }
+
+        return new SvgStyleUnit(string.Join(";", props.Select(x => $"{x.Key}:{x.Value}")));
+    }
+}

+ 98 - 4
src/PixiEditor.SVG/Units/SvgTransformUnit.cs

@@ -11,12 +11,12 @@ public struct SvgTransformUnit : ISvgUnit
     }
 
     public Matrix3X3 MatrixValue { get; set; } = Matrix3X3.Identity;
-    
+
     public SvgTransformUnit(Matrix3X3 matrixValue)
     {
         MatrixValue = matrixValue;
     }
-    
+
     public string ToXml()
     {
         string translateX = MatrixValue.TransX.ToString(CultureInfo.InvariantCulture);
@@ -25,7 +25,7 @@ public struct SvgTransformUnit : ISvgUnit
         string scaleY = MatrixValue.ScaleY.ToString(CultureInfo.InvariantCulture);
         string skewX = MatrixValue.SkewX.ToString(CultureInfo.InvariantCulture);
         string skewY = MatrixValue.SkewY.ToString(CultureInfo.InvariantCulture);
-        
+
         return $"matrix({scaleX}, {skewY}, {skewX}, {scaleY}, {translateX}, {translateY})";
     }
 
@@ -49,7 +49,101 @@ public struct SvgTransformUnit : ISvgUnit
         }
         else
         {
-            // todo: parse other types of transformation syntax (rotate, translate, scale etc)
+            MatrixValue = TryParseDescriptiveTransform(readerValue);
+        }
+    }
+
+    private Matrix3X3 TryParseDescriptiveTransform(string readerValue)
+    {
+        if (!readerValue.Contains('(') || !readerValue.Contains(')'))
+        {
+            return Matrix3X3.Identity;
+        }
+
+        string[] parts = readerValue.Split(')').Where(s => !string.IsNullOrWhiteSpace(s)).ToArray();
+
+        Matrix3X3 result = Matrix3X3.Identity;
+        for (int i = 0; i < parts.Length; i++)
+        {
+            string[] part = parts[i].Split('(');
+            if (part.Length != 2)
+            {
+                continue;
+            }
+
+            result = result.Concat(ParsePart(part));
+        }
+
+        return result;
+    }
+
+    private static Matrix3X3 ParsePart(string[] part)
+    {
+        string transformType = part[0].Trim();
+        string[] values = part[1].Split(' ', ')', ',').Where(s => !string.IsNullOrWhiteSpace(s)).ToArray();
+
+        if (values.Length == 0)
+        {
+            return Matrix3X3.Identity;
+        }
+
+        if (transformType == "translate")
+        {
+            if (float.TryParse(values[0], NumberStyles.Any, CultureInfo.InvariantCulture, out float translateX))
+            {
+                float translateY = translateX;
+                if (values.Length > 1)
+                {
+                    float.TryParse(values[1], NumberStyles.Any, CultureInfo.InvariantCulture, out translateY);
+                }
+
+                return Matrix3X3.CreateTranslation(translateX, translateY);
+            }
+        }
+        else if (transformType == "scale")
+        {
+            if (float.TryParse(values[0], NumberStyles.Any, CultureInfo.InvariantCulture, out float scaleX))
+            {
+                float scaleY = scaleX;
+                if (values.Length > 1)
+                {
+                    float.TryParse(values[1], NumberStyles.Any, CultureInfo.InvariantCulture, out scaleY);
+                }
+
+                return Matrix3X3.CreateScale(scaleX, scaleY);
+            }
         }
+        else if (transformType == "rotate")
+        {
+            if (float.TryParse(values[0], NumberStyles.Any, CultureInfo.InvariantCulture, out float angle))
+            {
+                float radians = angle * (float)Math.PI / 180;
+
+                if (values.Length > 2)
+                {
+                    float.TryParse(values[1], NumberStyles.Any, CultureInfo.InvariantCulture, out float centerX);
+                    float.TryParse(values[2], NumberStyles.Any, CultureInfo.InvariantCulture, out float centerY);
+                    return Matrix3X3.CreateRotation(radians, centerX, centerY);
+                }
+
+                return Matrix3X3.CreateRotation(radians);
+            }
+        }
+        else if (transformType == "skewX")
+        {
+            if (float.TryParse(values[0], NumberStyles.Any, CultureInfo.InvariantCulture, out float skewX))
+            {
+                return Matrix3X3.CreateSkew(skewX, 0);
+            }
+        }
+        else if (transformType == "skewY")
+        {
+            if (float.TryParse(values[0], NumberStyles.Any, CultureInfo.InvariantCulture, out float skewY))
+            {
+                return Matrix3X3.CreateSkew(0, skewY);
+            }
+        }
+
+        return Matrix3X3.Identity;
     }
 }

+ 2 - 0
src/PixiEditor.UI.Common/Accents/Base.axaml

@@ -54,6 +54,7 @@
             <Color x:Key="VecDSocketColor">#c984ca</Color>
             <Color x:Key="VecISocketColor">#c9b4ca</Color>
             <Color x:Key="IntSocketColor">#4C64B1</Color>
+            <Color x:Key="StringSocketColor">#C9E4C6</Color>
             <Color x:Key="EllipseDataSocketColor">#a473a5</Color>
             <Color x:Key="PointsDataSocketColor">#e1d0e1</Color>
             <Color x:Key="TextDataSocketColor">#f2f2f2</Color>
@@ -145,6 +146,7 @@
             <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}"/>
             <ConicGradientBrush x:Key="ShapeVectorDataSocketBrush" GradientStops="{StaticResource ShapeDataSocketGradient}"/>
             <SolidColorBrush x:Key="EllipseVectorDataSocketBrush" Color="{StaticResource EllipseDataSocketColor}"/>
             <SolidColorBrush x:Key="PointsVectorDataSocketBrush" Color="{StaticResource PointsDataSocketColor}"/>

BIN
src/PixiEditor.UI.Common/Fonts/PixiPerfect.ttf


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

@@ -3,7 +3,7 @@
         xmlns:system="clr-namespace:System;assembly=System.Runtime">
     <Styles.Resources>
         <ResourceDictionary>
-            <FontFamily x:Key="PixiPerfectIcons">avares://PixiEditor.UI.Common/Fonts/PixiPerfect.ttf#PixiPerfect</FontFamily>
+            <FontFamily x:Key="PixiPerfectIcons">avares://PixiEditor.UI.Common/Fonts/PixiPerfect.ttf#pixiperfect</FontFamily>
 
             <system:String x:Key="icon-add-reference">&#xE900;</system:String>
             <system:String x:Key="icon-add-to-mask">&#xE901;</system:String>

+ 0 - 1
src/PixiEditor/App.axaml

@@ -21,7 +21,6 @@
         <StyleInclude Source="/Styles/PixiEditor.Layers.axaml" />
         <StyleInclude Source="/Styles/PixiEditorPopupTemplate.axaml" />
         <StyleInclude Source="/Styles/Buttons/CaptionButtonsStyle.axaml" />
-        <StyleInclude Source="/Styles/NodeIcons.axaml"/>
     </Application.Styles>
     <Application.Resources>
         <ResourceDictionary>

+ 9 - 3
src/PixiEditor/Data/Localization/Languages/en.json

@@ -282,7 +282,7 @@
   "EDITOR_DATA": "Editor data (Local)",
   "MOVE_VIEWPORT_TOOLTIP": "Moves viewport. ({0})",
   "MOVE_VIEWPORT_ACTION_DISPLAY": "Click and move to pan the viewport",
-  "MOVE_TOOL_TOOLTIP": "Moves selected pixels ({0}). Hold Ctrl to move all layers.",
+  "MOVE_TOOL_TOOLTIP": "Select and transform layers ({0}).",
   "MOVE_TOOL_ACTION_DISPLAY": "Hold mouse to move selected pixels. Hold Ctrl to move all layers.",
   "PEN_TOOL_TOOLTIP": "Pen. ({0})",
   "PEN_TOOL_ACTION_DISPLAY": "Click and move to draw.",
@@ -799,7 +799,7 @@
   "PREVIOUS_TOOL_SET": "Previous tool set",
   "FILL_MODE": "Fill mode",
   "USE_LINEAR_SRGB_PROCESSING": "Use linear sRGB for processing colors",
-  "USE_LINEAR_SRGB_PROCESSING_DESC": "Convert document using legacy blending mode to linear sRGB for processing colors. This will affect the colors of the document, but will make blending more accurate.",
+  "USE_LINEAR_SRGB_PROCESSING_DESC": "Convert document using sRGB blending mode to linear sRGB for processing colors. This will affect the colors of the document, but will make blending more accurate.",
   "FILL_TYPE_WINDING": "Winding",
   "FILL_TYPE_EVEN_ODD": "Even Odd",
   "FILL_TYPE_INVERSE_WINDING": "Inverse Winding",
@@ -849,5 +849,11 @@
   "BOLD_TOOLTIP": "Bold",
   "ITALIC_TOOLTIP": "Italic",
   "CUSTOM_FONT": "Custom font",
-  "DUMP_GPU_DIAGNOSTICS": "Dump GPU diagnostics"
+  "DUMP_GPU_DIAGNOSTICS": "Dump GPU diagnostics",
+  "USE_SRGB_PROCESSING": "Use sRGB for processing colors",
+  "USE_SRGB_PROCESSING_DESC": "Convert document using linear sRGB to sRGB for processing colors. This will affect the colors of the document.",
+  "TEXT_NODE": "Text",
+  "TEXT_LABEL": "Text",
+  "TEXT_ON_PATH_NODE": "Text on Path",
+  "HIGH_DPI_RENDERING": "High DPI Rendering"
 }

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

@@ -33,7 +33,7 @@ internal class DocumentViewModelBuilder
 
     public NodeGraphBuilder Graph { get; set; }
     public string ImageEncoderUsed { get; set; } = "QOI";
-    public bool UsesLegacyColorBlending { get; set; } = false;
+    public bool UsesSrgbColorBlending { get; set; } = false;
     public Version? PixiParserVersionUsed { get; set; }
     public ResourceStorage DocumentResources { get; set; }
 
@@ -127,9 +127,9 @@ internal class DocumentViewModelBuilder
         return this;
     }
 
-    public DocumentViewModelBuilder WithLegacyColorBlending(bool usesLegacyColorBlending)
+    public DocumentViewModelBuilder? WithSrgbColorBlending(bool usesLegacyColorBlending)
     {
-        UsesLegacyColorBlending = usesLegacyColorBlending;
+        UsesSrgbColorBlending = usesLegacyColorBlending;
         return this;
     }
 

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

@@ -32,7 +32,7 @@ internal static class PixiParserDocumentEx
         return DocumentViewModel.Build(b => b
             .WithPixiParserVersion(document.Version)
             .WithSerializerData(document.SerializerName, document.SerializerVersion)
-            .WithLegacyColorBlending(document.LegacyColorBlending)
+            .WithSrgbColorBlending(document.SrgbColorBlending)
             .WithSize(document.Width, document.Height)
             .WithImageEncoder(document.ImageEncoderUsed)
             .WithPalette(document.Palette, color => new PaletteColor(color.R, color.G, color.B))

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

@@ -86,7 +86,7 @@ internal class ClassicDesktopEntry
             return;
         }
 
-#if !STEAM
+#if !STEAM && !DEBUG
         if (!HandleNewInstance(dispatcher))
         {
             return;

+ 10 - 0
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/UpdateableChangeExecutor.cs

@@ -62,4 +62,14 @@ internal abstract class UpdateableChangeExecutor
     public virtual void OnSettingsChanged(string name, object value) { }
     public virtual void OnColorChanged(Color color, bool primary) { }
     public virtual void OnMembersSelected(List<Guid> memberGuids) { }
+
+    protected T[] QueryLayers<T>(VecD pos) where T : ILayerHandler
+    {
+        var allLayers = document.StructureHelper.GetAllLayers();
+        var topMostWithinClick = allLayers.Where(x =>
+                x is T { IsVisibleBindable: true, TightBounds: not null } &&
+                x.TightBounds.Value.ContainsInclusive(pos))
+            .OrderByDescending(x => allLayers.IndexOf(x));
+        return topMostWithinClick.Cast<T>().ToArray();
+    }
 }

+ 44 - 8
src/PixiEditor/Models/DocumentModels/UpdateableChangeExecutors/VectorTextToolExecutor.cs

@@ -3,8 +3,10 @@ using Avalonia.Threading;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Text;
+using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 using PixiEditor.ChangeableDocument.Actions.Generated;
+using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 using PixiEditor.Helpers.Extensions;
 using PixiEditor.Models.Controllers.InputDevice;
@@ -28,6 +30,7 @@ internal class VectorTextToolExecutor : UpdateableChangeExecutor, ITextOverlayEv
     private Matrix3X3 lastMatrix = Matrix3X3.Identity;
     private Font? cachedFont;
     private bool isListeningForValidLayer;
+    private VectorPath? onPath;
 
     public override bool BlocksOtherActions => false;
 
@@ -72,6 +75,7 @@ internal class VectorTextToolExecutor : UpdateableChangeExecutor, ITextOverlayEv
             toolbar.Bold = textData.Font.Bold;
             toolbar.Italic = textData.Font.Italic;
 
+            onPath = textData.Path;
             lastText = textData.Text;
             position = textData.Position;
             lastMatrix = textData.TransformationMatrix;
@@ -82,6 +86,11 @@ internal class VectorTextToolExecutor : UpdateableChangeExecutor, ITextOverlayEv
                 Matrix3X3.Identity, toolbar.Spacing);
             lastText = "";
             position = controller.LastPrecisePosition;
+            // TODO: Implement proper putting on path editing
+            /*if (controller.LeftMousePressed)
+            {
+                TryPutOnPath(controller.LastPrecisePosition);
+            }*/
         }
         else
         {
@@ -93,11 +102,7 @@ internal class VectorTextToolExecutor : UpdateableChangeExecutor, ITextOverlayEv
 
     public override void OnLeftMouseButtonDown(MouseOnCanvasEventArgs args)
     {
-        var allLayers = document.StructureHelper.GetAllLayers();
-        var topMostWithinClick = allLayers.Where(x =>
-                x is IVectorLayerHandler { IsVisibleBindable: true, TightBounds: not null } &&
-                x.TightBounds.Value.ContainsInclusive(args.PositionOnCanvas))
-            .OrderByDescending(x => allLayers.IndexOf(x));
+        var topMostWithinClick = QueryLayers<IVectorLayerHandler>(args.PositionOnCanvas);
 
         var firstLayer = topMostWithinClick.FirstOrDefault();
         args.Handled = firstLayer != null;
@@ -108,15 +113,16 @@ internal class VectorTextToolExecutor : UpdateableChangeExecutor, ITextOverlayEv
                 args.Handled = true;
                 document.TextOverlayHandler.Hide();
             }
+
             return;
         }
 
         document.Operations.SetSelectedMember(layerHandler.Id);
         document.Operations.InvokeCustomAction(
             () =>
-        {
-            document.TextOverlayHandler.SetCursorPosition(args.PositionOnCanvas);
-        }, false);
+            {
+                document.TextOverlayHandler.SetCursorPosition(args.PositionOnCanvas);
+            }, false);
     }
 
     public void OnQuickToolSwitch()
@@ -192,6 +198,35 @@ internal class VectorTextToolExecutor : UpdateableChangeExecutor, ITextOverlayEv
         toolbar.FillColor = color.ToColor();
     }
 
+    private void TryPutOnPath(VecD pos)
+    {
+        var topMostWithinClick = QueryLayers<IVectorLayerHandler>(pos);
+        var firstValidLayer = topMostWithinClick.FirstOrDefault(x =>
+            x.GetShapeData(document.AnimationHandler.ActiveFrameTime) is not null and not TextVectorData);
+
+        if (firstValidLayer is null)
+        {
+            return;
+        }
+
+        var shape = firstValidLayer.GetShapeData(document.AnimationHandler.ActiveFrameTime);
+
+        ShapeVectorData newShape = (ShapeVectorData)(shape as ShapeVectorData).Clone();
+
+        newShape.Fill = false;
+        newShape.StrokeWidth = 0;
+
+        onPath = newShape.ToPath();
+
+        var constructedText = ConstructTextData(lastText);
+        internals.ActionAccumulator.AddFinishedActions(
+            new SetShapeGeometry_Action(selectedMember.Id, constructedText),
+            new EndSetShapeGeometry_Action(),
+            new SetLowDpiRendering_Action(selectedMember.Id, toolbar.ForceLowDpiRendering),
+            new SetShapeGeometry_Action(firstValidLayer.Id, newShape),
+            new EndSetShapeGeometry_Action());
+    }
+
     private TextVectorData ConstructTextData(string text)
     {
         if (cachedFont == null || cachedFont.Family.Name != toolbar.FontFamily.Name)
@@ -220,6 +255,7 @@ internal class VectorTextToolExecutor : UpdateableChangeExecutor, ITextOverlayEv
             Font = cachedFont,
             Spacing = toolbar.Spacing,
             AntiAlias = toolbar.AntiAliasing,
+            Path = onPath,
             // TODO: MaxWidth = toolbar.MaxWidth
             // TODO: Path
         };

+ 2 - 0
src/PixiEditor/Models/Files/SvgFileType.cs

@@ -26,6 +26,8 @@ internal class SvgFileType : IoFileType
         job?.Report(0.5, string.Empty); 
         string xml = svgDocument.ToXml();
 
+        xml = $"<!-- Created with PixiEditor (https://pixieditor.net) -->{Environment.NewLine}" + xml;
+
         job?.Report(0.75, string.Empty);
         await using FileStream fileStream = new(pathWithExtension, FileMode.Create);
         await using StreamWriter writer = new(fileStream);

+ 116 - 41
src/PixiEditor/Models/IO/CustomDocumentFormats/SvgDocumentBuilder.cs

@@ -1,5 +1,8 @@
 using System.Diagnostics.CodeAnalysis;
+using Drawie.Backend.Core.ColorsImpl;
+using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
+using Drawie.Backend.Core.Text;
 using Drawie.Backend.Core.Vector;
 using Drawie.Numerics;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Interfaces;
@@ -7,10 +10,12 @@ using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes;
 using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes.Data;
 using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Helpers;
+using PixiEditor.Models.Dialogs;
 using PixiEditor.Parser.Graph;
 using PixiEditor.SVG;
 using PixiEditor.SVG.Elements;
 using PixiEditor.SVG.Enums;
+using PixiEditor.SVG.Exceptions;
 using PixiEditor.ViewModels.Tools.Tools;
 
 namespace PixiEditor.Models.IO.CustomDocumentFormats;
@@ -24,9 +29,21 @@ internal class SvgDocumentBuilder : IDocumentBuilder
         string xml = File.ReadAllText(path);
         SvgDocument document = SvgDocument.Parse(xml);
 
+        if(document == null)
+        {
+            throw new SvgParsingException("Failed to parse SVG document");
+        }
+
         StyleContext styleContext = new(document);
 
-        builder.WithSize((int)document.ViewBox.Unit.Value.Value.Width, (int)document.ViewBox.Unit.Value.Value.Height)
+        VecI size = new((int)document.ViewBox.Unit.Value.Value.Width, (int)document.ViewBox.Unit.Value.Value.Height);
+        if (size.ShortestAxis < 1)
+        {
+            size = new VecI(1024, 1024);
+        }
+
+        builder.WithSize(size)
+            .WithSrgbColorBlending(true) // apparently svgs blend colors in SRGB space
             .WithGraph(graph =>
             {
                 int? lastId = null;
@@ -66,7 +83,7 @@ internal class SvgDocumentBuilder : IDocumentBuilder
         }
         else if (element is SvgPath pathElement)
         {
-            shapeData = AddPath(pathElement);
+            shapeData = AddPath(pathElement, styleContext);
             name = VectorPathToolViewModel.NewLayerKey;
         }
         else if (element is SvgRectangle rect)
@@ -74,11 +91,19 @@ internal class SvgDocumentBuilder : IDocumentBuilder
             shapeData = AddRect(rect);
             name = VectorRectangleToolViewModel.NewLayerKey;
         }
+        else if (element is SvgText text)
+        {
+            shapeData = AddText(text);
+            name = TextToolViewModel.NewLayerKey;
+        }
+
+        name = element.Id.Unit?.Value ?? name;
 
         AddCommonShapeData(shapeData, styleContext);
 
         NodeGraphBuilder.NodeBuilder nBuilder = graph.WithNodeOfType<VectorLayerNode>(out int id)
             .WithName(name)
+            .WithInputValues(new Dictionary<string, object>() { { StructureNode.OpacityPropertyName, (float)(styleContext.Opacity.Unit?.Value ?? 1f) } })
             .WithAdditionalData(new Dictionary<string, object>() { { "ShapeData", shapeData } });
 
         if (lastId != null)
@@ -95,15 +120,15 @@ internal class SvgDocumentBuilder : IDocumentBuilder
         return lastId;
     }
 
-    private int? AddGroup(SvgGroup group, NodeGraphBuilder graph, StyleContext style, int? lastId, string connectionName = "Background")
+    private int? AddGroup(SvgGroup group, NodeGraphBuilder graph, StyleContext style, int? lastId,
+        string connectionName = "Background")
     {
         int? childId = null;
         var connectTo = "Background";
-        
         foreach (var child in group.Children)
         {
             StyleContext childStyle = style.WithElement(child);
-            
+
             if (child is SvgPrimitive primitive)
             {
                 childId = AddPrimitive(child, childStyle, graph, childId, connectTo);
@@ -117,18 +142,30 @@ internal class SvgDocumentBuilder : IDocumentBuilder
         NodeGraphBuilder.NodeBuilder nBuilder = graph.WithNodeOfType<FolderNode>(out int id)
             .WithName(group.Id.Unit != null ? group.Id.Unit.Value.Value : new LocalizedString("NEW_FOLDER"));
 
+        int connectionsCount = 0;
+        if (lastId != null) connectionsCount++;
+        if (childId != null) connectionsCount++;
+
+        PropertyConnection[] connections = new PropertyConnection[connectionsCount];
         if (lastId != null)
         {
-            nBuilder.WithConnections([
-                new PropertyConnection()
-                {
-                    InputPropertyName = connectionName, OutputPropertyName = "Output", OutputNodeId = lastId.Value
-                },
-                new PropertyConnection()
-                {
-                    InputPropertyName = "Content", OutputPropertyName = "Output", OutputNodeId = childId.Value
-                }
-            ]);
+            connections[0] = new PropertyConnection()
+            {
+                InputPropertyName = connectionName, OutputPropertyName = "Output", OutputNodeId = lastId.Value
+            };
+        }
+
+        if (childId != null)
+        {
+            connections[^1] = new PropertyConnection()
+            {
+                InputPropertyName = "Content", OutputPropertyName = "Output", OutputNodeId = childId.Value
+            };
+        }
+
+        if (connections.Length > 0)
+        {
+            nBuilder.WithConnections(connections);
         }
 
         lastId = id;
@@ -141,15 +178,15 @@ internal class SvgDocumentBuilder : IDocumentBuilder
         if (element is SvgCircle circle)
         {
             return new EllipseVectorData(
-                new VecD(circle.Cx.Unit.Value.Value, circle.Cy.Unit.Value.Value),
-                new VecD(circle.R.Unit.Value.Value, circle.R.Unit.Value.Value));
+                new VecD(circle.Cx.Unit?.PixelsValue ?? 0, circle.Cy.Unit?.PixelsValue ?? 0),
+                new VecD(circle.R.Unit?.PixelsValue ?? 0, circle.R.Unit?.PixelsValue ?? 0));
         }
 
         if (element is SvgEllipse ellipse)
         {
             return new EllipseVectorData(
-                new VecD(ellipse.Cx.Unit.Value.Value, ellipse.Cy.Unit.Value.Value),
-                new VecD(ellipse.Rx.Unit.Value.Value, ellipse.Ry.Unit.Value.Value));
+                new VecD(ellipse.Cx.Unit?.PixelsValue ?? 0, ellipse.Cy.Unit?.PixelsValue ?? 0),
+                new VecD(ellipse.Rx.Unit?.PixelsValue ?? 0, ellipse.Ry.Unit?.PixelsValue ?? 0));
         }
 
         return null;
@@ -158,13 +195,17 @@ internal class SvgDocumentBuilder : IDocumentBuilder
     private LineVectorData AddLine(SvgLine element)
     {
         return new LineVectorData(
-            new VecD(element.X1.Unit.Value.Value, element.Y1.Unit.Value.Value),
-            new VecD(element.X2.Unit.Value.Value, element.Y2.Unit.Value.Value));
+            new VecD(element.X1.Unit?.PixelsValue ?? 0, element.Y1.Unit?.PixelsValue ?? 0),
+            new VecD(element.X2.Unit?.PixelsValue ?? 0, element.Y2.Unit?.PixelsValue ?? 0));
     }
 
-    private PathVectorData AddPath(SvgPath element)
+    private PathVectorData AddPath(SvgPath element, StyleContext styleContext)
     {
-        VectorPath path = VectorPath.FromSvgPath(element.PathData.Unit.Value.Value);
+        VectorPath? path = null;
+        if (element.PathData.Unit != null)
+        {
+            path = VectorPath.FromSvgPath(element.PathData.Unit.Value.Value);
+        }
 
         if (element.FillRule.Unit != null)
         {
@@ -175,23 +216,53 @@ internal class SvgDocumentBuilder : IDocumentBuilder
                 _ => PathFillType.Winding
             };
         }
-        
-        StrokeCap strokeLineCap = StrokeCap.Round;
-        StrokeJoin strokeLineJoin = StrokeJoin.Round;
-        
-        if(element.StrokeLineCap.Unit != null)
+
+        StrokeCap strokeLineCap = StrokeCap.Butt;
+        StrokeJoin strokeLineJoin = StrokeJoin.Miter;
+
+        if (styleContext.StrokeLineCap.Unit != null)
+        {
+            strokeLineCap = (StrokeCap)(styleContext.StrokeLineCap.Unit?.Value ?? SvgStrokeLineCap.Butt);
+        }
+
+        if (styleContext.StrokeLineJoin.Unit != null)
         {
-            strokeLineCap = (StrokeCap)element.StrokeLineCap.Unit.Value.Value;
-            strokeLineJoin = (StrokeJoin)element.StrokeLineJoin.Unit.Value.Value;
+            strokeLineJoin = (StrokeJoin)(styleContext.StrokeLineJoin.Unit?.Value ?? SvgStrokeLineJoin.Miter);
         }
 
-        return new PathVectorData(path) { StrokeLineCap = strokeLineCap, StrokeLineJoin = strokeLineJoin };
+        return new PathVectorData(path) { StrokeLineCap = strokeLineCap, StrokeLineJoin = strokeLineJoin, };
     }
 
     private RectangleVectorData AddRect(SvgRectangle element)
     {
-        return new RectangleVectorData(element.X.Unit.Value.Value, element.Y.Unit.Value.Value,
-            element.Width.Unit.Value.Value, element.Height.Unit.Value.Value);
+        return new RectangleVectorData(
+            element.X.Unit?.PixelsValue ?? 0, element.Y.Unit?.PixelsValue ?? 0,
+            element.Width.Unit?.PixelsValue ?? 0, element.Height.Unit?.PixelsValue ?? 0);
+    }
+
+    private TextVectorData AddText(SvgText element)
+    {
+        Font font = element.FontFamily.Unit.HasValue ? Font.FromFamilyName(element.FontFamily.Unit.Value.Value) : Font.CreateDefault();
+        FontFamilyName? missingFont = null;
+        if(font == null)
+        {
+            font = Font.CreateDefault();
+            missingFont = new FontFamilyName(element.FontFamily.Unit.Value.Value);
+        }
+
+        font.Size = element.FontSize.Unit?.PixelsValue ?? 12;
+        font.Bold = element.FontWeight.Unit?.Value == SvgFontWeight.Bold;
+        font.Italic = element.FontStyle.Unit?.Value == SvgFontStyle.Italic;
+
+        return new TextVectorData(element.Text.Unit.Value.Value)
+        {
+            Position = new VecD(
+                element.X.Unit?.PixelsValue ?? 0,
+                element.Y.Unit?.PixelsValue ?? 0),
+            Font = font,
+            MissingFontFamily = missingFont,
+            MissingFontText = "MISSING_FONT"
+        };
     }
 
     private void AddCommonShapeData(ShapeVectorData? shapeData, StyleContext styleContext)
@@ -202,26 +273,24 @@ internal class SvgDocumentBuilder : IDocumentBuilder
         }
 
         bool hasFill = styleContext.Fill.Unit is { Color.A: > 0 };
-        bool hasStroke = styleContext.Stroke.Unit is { Color.A: > 0 };
+        bool hasStroke = styleContext.Stroke.Unit is { Color.A: > 0 } || styleContext.StrokeWidth.Unit is { PixelsValue: > 0 };
         bool hasTransform = styleContext.Transform.Unit is { MatrixValue.IsIdentity: false };
 
         shapeData.Fill = hasFill;
         if (hasFill)
         {
             var target = styleContext.Fill.Unit;
-            shapeData.FillColor = target.Value.Color;
+            float opacity = (float)(styleContext.FillOpacity.Unit?.Value ?? 1);
+            shapeData.FillColor = target.Value.Color.WithAlpha((byte)(Math.Clamp(opacity, 0, 1) * 255));
         }
 
         if (hasStroke)
         {
             var targetColor = styleContext.Stroke.Unit;
             var targetWidth = styleContext.StrokeWidth.Unit;
-            
-            shapeData.StrokeColor = targetColor.Value.Color;
-            if (targetWidth != null)
-            {
-                shapeData.StrokeWidth = (float)targetWidth.Value.Value;
-            }
+
+            shapeData.StrokeColor = targetColor?.Color ?? Colors.Black;
+            shapeData.StrokeWidth = (float)(targetWidth?.PixelsValue ?? 1);
         }
 
         if (hasTransform)
@@ -229,5 +298,11 @@ internal class SvgDocumentBuilder : IDocumentBuilder
             var target = styleContext.Transform.Unit;
             shapeData.TransformationMatrix = target.Value.MatrixValue;
         }
+
+        if (styleContext.ViewboxOrigin != VecD.Zero)
+        {
+            shapeData.TransformationMatrix = shapeData.TransformationMatrix.PostConcat(
+                Matrix3X3.CreateTranslation((float)styleContext.ViewboxOrigin.X, (float)styleContext.ViewboxOrigin.Y));
+        }
     }
 }

+ 0 - 22
src/PixiEditor/Styles/NodeIcons.axaml

@@ -1,22 +0,0 @@
-<Styles xmlns="https://github.com/avaloniaui"
-        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
-    <Design.PreviewWith>
-        <Border Padding="20">
-            <!-- Add Controls for Previewer Here -->
-        </Border>
-    </Design.PreviewWith>
-
-    <Styles.Resources>
-        <ResourceDictionary>
-            <FontFamily x:Key="NodeIcons">avares://PixiEditor/Fonts/nodeicons.ttf#nodeicons</FontFamily>
-        </ResourceDictionary>
-    </Styles.Resources>
-    
-    <Style Selector="TextBlock.node-icon">
-        <Setter Property="FontFamily" Value="{DynamicResource NodeIcons}" />
-    </Style>
-
-    <Style Selector="Run.node-icon">
-        <Setter Property="FontFamily" Value="{DynamicResource NodeIcons}" />
-    </Style>
-</Styles>

+ 1 - 1
src/PixiEditor/Styles/Templates/NodePicker.axaml

@@ -55,7 +55,7 @@
                                                             HorizontalContentAlignment="Left"
                                                             IsVisible="{Binding !Hidden}">
                                                             <TextBlock Margin="10 0 0 0">
-                                                                <Run Classes="node-icon"
+                                                                <Run Classes="pixi-icon"
                                                                      BaselineAlignment="Center"
                                                                      Text="{Binding Icon}" />
                                                                 <Run ui:Translator.Key="{Binding FinalPickerName}" />

+ 85 - 40
src/PixiEditor/ViewModels/Document/DocumentManagerViewModel.cs

@@ -21,6 +21,7 @@ internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>, IDocument
     public event EventHandler<DocumentChangedEventArgs>? ActiveDocumentChanged;
 
     private DocumentViewModel? activeDocument;
+
     public DocumentViewModel? ActiveDocument
     {
         get => activeDocument;
@@ -33,11 +34,12 @@ internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>, IDocument
             activeDocument = value;
             OnPropertyChanged(nameof(ActiveDocument));
             ActiveDocumentChanged?.Invoke(this, new(value, prevDoc));
-            
+
             if (ViewModelMain.Current.ToolsSubViewModel.ActiveTool == null)
             {
                 var firstTool =
-                    ViewModelMain.Current.ToolsSubViewModel.ActiveToolSet.Tools.FirstOrDefault(x => x.CanBeUsedOnActiveLayer);
+                    ViewModelMain.Current.ToolsSubViewModel.ActiveToolSet.Tools.FirstOrDefault(x =>
+                        x.CanBeUsedOnActiveLayer);
                 if (firstTool != null)
                 {
                     ViewModelMain.Current.ToolsSubViewModel.SetActiveTool(firstTool.GetType(), false);
@@ -67,57 +69,76 @@ internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>, IDocument
     [Evaluator.CanExecute("PixiEditor.HasDocument", nameof(ActiveDocument))]
     public bool DocumentNotNull() => ActiveDocument != null;
 
-    [Command.Basic("PixiEditor.Document.ClipCanvas", "CLIP_CANVAS", "CLIP_CANVAS", CanExecute = "PixiEditor.HasDocument",
+    [Command.Basic("PixiEditor.Document.ClipCanvas", "CLIP_CANVAS", "CLIP_CANVAS",
+        CanExecute = "PixiEditor.HasDocument",
         Icon = PixiPerfectIcons.Crop, MenuItemPath = "IMAGE/CLIP_CANVAS", MenuItemOrder = 2, AnalyticsTrack = true)]
     public void ClipCanvas() => ActiveDocument?.Operations.ClipCanvas();
 
-    [Command.Basic("PixiEditor.Document.FlipImageHorizontal", FlipType.Horizontal, "FLIP_IMG_HORIZONTALLY", "FLIP_IMG_HORIZONTALLY", CanExecute = "PixiEditor.HasDocument",
-        MenuItemPath = "IMAGE/FLIP/FLIP_IMG_HORIZONTALLY", MenuItemOrder = 14, Icon = PixiPerfectIcons.XFlip, AnalyticsTrack = true)]
-    [Command.Basic("PixiEditor.Document.FlipImageVertical", FlipType.Vertical, "FLIP_IMG_VERTICALLY", "FLIP_IMG_VERTICALLY", CanExecute = "PixiEditor.HasDocument",
-        MenuItemPath = "IMAGE/FLIP/FLIP_IMG_VERTICALLY", MenuItemOrder = 15, Icon = PixiPerfectIcons.YFlip, AnalyticsTrack = true)]
-    public void FlipImage(FlipType type) => ActiveDocument?.Operations.FlipImage(type, activeDocument.AnimationDataViewModel.ActiveFrameBindable);
+    [Command.Basic("PixiEditor.Document.FlipImageHorizontal", FlipType.Horizontal, "FLIP_IMG_HORIZONTALLY",
+        "FLIP_IMG_HORIZONTALLY", CanExecute = "PixiEditor.HasDocument",
+        MenuItemPath = "IMAGE/FLIP/FLIP_IMG_HORIZONTALLY", MenuItemOrder = 14, Icon = PixiPerfectIcons.XFlip,
+        AnalyticsTrack = true)]
+    [Command.Basic("PixiEditor.Document.FlipImageVertical", FlipType.Vertical, "FLIP_IMG_VERTICALLY",
+        "FLIP_IMG_VERTICALLY", CanExecute = "PixiEditor.HasDocument",
+        MenuItemPath = "IMAGE/FLIP/FLIP_IMG_VERTICALLY", MenuItemOrder = 15, Icon = PixiPerfectIcons.YFlip,
+        AnalyticsTrack = true)]
+    public void FlipImage(FlipType type) =>
+        ActiveDocument?.Operations.FlipImage(type, activeDocument.AnimationDataViewModel.ActiveFrameBindable);
 
-    [Command.Basic("PixiEditor.Document.FlipLayersHorizontal", FlipType.Horizontal, "FLIP_LAYERS_HORIZONTALLY", "FLIP_LAYERS_HORIZONTALLY", CanExecute = "PixiEditor.HasDocument",
-        MenuItemPath = "IMAGE/FLIP/FLIP_LAYERS_HORIZONTALLY", MenuItemOrder = 16, Icon = PixiPerfectIcons.XSelectedFlip, AnalyticsTrack = true)]
-    [Command.Basic("PixiEditor.Document.FlipLayersVertical", FlipType.Vertical, "FLIP_LAYERS_VERTICALLY", "FLIP_LAYERS_VERTICALLY", CanExecute = "PixiEditor.HasDocument",
-        MenuItemPath = "IMAGE/FLIP/FLIP_LAYERS_VERTICALLY", MenuItemOrder = 17, Icon = PixiPerfectIcons.YSelectedFlip, AnalyticsTrack = true)]
+    [Command.Basic("PixiEditor.Document.FlipLayersHorizontal", FlipType.Horizontal, "FLIP_LAYERS_HORIZONTALLY",
+        "FLIP_LAYERS_HORIZONTALLY", CanExecute = "PixiEditor.HasDocument",
+        MenuItemPath = "IMAGE/FLIP/FLIP_LAYERS_HORIZONTALLY", MenuItemOrder = 16, Icon = PixiPerfectIcons.XSelectedFlip,
+        AnalyticsTrack = true)]
+    [Command.Basic("PixiEditor.Document.FlipLayersVertical", FlipType.Vertical, "FLIP_LAYERS_VERTICALLY",
+        "FLIP_LAYERS_VERTICALLY", CanExecute = "PixiEditor.HasDocument",
+        MenuItemPath = "IMAGE/FLIP/FLIP_LAYERS_VERTICALLY", MenuItemOrder = 17, Icon = PixiPerfectIcons.YSelectedFlip,
+        AnalyticsTrack = true)]
     public void FlipLayers(FlipType type)
     {
         if (ActiveDocument?.SelectedStructureMember == null)
             return;
 
-        ActiveDocument?.Operations.FlipImage(type, ActiveDocument.GetSelectedMembers(), activeDocument.AnimationDataViewModel.ActiveFrameBindable);
+        ActiveDocument?.Operations.FlipImage(type, ActiveDocument.GetSelectedMembers(),
+            activeDocument.AnimationDataViewModel.ActiveFrameBindable);
     }
 
     [Command.Basic("PixiEditor.Document.Rotate90Deg", "ROT_IMG_90",
         "ROT_IMG_90", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D90,
-        MenuItemPath = "IMAGE/ROTATION/ROT_IMG_90_D", MenuItemOrder = 8, Icon = PixiPerfectIcons.RotateImage90, AnalyticsTrack = true)]
+        MenuItemPath = "IMAGE/ROTATION/ROT_IMG_90_D", MenuItemOrder = 8, Icon = PixiPerfectIcons.RotateImage90,
+        AnalyticsTrack = true)]
     [Command.Basic("PixiEditor.Document.Rotate180Deg", "ROT_IMG_180",
         "ROT_IMG_180", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D180,
-        MenuItemPath = "IMAGE/ROTATION/ROT_IMG_180_D", MenuItemOrder = 9, Icon = PixiPerfectIcons.RotateImage180, AnalyticsTrack = true)]
+        MenuItemPath = "IMAGE/ROTATION/ROT_IMG_180_D", MenuItemOrder = 9, Icon = PixiPerfectIcons.RotateImage180,
+        AnalyticsTrack = true)]
     [Command.Basic("PixiEditor.Document.Rotate270Deg", "ROT_IMG_-90",
         "ROT_IMG_-90", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D270,
-        MenuItemPath = "IMAGE/ROTATION/ROT_IMG_-90_D", MenuItemOrder = 10, Icon = PixiPerfectIcons.RotateImageMinus90, AnalyticsTrack = true)]
+        MenuItemPath = "IMAGE/ROTATION/ROT_IMG_-90_D", MenuItemOrder = 10, Icon = PixiPerfectIcons.RotateImageMinus90,
+        AnalyticsTrack = true)]
     public void RotateImage(RotationAngle angle) => ActiveDocument?.Operations.RotateImage(angle);
 
     [Command.Basic("PixiEditor.Document.Rotate90DegLayers", "ROT_LAYERS_90",
         "ROT_LAYERS_90", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D90,
-        MenuItemPath = "IMAGE/ROTATION/ROT_LAYERS_90_D", MenuItemOrder = 11, Icon = PixiPerfectIcons.RotateFile90, AnalyticsTrack = true)]
+        MenuItemPath = "IMAGE/ROTATION/ROT_LAYERS_90_D", MenuItemOrder = 11, Icon = PixiPerfectIcons.RotateFile90,
+        AnalyticsTrack = true)]
     [Command.Basic("PixiEditor.Document.Rotate180DegLayers", "ROT_LAYERS_180",
         "ROT_LAYERS_180", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D180,
-        MenuItemPath = "IMAGE/ROTATION/ROT_LAYERS_180_D", MenuItemOrder = 12, Icon = PixiPerfectIcons.RotateFile180, AnalyticsTrack = true)]
+        MenuItemPath = "IMAGE/ROTATION/ROT_LAYERS_180_D", MenuItemOrder = 12, Icon = PixiPerfectIcons.RotateFile180,
+        AnalyticsTrack = true)]
     [Command.Basic("PixiEditor.Document.Rotate270DegLayers", "ROT_LAYERS_-90",
         "ROT_LAYERS_-90", CanExecute = "PixiEditor.HasDocument", Parameter = RotationAngle.D270,
-        MenuItemPath = "IMAGE/ROTATION/ROT_LAYERS_-90_D", MenuItemOrder = 13, Icon = PixiPerfectIcons.RotateFileMinus90, AnalyticsTrack = true)]
+        MenuItemPath = "IMAGE/ROTATION/ROT_LAYERS_-90_D", MenuItemOrder = 13, Icon = PixiPerfectIcons.RotateFileMinus90,
+        AnalyticsTrack = true)]
     public void RotateLayers(RotationAngle angle)
     {
         if (ActiveDocument?.SelectedStructureMember == null)
             return;
-        
-        ActiveDocument?.Operations.RotateImage(angle, ActiveDocument.GetSelectedMembers(), activeDocument.AnimationDataViewModel.ActiveFrameBindable);
+
+        ActiveDocument?.Operations.RotateImage(angle, ActiveDocument.GetSelectedMembers(),
+            activeDocument.AnimationDataViewModel.ActiveFrameBindable);
     }
 
-    [Command.Basic("PixiEditor.Document.ToggleVerticalSymmetryAxis", "TOGGLE_VERT_SYMMETRY_AXIS", "TOGGLE_VERT_SYMMETRY_AXIS", CanExecute = "PixiEditor.HasDocument", 
+    [Command.Basic("PixiEditor.Document.ToggleVerticalSymmetryAxis", "TOGGLE_VERT_SYMMETRY_AXIS",
+        "TOGGLE_VERT_SYMMETRY_AXIS", CanExecute = "PixiEditor.HasDocument",
         Icon = PixiPerfectIcons.YSymmetry, AnalyticsTrack = true)]
     public void ToggleVerticalSymmetryAxis()
     {
@@ -126,7 +147,8 @@ internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>, IDocument
         ActiveDocument.VerticalSymmetryAxisEnabledBindable ^= true;
     }
 
-    [Command.Basic("PixiEditor.Document.ToggleHorizontalSymmetryAxis", "TOGGLE_HOR_SYMMETRY_AXIS", "TOGGLE_HOR_SYMMETRY_AXIS", CanExecute = "PixiEditor.HasDocument", 
+    [Command.Basic("PixiEditor.Document.ToggleHorizontalSymmetryAxis", "TOGGLE_HOR_SYMMETRY_AXIS",
+        "TOGGLE_HOR_SYMMETRY_AXIS", CanExecute = "PixiEditor.HasDocument",
         Icon = PixiPerfectIcons.XSymmetry, AnalyticsTrack = true)]
     public void ToggleHorizontalSymmetryAxis()
     {
@@ -143,7 +165,8 @@ internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>, IDocument
         ActiveDocument.EventInlet.OnSymmetryDragged(info);
     }
 
-    [Command.Internal("PixiEditor.Document.StartDragSymmetry", CanExecute = "PixiEditor.HasDocument", AnalyticsTrack = true)]
+    [Command.Internal("PixiEditor.Document.StartDragSymmetry", CanExecute = "PixiEditor.HasDocument",
+        AnalyticsTrack = true)]
     public void StartDragSymmetry(SymmetryAxisDirection dir)
     {
         if (ActiveDocument is null)
@@ -160,21 +183,25 @@ internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>, IDocument
         ActiveDocument.EventInlet.OnSymmetryDragEnded(dir);
     }
 
-    [Command.Basic("PixiEditor.Document.DeletePixels", "DELETE_PIXELS", "DELETE_PIXELS_DESCRIPTIVE", 
+    [Command.Basic("PixiEditor.Document.DeletePixels", "DELETE_PIXELS", "DELETE_PIXELS_DESCRIPTIVE",
         CanExecute = "PixiEditor.Selection.IsNotEmpty", Key = Key.Delete,
         ShortcutContexts = [typeof(ViewportWindowViewModel)],
         Icon = PixiPerfectIcons.Eraser,
         MenuItemPath = "EDIT/DELETE_SELECTED_PIXELS", MenuItemOrder = 6, AnalyticsTrack = true)]
     public void DeletePixels()
     {
-        Owner.DocumentManagerSubViewModel.ActiveDocument?.Operations.DeleteSelectedPixels(activeDocument.AnimationDataViewModel.ActiveFrameBindable);
+        Owner.DocumentManagerSubViewModel.ActiveDocument?.Operations.DeleteSelectedPixels(activeDocument
+            .AnimationDataViewModel.ActiveFrameBindable);
     }
 
 
-    [Command.Basic("PixiEditor.Document.ResizeDocument", false, "RESIZE_DOCUMENT", "RESIZE_DOCUMENT", CanExecute = "PixiEditor.HasDocument", Key = Key.I, Modifiers = KeyModifiers.Control | KeyModifiers.Shift,
+    [Command.Basic("PixiEditor.Document.ResizeDocument", false, "RESIZE_DOCUMENT", "RESIZE_DOCUMENT",
+        CanExecute = "PixiEditor.HasDocument", Key = Key.I, Modifiers = KeyModifiers.Control | KeyModifiers.Shift,
         Icon = PixiPerfectIcons.Resize, MenuItemPath = "IMAGE/RESIZE_IMAGE", MenuItemOrder = 0, AnalyticsTrack = true)]
-    [Command.Basic("PixiEditor.Document.ResizeCanvas", true, "RESIZE_CANVAS", "RESIZE_CANVAS", CanExecute = "PixiEditor.HasDocument", Key = Key.C, Modifiers = KeyModifiers.Control | KeyModifiers.Shift,
-        Icon = PixiPerfectIcons.CanvasResize, MenuItemPath = "IMAGE/RESIZE_CANVAS", MenuItemOrder = 1, AnalyticsTrack = true)]
+    [Command.Basic("PixiEditor.Document.ResizeCanvas", true, "RESIZE_CANVAS", "RESIZE_CANVAS",
+        CanExecute = "PixiEditor.HasDocument", Key = Key.C, Modifiers = KeyModifiers.Control | KeyModifiers.Shift,
+        Icon = PixiPerfectIcons.CanvasResize, MenuItemPath = "IMAGE/RESIZE_CANVAS", MenuItemOrder = 1,
+        AnalyticsTrack = true)]
     public async Task OpenResizePopup(bool canvas)
     {
         DocumentViewModel? doc = Owner.DocumentManagerSubViewModel.ActiveDocument;
@@ -199,26 +226,44 @@ internal class DocumentManagerViewModel : SubViewModel<ViewModelMain>, IDocument
         }
     }
 
-    [Command.Basic("PixiEditor.Document.CenterContent", "CENTER_CONTENT", "CENTER_CONTENT", CanExecute = "PixiEditor.HasDocument",
-        Icon = PixiPerfectIcons.Center, MenuItemPath = "IMAGE/CENTER_CONTENT", MenuItemOrder = 3, AnalyticsTrack = true)]
+    [Command.Basic("PixiEditor.Document.CenterContent", "CENTER_CONTENT", "CENTER_CONTENT",
+        CanExecute = "PixiEditor.HasDocument",
+        Icon = PixiPerfectIcons.Center, MenuItemPath = "IMAGE/CENTER_CONTENT", MenuItemOrder = 3,
+        AnalyticsTrack = true)]
     public void CenterContent()
     {
-        if(ActiveDocument?.SelectedStructureMember == null)
+        if (ActiveDocument?.SelectedStructureMember == null)
             return;
-        
-        ActiveDocument.Operations.CenterContent(ActiveDocument.GetSelectedMembers(), activeDocument.AnimationDataViewModel.ActiveFrameBindable);
+
+        ActiveDocument.Operations.CenterContent(ActiveDocument.GetSelectedMembers(),
+            activeDocument.AnimationDataViewModel.ActiveFrameBindable);
     }
-    
-    [Command.Basic("PixiEditor.Document.UseLinearSrgbProcessing", "USE_LINEAR_SRGB_PROCESSING", "USE_LINEAR_SRGB_PROCESSING_DESC", CanExecute = "PixiEditor.DocumentUsesLegacyBlending", 
+
+    [Command.Basic("PixiEditor.Document.UseLinearSrgbProcessing", "USE_LINEAR_SRGB_PROCESSING",
+        "USE_LINEAR_SRGB_PROCESSING_DESC", CanExecute = "PixiEditor.DocumentUsesSrgbBlending",
         AnalyticsTrack = true)]
     public void UseLinearSrgbProcessing()
     {
         if (ActiveDocument is null)
             return;
-        
+
         ActiveDocument.Operations.UseLinearSrgbProcessing();
     }
-    
-    [Evaluator.CanExecute("PixiEditor.DocumentUsesLegacyBlending", nameof(ActiveDocument))]
-    public bool DocumentUsesLegacyBlending() => ActiveDocument?.UsesLegacyBlending ?? false;
+
+    [Command.Basic("PixiEditor.Document.UseSrgbProcessing", "USE_SRGB_PROCESSING",
+        "USE_SRGB_PROCESSING_DESC", CanExecute = "PixiEditor.DocumentUsesLinearBlending",
+        AnalyticsTrack = true)]
+    public void UseSrgbProcessing()
+    {
+        if (ActiveDocument is null)
+            return;
+
+        ActiveDocument.Operations.UseSrgbProcessing();
+    }
+
+    [Evaluator.CanExecute("PixiEditor.DocumentUsesSrgbBlending", nameof(ActiveDocument))]
+    public bool DocumentUsesSrgbBlending() => ActiveDocument?.UsesSrgbBlending ?? false;
+
+    [Evaluator.CanExecute("PixiEditor.DocumentUsesLinearBlending", nameof(ActiveDocument))]
+    public bool DocumentUsesLinearBlending() => !ActiveDocument?.UsesSrgbBlending ?? true;
 }

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

@@ -18,6 +18,7 @@ using Drawie.Backend.Core.Numerics;
 using Drawie.Backend.Core.Surfaces;
 using Drawie.Backend.Core.Surfaces.ImageData;
 using Drawie.Backend.Core.Surfaces.PaintImpl;
+using Drawie.Backend.Core.Text;
 using Drawie.Backend.Core.Vector;
 using PixiEditor.Extensions.CommonApi.Palettes;
 using PixiEditor.Helpers;
@@ -75,7 +76,7 @@ internal partial class DocumentViewModel
         {
             SerializerName = "PixiEditor",
             SerializerVersion = VersionHelpers.GetCurrentAssemblyVersion().ToString(),
-            LegacyColorBlending = doc.ProcessingColorSpace.IsSrgb,
+            SrgbColorBlending = doc.ProcessingColorSpace.IsSrgb,
             Width = Width,
             Height = Height,
             Swatches = ToCollection(Swatches),
@@ -160,6 +161,10 @@ internal partial class DocumentViewModel
         {
             elementToAdd = AddVectorPath(shapeData);
         }
+        else if (vectorNode.ShapeData is IReadOnlyTextData textData)
+        {
+            elementToAdd = AddText(textData);
+        }
 
         IReadOnlyShapeVectorData data = vectorNode.ShapeData;
 
@@ -178,6 +183,13 @@ internal partial class DocumentViewModel
 
             primitive.StrokeWidth.Unit = SvgNumericUnit.FromUserUnits(data.StrokeWidth);
         }
+        else if (elementToAdd is SvgGroup group)
+        {
+            Matrix3X3 transform = data.TransformationMatrix;
+
+            transform = transform.PostConcat(Matrix3X3.CreateScale((float)resizeFactor.X, (float)resizeFactor.Y));
+            group.Transform.Unit = new SvgTransformUnit?(new SvgTransformUnit(transform));
+        }
 
         if (elementToAdd != null)
         {
@@ -276,6 +288,46 @@ internal partial class DocumentViewModel
         elementContainer.Children.Add(image);
     }
 
+    private SvgElement AddText(IReadOnlyTextData textData)
+    {
+        RichText rt = new RichText(textData.Text);
+        rt.Spacing = textData.Spacing;
+        rt.MaxWidth = textData.MaxWidth;
+
+        using Font font = textData.ConstructFont();
+
+        if (rt.Lines.Length <= 1)
+        {
+            return BuildTextElement(textData, textData.Text, font);
+        }
+
+        SvgGroup group = new SvgGroup();
+        for (int i = 0; i < rt.Lines.Length; i++)
+        {
+            var offset = rt.GetLineOffset(i, font);
+
+            var text = BuildTextElement(textData, rt.Lines[i], font);
+            text.Y.Unit = SvgNumericUnit.FromUserUnits(textData.Position.Y + offset.Y);
+
+            group.Children.Add(text);
+        }
+
+        return group;
+    }
+
+    private static SvgText BuildTextElement(IReadOnlyTextData textData, string value, Font font)
+    {
+        SvgText text = new SvgText();
+        text.Text.Unit = new SvgStringUnit(value);
+        text.X.Unit = SvgNumericUnit.FromUserUnits(textData.Position.X);
+        text.Y.Unit = SvgNumericUnit.FromUserUnits(textData.Position.Y);
+        text.FontSize.Unit = SvgNumericUnit.FromUserUnits(font.Size);
+        text.FontFamily.Unit = new SvgStringUnit(font.Family.Name);
+        text.FontWeight.Unit = new SvgEnumUnit<SvgFontWeight>(font.Bold ? SvgFontWeight.Bold : SvgFontWeight.Normal);
+        text.FontStyle.Unit = new SvgEnumUnit<SvgFontStyle>(font.Italic ? SvgFontStyle.Italic : SvgFontStyle.Normal);
+
+        return text;
+    }
 
     private static SvgImage CreateImageElement(VecD resizeFactor, RectD tightBounds,
         Image toSerialize, bool useNearestNeighborForImageUpscaling)

+ 6 - 6
src/PixiEditor/ViewModels/Document/DocumentViewModel.cs

@@ -224,7 +224,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
     ILineOverlayHandler IDocument.LineToolOverlayHandler => LineToolOverlayViewModel;
     IReferenceLayerHandler IDocument.ReferenceLayerHandler => ReferenceLayerViewModel;
     IAnimationHandler IDocument.AnimationHandler => AnimationDataViewModel;
-    public bool UsesLegacyBlending { get; private set; }
+    public bool UsesSrgbBlending { get; private set; }
 
     private DocumentViewModel()
     {
@@ -318,12 +318,12 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
         var acc = viewModel.Internals.ActionAccumulator;
 
         ColorSpace targetProcessingColorSpace = ColorSpace.CreateSrgbLinear();
-        if (builderInstance.UsesLegacyColorBlending ||
-            IsFileWithOldColorBlending(serializerData, builderInstance.PixiParserVersionUsed))
+        if (builderInstance.UsesSrgbColorBlending ||
+            IsFileWithSrgbColorBlending(serializerData, builderInstance.PixiParserVersionUsed))
         {
             targetProcessingColorSpace = ColorSpace.CreateSrgb();
             viewModel.Internals.Tracker.Document.InitProcessingColorSpace(ColorSpace.CreateSrgb());
-            viewModel.UsesLegacyBlending = true;
+            viewModel.UsesSrgbBlending = true;
         }
 
         viewModel.Internals.ChangeController.SymmetryDraggedInlet(
@@ -486,7 +486,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
             }
         }
 
-        bool IsFileWithOldColorBlending((string serializerName, string serializerVersion) serializerData,
+        bool IsFileWithSrgbColorBlending((string serializerName, string serializerVersion) serializerData,
             Version? pixiParserVersionUsed)
         {
             if (pixiParserVersionUsed != null && pixiParserVersionUsed.Major < 5)
@@ -813,7 +813,7 @@ internal partial class DocumentViewModel : PixiObservableObject, IDocument
 
     public void SetProcessingColorSpace(ColorSpace infoColorSpace)
     {
-        UsesLegacyBlending = infoColorSpace.IsSrgb;
+        UsesSrgbBlending = infoColorSpace.IsSrgb;
     }
 
     public void SetSize(VecI size)

+ 1 - 1
src/PixiEditor/ViewModels/Document/Nodes/Animable/TimeNodeViewModel.cs

@@ -3,5 +3,5 @@ using PixiEditor.ViewModels.Nodes;
 
 namespace PixiEditor.ViewModels.Document.Nodes.Animable;
 
-[NodeViewModel("TIME_NODE", "ANIMATION", "\uE900")]
+[NodeViewModel("TIME_NODE", "ANIMATION", "\uE800")]
 internal class TimeNodeViewModel : NodeViewModel<TimeNode>;

+ 1 - 1
src/PixiEditor/ViewModels/Document/Nodes/ColorNodeViewModel.cs

@@ -3,7 +3,7 @@ using PixiEditor.ViewModels.Nodes;
 
 namespace PixiEditor.ViewModels.Document.Nodes;
 
-[NodeViewModel("COLOR_NODE", "COLOR", "\ue907")]
+[NodeViewModel("COLOR_NODE", "COLOR", "\uE912")]
 internal class ColorNodeViewModel : NodeViewModel<ColorNode>
 {
     

+ 1 - 1
src/PixiEditor/ViewModels/Document/Nodes/CombineSeparate/CombineChannelsNodeViewModel.cs

@@ -3,5 +3,5 @@ using PixiEditor.ViewModels.Nodes;
 
 namespace PixiEditor.ViewModels.Document.Nodes.CombineSeparate;
 
-[NodeViewModel("COMBINE_CHANNELS_NODE", "IMAGE", "\ue915")]
+[NodeViewModel("COMBINE_CHANNELS_NODE", "IMAGE", "\uE815")]
 internal class CombineChannelsNodeViewModel : NodeViewModel<CombineChannelsNode>;

+ 1 - 1
src/PixiEditor/ViewModels/Document/Nodes/CombineSeparate/CombineColorNodeViewModel.cs

@@ -7,5 +7,5 @@ using PixiEditor.ViewModels.Nodes.Properties;
 
 namespace PixiEditor.ViewModels.Document.Nodes.CombineSeparate;
 
-[NodeViewModel("COMBINE_COLOR_NODE", "COLOR", "\ue908")]
+[NodeViewModel("COMBINE_COLOR_NODE", "COLOR", "\uE808")]
 internal class CombineColorNodeViewModel() : CombineSeparateColorNodeViewModel<CombineColorNode>(true);

+ 1 - 1
src/PixiEditor/ViewModels/Document/Nodes/CombineSeparate/CombineVecDNodeViewModel.cs

@@ -3,5 +3,5 @@ using PixiEditor.ViewModels.Nodes;
 
 namespace PixiEditor.ViewModels.Document.Nodes.CombineSeparate;
 
-[NodeViewModel("COMBINE_VECD_NODE", "NUMBERS", "\ue916")]
+[NodeViewModel("COMBINE_VECD_NODE", "NUMBERS", "\uE816")]
 internal class CombineVecDNodeViewModel : NodeViewModel<CombineVecDNode>;

+ 1 - 1
src/PixiEditor/ViewModels/Document/Nodes/CombineSeparate/CombineVecINodeViewModel.cs

@@ -3,5 +3,5 @@ using PixiEditor.ViewModels.Nodes;
 
 namespace PixiEditor.ViewModels.Document.Nodes.CombineSeparate;
 
-[NodeViewModel("COMBINE_VECI_NODE", "NUMBERS", "\ue917")]
+[NodeViewModel("COMBINE_VECI_NODE", "NUMBERS", "\uE817")]
 internal class CombineVecINodeViewModel : NodeViewModel<CombineVecINode>;

+ 1 - 1
src/PixiEditor/ViewModels/Document/Nodes/CombineSeparate/SeparateChannelsNodeViewModel.cs

@@ -3,5 +3,5 @@ using PixiEditor.ViewModels.Nodes;
 
 namespace PixiEditor.ViewModels.Document.Nodes.CombineSeparate;
 
-[NodeViewModel("SEPARATE_CHANNELS_NODE", "IMAGE", "\ue910")]
+[NodeViewModel("SEPARATE_CHANNELS_NODE", "IMAGE", "\uE810")]
 internal class SeparateChannelsNodeViewModel : NodeViewModel<SeparateChannelsNode>;

+ 1 - 1
src/PixiEditor/ViewModels/Document/Nodes/CombineSeparate/SeparateColorNodeViewModel.cs

@@ -3,5 +3,5 @@ using PixiEditor.ViewModels.Nodes;
 
 namespace PixiEditor.ViewModels.Document.Nodes.CombineSeparate;
 
-[NodeViewModel("SEPARATE_COLOR_NODE", "COLOR", "\ue913")]
+[NodeViewModel("SEPARATE_COLOR_NODE", "COLOR", "\uE813")]
 internal class SeparateColorNodeViewModel() : CombineSeparateColorNodeViewModel<SeparateColorNode>(false);

+ 1 - 1
src/PixiEditor/ViewModels/Document/Nodes/CombineSeparate/SeparateVecDNodeViewModel.cs

@@ -3,5 +3,5 @@ using PixiEditor.ViewModels.Nodes;
 
 namespace PixiEditor.ViewModels.Document.Nodes.CombineSeparate;
 
-[NodeViewModel("SEPARATE_VECD_NODE", "NUMBERS", "\ue918")]
+[NodeViewModel("SEPARATE_VECD_NODE", "NUMBERS", "\uE818")]
 internal class SeparateVecDNodeViewModel : NodeViewModel<SeparateVecDNode>;

+ 1 - 1
src/PixiEditor/ViewModels/Document/Nodes/CombineSeparate/SeparateVecINodeViewModel.cs

@@ -3,5 +3,5 @@ using PixiEditor.ViewModels.Nodes;
 
 namespace PixiEditor.ViewModels.Document.Nodes.CombineSeparate;
 
-[NodeViewModel("SEPARATE_VECI_NODE", "NUMBERS", "\ue917")]
+[NodeViewModel("SEPARATE_VECI_NODE", "NUMBERS", "\uE817")]
 internal class SeparateVecINodeViewModel : NodeViewModel<SeparateVecINode>;

+ 1 - 1
src/PixiEditor/ViewModels/Document/Nodes/CreateImageNodeViewModel.cs

@@ -3,5 +3,5 @@ using PixiEditor.ViewModels.Nodes;
 
 namespace PixiEditor.ViewModels.Document.Nodes;
 
-[NodeViewModel("CREATE_IMAGE_NODE", "IMAGE", "\ue902")]
+[NodeViewModel("CREATE_IMAGE_NODE", "IMAGE", "\uE802")]
 internal class CreateImageNodeViewModel : NodeViewModel<CreateImageNode>;

+ 1 - 1
src/PixiEditor/ViewModels/Document/Nodes/CustomOutputNodeViewModel.cs

@@ -4,5 +4,5 @@ using PixiEditor.ViewModels.Nodes;
 
 namespace PixiEditor.ViewModels.Document.Nodes;
 
-[NodeViewModel("CUSTOM_OUTPUT_NODE", null, "\ue920")]
+[NodeViewModel("CUSTOM_OUTPUT_NODE", null, "\uE81A")]
 internal class CustomOutputNodeViewModel : NodeViewModel<CustomOutputNode>;

+ 1 - 1
src/PixiEditor/ViewModels/Document/Nodes/FilterNodes/ApplyFilterNodeViewModel.cs

@@ -3,5 +3,5 @@ using PixiEditor.ViewModels.Nodes;
 
 namespace PixiEditor.ViewModels.Document.Nodes.FilterNodes;
 
-[NodeViewModel("APPLY_FILTER_NODE", "FILTERS", "\ue909")]
+[NodeViewModel("APPLY_FILTER_NODE", "FILTERS", "\uE809")]
 internal class ApplyFilterNodeViewModel : NodeViewModel<ApplyFilterNode>;

+ 1 - 1
src/PixiEditor/ViewModels/Document/Nodes/FilterNodes/BlurNodeViewModel.cs

@@ -3,5 +3,5 @@ using PixiEditor.ViewModels.Nodes;
 
 namespace PixiEditor.ViewModels.Document.Nodes.FilterNodes;
 
-[NodeViewModel("BLUR_FILTER_NODE", "FILTERS", "\ue822")]
+[NodeViewModel("BLUR_FILTER_NODE", "FILTERS", "\uE822")]
 internal class BlurNodeViewModel : NodeViewModel<BlurNode>;

+ 1 - 1
src/PixiEditor/ViewModels/Document/Nodes/FilterNodes/ColorMatrixFilterNodeViewModel.cs

@@ -3,5 +3,5 @@ using PixiEditor.ViewModels.Nodes;
 
 namespace PixiEditor.ViewModels.Document.Nodes.FilterNodes;
 
-[NodeViewModel("COLOR_MATRIX_TRANSFORM_FILTER_NODE", "FILTERS", "\ue911")]
+[NodeViewModel("COLOR_MATRIX_TRANSFORM_FILTER_NODE", "FILTERS", "\uE811")]
 internal class ColorMatrixFilterNodeViewModel : NodeViewModel<ColorMatrixFilterNode>;

+ 1 - 1
src/PixiEditor/ViewModels/Document/Nodes/FilterNodes/GrayscaleNodeViewModel.cs

@@ -3,5 +3,5 @@ using PixiEditor.ViewModels.Nodes;
 
 namespace PixiEditor.ViewModels.Document.Nodes.FilterNodes;
 
-[NodeViewModel("GRAYSCALE_FILTER_NODE", "FILTERS", "\ue912")]
+[NodeViewModel("GRAYSCALE_FILTER_NODE", "FILTERS", "\ue812")]
 internal class GrayscaleNodeViewModel : NodeViewModel<GrayscaleNode>;

+ 1 - 1
src/PixiEditor/ViewModels/Document/Nodes/FilterNodes/KernelFilterNodeViewModel.cs

@@ -3,5 +3,5 @@ using PixiEditor.ViewModels.Nodes;
 
 namespace PixiEditor.ViewModels.Document.Nodes.FilterNodes;
 
-[NodeViewModel("KERNEL_FILTER_NODE", "FILTERS", "\ue90f")]
+[NodeViewModel("KERNEL_FILTER_NODE", "FILTERS", "\uE80F")]
 internal class KernelFilterNodeViewModel : NodeViewModel<KernelFilterNode>;

+ 1 - 1
src/PixiEditor/ViewModels/Document/Nodes/FolderNodeViewModel.cs

@@ -6,7 +6,7 @@ using PixiEditor.ViewModels.Nodes;
 
 namespace PixiEditor.ViewModels.Document.Nodes;
 
-[NodeViewModel("FOLDER_NODE", "STRUCTURE", "\ue901")]
+[NodeViewModel("FOLDER_NODE", "STRUCTURE", "\uE801")]
 internal class FolderNodeViewModel : StructureMemberViewModel<FolderNode>, IFolderHandler
 {
     public ObservableCollection<IStructureMemberHandler> Children { get; } = new();

+ 1 - 1
src/PixiEditor/ViewModels/Document/Nodes/ImageLayerNodeViewModel.cs

@@ -6,7 +6,7 @@ using PixiEditor.ViewModels.Tools.Tools;
 
 namespace PixiEditor.ViewModels.Document.Nodes;
 
-[NodeViewModel("IMAGE_LAYER_NODE", "STRUCTURE", "\ue905")]
+[NodeViewModel("IMAGE_LAYER_NODE", "STRUCTURE", "\uE805")]
 internal class ImageLayerNodeViewModel : StructureMemberViewModel<ImageLayerNode>, ITransparencyLockableMember, IRasterLayerHandler
 {
     bool lockTransparency;

+ 1 - 1
src/PixiEditor/ViewModels/Document/Nodes/LerpColorNodeViewModel.cs

@@ -4,5 +4,5 @@ using PixiEditor.ViewModels.Nodes;
 
 namespace PixiEditor.ViewModels.Document.Nodes;
 
-[NodeViewModel("LERP_NODE", "NUMBERS", "\ue90b")]
+[NodeViewModel("LERP_NODE", "NUMBERS", "\uE80B")]
 internal class LerpColorNodeViewModel : NodeViewModel<LerpColorNode>;

+ 1 - 1
src/PixiEditor/ViewModels/Document/Nodes/MathNodeViewModel.cs

@@ -9,7 +9,7 @@ using PixiEditor.ViewModels.Nodes.Properties;
 
 namespace PixiEditor.ViewModels.Document.Nodes;
 
-[NodeViewModel("MATH_NODE", "NUMBERS", "\ue90e")]
+[NodeViewModel("MATH_NODE", "NUMBERS", "\uE80E")]
 internal class MathNodeViewModel : NodeViewModel<MathNode>
 {
     private GenericEnumPropertyViewModel Mode { get; set; }

+ 1 - 1
src/PixiEditor/ViewModels/Document/Nodes/MergeNodeViewModel.cs

@@ -4,5 +4,5 @@ using PixiEditor.ViewModels.Nodes;
 
 namespace PixiEditor.ViewModels.Document.Nodes;
 
-[NodeViewModel("MERGE_NODE", "IMAGE", "\ue903")]
+[NodeViewModel("MERGE_NODE", "IMAGE", "\uE803")]
 internal class MergeNodeViewModel : NodeViewModel<MergeNode>;

+ 1 - 1
src/PixiEditor/ViewModels/Document/Nodes/ModifyImageLeftNodeViewModel.cs

@@ -4,5 +4,5 @@ using PixiEditor.ViewModels.Nodes;
 
 namespace PixiEditor.ViewModels.Document.Nodes;
 
-[NodeViewModel("MODIFY_IMAGE_LEFT_NODE", "IMAGE", "\ue904")]
+[NodeViewModel("MODIFY_IMAGE_LEFT_NODE", "IMAGE", "\uE804")]
 internal class ModifyImageLeftNodeViewModel : NodeViewModel<ModifyImageLeftNode>;

+ 1 - 1
src/PixiEditor/ViewModels/Document/Nodes/NoiseNodeViewModel.cs

@@ -4,5 +4,5 @@ using PixiEditor.ViewModels.Nodes;
 
 namespace PixiEditor.ViewModels.Document.Nodes;
 
-[NodeViewModel("NOISE_NODE", "IMAGE", "\ue90c")]
+[NodeViewModel("NOISE_NODE", "IMAGE", "\uE80C")]
 internal class NoiseNodeViewModel : NodeViewModel<NoiseNode>;

+ 1 - 1
src/PixiEditor/ViewModels/Document/Nodes/SampleImageNodeViewModel.cs

@@ -4,5 +4,5 @@ using PixiEditor.ViewModels.Nodes;
 
 namespace PixiEditor.ViewModels.Document.Nodes;
 
-[NodeViewModel("SAMPLE_IMAGE", "IMAGE", "\ue907")]
+[NodeViewModel("SAMPLE_IMAGE", "IMAGE", "\uE807")]
 internal class SampleImageNodeViewModel : NodeViewModel<SampleImageNode>;

+ 1 - 1
src/PixiEditor/ViewModels/Document/Nodes/Shapes/DistributePointsNodeViewModel.cs

@@ -3,5 +3,5 @@ using PixiEditor.ViewModels.Nodes;
 
 namespace PixiEditor.ViewModels.Document.Nodes.Shapes;
 
-[NodeViewModel("DISTRIBUTE_POINTS", "SHAPE", "\ue90a")]
+[NodeViewModel("DISTRIBUTE_POINTS", "SHAPE", "\uE80A")]
 internal class DistributePointsNodeViewModel : NodeViewModel<DistributePointsNode>;

+ 1 - 1
src/PixiEditor/ViewModels/Document/Nodes/Shapes/EllipseNodeViewModel.cs

@@ -3,5 +3,5 @@ using PixiEditor.ViewModels.Nodes;
 
 namespace PixiEditor.ViewModels.Document.Nodes.Shapes;
 
-[NodeViewModel("ELLIPSE_NODE", "SHAPE", "\ue90d")]
+[NodeViewModel("ELLIPSE_NODE", "SHAPE", "\uE910")]
 internal class EllipseNodeViewModel : NodeViewModel<EllipseNode>;

+ 1 - 1
src/PixiEditor/ViewModels/Document/Nodes/Shapes/RasterizeShapeNodeViewModel.cs

@@ -3,5 +3,5 @@ using PixiEditor.ViewModels.Nodes;
 
 namespace PixiEditor.ViewModels.Document.Nodes.Shapes;
 
-[NodeViewModel("RASTERIZE_SHAPE", "SHAPE", "\ue906")]
+[NodeViewModel("RASTERIZE_SHAPE", "SHAPE", "\uE806")]
 internal class RasterizeShapeNodeViewModel : NodeViewModel<RasterizeShapeNode>;

+ 1 - 1
src/PixiEditor/ViewModels/Document/Nodes/Shapes/RemoveClosePointsNodeViewModel.cs

@@ -3,5 +3,5 @@ using PixiEditor.ViewModels.Nodes;
 
 namespace PixiEditor.ViewModels.Document.Nodes.Shapes;
 
-[NodeViewModel("REMOVE_CLOSE_POINTS", "SHAPE", "\ue914")]
+[NodeViewModel("REMOVE_CLOSE_POINTS", "SHAPE", "\uE814")]
 internal class RemoveClosePointsNodeViewModel : NodeViewModel<RemoveClosePointsNode>;

+ 1 - 1
src/PixiEditor/ViewModels/Document/Nodes/Shapes/TextNodeViewModel.cs

@@ -3,7 +3,7 @@ using PixiEditor.ViewModels.Nodes;
 
 namespace PixiEditor.ViewModels.Document.Nodes.Shapes;
 
-[NodeViewModel("TEXT_NODE", "SHAPE", "\ue90d")]
+[NodeViewModel("TEXT_NODE", "SHAPE", "\uE999")]
 internal class TextNodeViewModel : NodeViewModel<TextNode>
 {
 }

+ 9 - 0
src/PixiEditor/ViewModels/Document/Nodes/Shapes/TextOnPathNodeViewModel.cs

@@ -0,0 +1,9 @@
+using PixiEditor.ChangeableDocument.Changeables.Graph.Nodes.Shapes;
+using PixiEditor.ViewModels.Nodes;
+
+namespace PixiEditor.ViewModels.Document.Nodes.Shapes;
+
+[NodeViewModel("TEXT_ON_PATH_NODE", "SHAPE", "\uE998")]
+internal class TextOnPathNodeViewModel : NodeViewModel<TextOnPathNode>
+{
+}

+ 1 - 1
src/PixiEditor/ViewModels/Document/Nodes/VectorLayerNodeViewModel.cs

@@ -9,7 +9,7 @@ using PixiEditor.ViewModels.Tools.Tools;
 
 namespace PixiEditor.ViewModels.Document.Nodes;
 
-[NodeViewModel("VECTOR_LAYER", "STRUCTURE", "\ue916")]
+[NodeViewModel("VECTOR_LAYER", "STRUCTURE", "\uE816")]
 internal class VectorLayerNodeViewModel : StructureMemberViewModel<VectorLayerNode>, IVectorLayerHandler
 {
     private Dictionary<Type, Type> quickToolsMap = new Dictionary<Type, Type>()

+ 10 - 0
src/PixiEditor/ViewModels/Nodes/Properties/FontFamilyNamePropertyViewModel.cs

@@ -0,0 +1,10 @@
+using Drawie.Backend.Core.Text;
+
+namespace PixiEditor.ViewModels.Nodes.Properties;
+
+internal class FontFamilyNamePropertyViewModel : NodePropertyViewModel<FontFamilyName>
+{
+    public FontFamilyNamePropertyViewModel(NodeViewModel node, Type valueType) : base(node, valueType)
+    {
+    }
+}

+ 29 - 13
src/PixiEditor/ViewModels/SubViewModels/FileViewModel.cs

@@ -48,7 +48,7 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
 
     public RecentlyOpenedCollection RecentlyOpened { get; init; }
     public IReadOnlyList<IDocumentBuilder> DocumentBuilders => documentBuilders;
-    
+
     private List<IDocumentBuilder> documentBuilders;
 
     public FileViewModel(ViewModelMain owner)
@@ -205,7 +205,7 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
             }
             else if (IsCustomFormat(path))
             {
-                OpenCustomFormat(path);
+                OpenCustomFormat(path, associatePath);
             }
             else
             {
@@ -221,26 +221,42 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
             NoticeDialog.Show("OLD_FILE_FORMAT_DESCRIPTION", "OLD_FILE_FORMAT");
         }
     }
-    
+
     private bool IsCustomFormat(string path)
     {
         string extension = Path.GetExtension(path);
         return documentBuilders.Any(x => x.Extensions.Contains(extension, StringComparer.OrdinalIgnoreCase));
     }
-    
-    private void OpenCustomFormat(string path)
+
+    private void OpenCustomFormat(string path, bool associatePath)
     {
-        IDocumentBuilder builder = documentBuilders.First(x => x.Extensions.Contains(Path.GetExtension(path), StringComparer.OrdinalIgnoreCase));
+        IDocumentBuilder builder = documentBuilders.First(x =>
+            x.Extensions.Contains(Path.GetExtension(path), StringComparer.OrdinalIgnoreCase));
 
-        if(!File.Exists(path))
+        if (!File.Exists(path))
         {
             NoticeDialog.Show("FILE_NOT_FOUND", "FAILED_TO_OPEN_FILE");
             return;
         }
-        
-        DocumentViewModel document = DocumentViewModel.Build(docBuilder => builder.Build(docBuilder, path));
-        AddDocumentViewModelToTheSystem(document);
-        AddRecentlyOpened(document.FullFilePath);
+
+        try
+        {
+            DocumentViewModel document = DocumentViewModel.Build(docBuilder => builder.Build(docBuilder, path));
+            AddDocumentViewModelToTheSystem(document);
+
+            if (associatePath)
+            {
+                document.FullFilePath = path;
+            }
+
+            AddRecentlyOpened(document.FullFilePath);
+        }
+        catch (Exception ex)
+        {
+            NoticeDialog.Show("FAILED_TO_OPEN_FILE", "ERROR");
+            Console.WriteLine(ex);
+            CrashHelper.SendExceptionInfo(ex);
+        }
     }
 
     /// <summary>
@@ -281,7 +297,7 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
             .WithGraph(x => x
                 .WithImageLayerNode(
                     new LocalizedString("IMAGE"),
-                    image, 
+                    image,
                     ColorSpace.CreateSrgbLinear(),
                     out int id)
                 .WithOutputNode(id, "Output")
@@ -349,7 +365,7 @@ internal class FileViewModel : SubViewModel<ViewModelMain>
                 .WithGraph(x => x
                     .WithImageLayerNode(
                         new LocalizedString("BASE_LAYER_NAME"),
-                        new VecI(newFile.Width, newFile.Height), 
+                        new VecI(newFile.Width, newFile.Height),
                         ColorSpace.CreateSrgbLinear(),
                         out int id)
                     .WithOutputNode(id, "Output")

+ 16 - 46
src/PixiEditor/ViewModels/Tools/ToolSettings/Settings/FontFamilySettingViewModel.cs

@@ -16,24 +16,9 @@ namespace PixiEditor.ViewModels.Tools.ToolSettings.Settings;
 
 internal class FontFamilySettingViewModel : Setting<FontFamilyName>
 {
+    private ObservableCollection<FontFamilyName> allFonts;
     private int selectedIndex;
 
-
-    private ObservableCollection<FontFamilyName> _fonts;
-
-    public ObservableCollection<FontFamilyName> Fonts
-    {
-        get
-        {
-            return _fonts;
-        }
-        set
-        {
-            SetProperty(ref _fonts, value);
-        }
-    }
-
-
     public int FontIndex
     {
         get
@@ -43,42 +28,27 @@ internal class FontFamilySettingViewModel : Setting<FontFamilyName>
         set
         {
             SetProperty(ref selectedIndex, value);
-            Value = Fonts[value];
+
+            if (Fonts?.Count > 0)
+            {
+                Value = Fonts[value];
+            }
+            else
+            {
+                Value = FontLibrary.DefaultFontFamily;
+            }
         }
     }
 
-    public AsyncRelayCommand UploadFontCommand { get; }
-
-    public FontFamilySettingViewModel(string name, string displayName) : base(name)
+    public ObservableCollection<FontFamilyName> Fonts
     {
-        Label = displayName;
-        Fonts = new ObservableCollection<FontFamilyName>(FontLibrary.AllFonts);
-        FontLibrary.FontAdded += (font) => Fonts.Add(font); 
-        UploadFontCommand = new AsyncRelayCommand(UploadFont);
+        get => allFonts;
+        set => SetProperty(ref allFonts, value);
     }
 
-    private async Task UploadFont()
-    {
-        FilePickerFileType[] filter =
-        [
-            new FilePickerFileType(new LocalizedString("FONT_FILES")) { Patterns = new List<string> { "*.ttf", "*.otf" } },
-            new FilePickerFileType("TrueType Font") { Patterns = new List<string> { "*.ttf" } },
-            new FilePickerFileType("OpenType Font") { Patterns = new List<string> { "*.otf" } },
-        ];
-        
-        if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
-        {
-            var dialog = await desktop.MainWindow.StorageProvider.OpenFilePickerAsync(
-                new FilePickerOpenOptions { FileTypeFilter = filter });
-
-            if (dialog.Count == 0)
-                return;
 
-            var fontPath = dialog[0];
-            FontFamilyName familyName = new FontFamilyName(fontPath.Path, Path.GetFileNameWithoutExtension(fontPath.Name));
-            FontLibrary.TryAddCustomFont(familyName);
-            
-            FontIndex = Fonts.IndexOf(familyName);
-        }
+    public FontFamilySettingViewModel(string name, string displayName) : base(name)
+    {
+        Label = displayName;
     }
 }

+ 2 - 1
src/PixiEditor/ViewModels/Tools/Tools/TextToolViewModel.cs

@@ -13,6 +13,7 @@ namespace PixiEditor.ViewModels.Tools.Tools;
 [Command.Tool(Key = Key.T)]
 internal class TextToolViewModel : ToolViewModel, ITextToolHandler
 {
+    public const string NewLayerKey = "TEXT_LAYER_NAME";
     public override string ToolNameLocalizationKey => "TEXT_TOOL";
     public override Type[]? SupportedLayerTypes => [];
     public override Type LayerTypeToCreateOnEmptyUse => typeof(VectorLayerNode);
@@ -23,7 +24,7 @@ internal class TextToolViewModel : ToolViewModel, ITextToolHandler
     public override bool IsErasable => false;
     public override bool StopsLinkedToolOnUse => false;
 
-    public string? DefaultNewLayerName { get; } = new LocalizedString("TEXT_LAYER_NAME");
+    public string? DefaultNewLayerName { get; } = new LocalizedString(NewLayerKey);
 
     [Settings.Inherited]
     public double FontSize

+ 6 - 0
src/PixiEditor/ViewModels/Tools/Tools/VectorPathToolViewModel.cs

@@ -62,6 +62,12 @@ internal class VectorPathToolViewModel : ShapeTool, IVectorPathToolHandler
         {
             fillSetting.Value = false;
         }
+
+        var strokeSetting = Toolbar.GetSetting(nameof(ShapeToolbar.ToolSize));
+        if (strokeSetting != null)
+        {
+            strokeSetting.Value = 1d;
+        }
         
         actionDisplayDefault = new LocalizedString("PATH_TOOL_ACTION_DISPLAY");
         actionDisplayCtrl = new LocalizedString("PATH_TOOL_ACTION_DISPLAY_CTRL");

+ 25 - 0
src/PixiEditor/Views/Input/FontFamilyPicker.axaml

@@ -0,0 +1,25 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+             xmlns:ui="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
+             xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters"
+             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+             x:Class="PixiEditor.Views.Nodes.Properties.FontFamilyPicker">
+    <StackPanel Orientation="Horizontal" Spacing="5" DataContext="{Binding RelativeSource={RelativeSource AncestorType=UserControl}}">
+        <Button Classes="pixi-icon" FontSize="16"
+                ui:Translator.TooltipKey="CUSTOM_FONT"
+                Command="{Binding UploadFontCommand}" Content="{DynamicResource icon-upload}" />
+        <ComboBox VerticalAlignment="Center"
+                  MinWidth="85"
+                  ItemsSource="{Binding Fonts}"
+                  SelectedIndex="{Binding FontIndex, Mode=TwoWay}">
+            <ComboBox.ItemTemplate>
+                <DataTemplate>
+                    <TextBlock Text="{Binding Name}"
+                               FontFamily="{Binding Converter={converters:FontFamilyNameToAvaloniaFontFamily}}" />
+                </DataTemplate>
+            </ComboBox.ItemTemplate>
+        </ComboBox>
+    </StackPanel>
+</UserControl>

+ 105 - 0
src/PixiEditor/Views/Input/FontFamilyPicker.axaml.cs

@@ -0,0 +1,105 @@
+using System.Collections.ObjectModel;
+using System.Windows.Input;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Markup.Xaml;
+using Avalonia.Platform.Storage;
+using CommunityToolkit.Mvvm.Input;
+using Drawie.Backend.Core.Text;
+using PixiEditor.Extensions.Common.Localization;
+using PixiEditor.Models.Controllers;
+
+namespace PixiEditor.Views.Nodes.Properties;
+
+public partial class FontFamilyPicker : UserControl
+{
+    private int selectedIndex;
+
+    public static readonly StyledProperty<ICommand> UploadFontCommandProperty =
+        AvaloniaProperty.Register<FontFamilyPicker, ICommand>(
+            nameof(UploadFontCommand));
+
+    public static readonly StyledProperty<ObservableCollection<FontFamilyName>> FontsProperty = AvaloniaProperty.Register<FontFamilyPicker, ObservableCollection<FontFamilyName>>(
+        nameof(Fonts));
+
+    public static readonly StyledProperty<FontFamilyName> SelectedFontFamilyProperty = AvaloniaProperty.Register<FontFamilyPicker, FontFamilyName>(
+        nameof(SelectedFontFamily));
+
+    public FontFamilyName SelectedFontFamily
+    {
+        get => GetValue(SelectedFontFamilyProperty);
+        set => SetValue(SelectedFontFamilyProperty, value);
+    }
+
+    public ObservableCollection<FontFamilyName> Fonts
+    {
+        get => GetValue(FontsProperty);
+        set => SetValue(FontsProperty, value);
+    }
+
+    public static readonly StyledProperty<int> FontIndexProperty = AvaloniaProperty.Register<FontFamilyPicker, int>(
+        nameof(FontIndex));
+
+    public int FontIndex
+    {
+        get => GetValue(FontIndexProperty);
+        set => SetValue(FontIndexProperty, value);
+    }
+
+    public ICommand UploadFontCommand
+    {
+        get => GetValue(UploadFontCommandProperty);
+        set => SetValue(UploadFontCommandProperty, value);
+    }
+
+    static FontFamilyPicker()
+    {
+        FontIndexProperty.Changed.AddClassHandler<FontFamilyPicker>((sender, e) =>
+        {
+            if (e.NewValue is int newIndex)
+            {
+                sender.FontIndex = newIndex;
+                sender.SelectedFontFamily = sender.Fonts[newIndex];
+            }
+        });
+    }
+
+    public FontFamilyPicker()
+    {
+        InitializeComponent();
+        UploadFontCommand = new AsyncRelayCommand(UploadFont);
+        Fonts = new ObservableCollection<FontFamilyName>(FontLibrary.AllFonts);
+        FontLibrary.FontAdded += (font) => Fonts.Add(font);
+        SelectedFontFamily = Fonts[0];
+    }
+
+    private async Task UploadFont()
+    {
+        FilePickerFileType[] filter =
+        [
+            new FilePickerFileType(new LocalizedString("FONT_FILES"))
+            {
+                Patterns = new List<string> { "*.ttf", "*.otf" }
+            },
+            new FilePickerFileType("TrueType Font") { Patterns = new List<string> { "*.ttf" } },
+            new FilePickerFileType("OpenType Font") { Patterns = new List<string> { "*.otf" } },
+        ];
+
+        if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+        {
+            var dialog = await desktop.MainWindow.StorageProvider.OpenFilePickerAsync(
+                new FilePickerOpenOptions { FileTypeFilter = filter });
+
+            if (dialog.Count == 0)
+                return;
+
+            var fontPath = dialog[0];
+            FontFamilyName familyName =
+                new FontFamilyName(fontPath.Path, Path.GetFileNameWithoutExtension(fontPath.Name));
+            FontLibrary.TryAddCustomFont(familyName);
+
+            FontIndex = Fonts.IndexOf(familyName);
+        }
+    }
+}

+ 25 - 0
src/PixiEditor/Views/Nodes/Properties/FontFamilyNamePropertyView.axaml

@@ -0,0 +1,25 @@
+<properties:NodePropertyView xmlns="https://github.com/avaloniaui"
+                             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+                             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+                             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+                             xmlns:properties="clr-namespace:PixiEditor.Views.Nodes.Properties"
+                             xmlns:ui="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
+                             xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters"
+                             xmlns:behaviours="clr-namespace:PixiEditor.Helpers.Behaviours"
+                             xmlns:system="clr-namespace:System;assembly=System.Runtime"
+                             xmlns:properties1="clr-namespace:PixiEditor.ViewModels.Nodes.Properties"
+                             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+                             x:DataType="properties1:FontFamilyNamePropertyViewModel"
+                             x:Class="PixiEditor.Views.Nodes.Properties.FontFamilyNamePropertyView">
+    <DockPanel LastChildFill="True"
+        HorizontalAlignment="{Binding IsInput, Converter={converters:BoolToValueConverter FalseValue='Right', TrueValue='Stretch'}}">
+        <TextBlock VerticalAlignment="Center" ui:Translator.Key="{Binding DisplayName}" />
+        <properties:FontFamilyPicker Margin="5 0"
+           SelectedFontFamily="{Binding Value, Mode=TwoWay}"
+                          IsVisible="{Binding ShowInputField}">
+            <Interaction.Behaviors>
+                <behaviours:GlobalShortcutFocusBehavior />
+            </Interaction.Behaviors>
+        </properties:FontFamilyPicker>
+    </DockPanel>
+</properties:NodePropertyView>

+ 20 - 0
src/PixiEditor/Views/Nodes/Properties/FontFamilyNamePropertyView.axaml.cs

@@ -0,0 +1,20 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.Markup.Xaml;
+
+namespace PixiEditor.Views.Nodes.Properties;
+
+public partial class FontFamilyNamePropertyView : NodePropertyView
+{
+    public FontFamilyNamePropertyView()
+    {
+        InitializeComponent();
+    }
+
+    protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
+    {
+        base.OnApplyTemplate(e);
+        HideSocket(true, true);
+    }
+}

+ 13 - 0
src/PixiEditor/Views/Nodes/Properties/NodePropertyView.cs

@@ -57,6 +57,19 @@ public abstract class NodePropertyView : UserControl
         
         return point ?? default;
     }
+
+    protected void HideSocket(bool hideInputSocket, bool hideOutputSocket)
+    {
+        if (hideInputSocket)
+        {
+            InputSocket.IsVisible = false;
+        }
+
+        if (hideOutputSocket)
+        {
+            OutputSocket.IsVisible = false;
+        }
+    }
 }
 
 public abstract class NodePropertyView<T> : NodePropertyView

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

@@ -14,7 +14,7 @@
     <DockPanel LastChildFill="True"
         HorizontalAlignment="{Binding IsInput, Converter={converters:BoolToValueConverter FalseValue='Right', TrueValue='Stretch'}}">
         <TextBlock VerticalAlignment="Center" ui:Translator.Key="{Binding DisplayName}" />
-        <TextBox Text="{CompiledBinding StringValue, Mode=TwoWay}" IsVisible="{Binding ShowInputField}">
+        <TextBox AcceptsReturn="True" Text="{CompiledBinding StringValue, Mode=TwoWay}" IsVisible="{Binding ShowInputField}">
             <Interaction.Behaviors>
                 <behaviours:GlobalShortcutFocusBehavior />
             </Interaction.Behaviors>

+ 20 - 0
src/PixiEditor/Views/Overlays/Overlay.cs

@@ -123,6 +123,18 @@ public abstract class Overlay : Decorator, IOverlay // TODO: Maybe make it not a
         PointerMovedOverlay?.Invoke(args);
     }
 
+    public void FocusChanged(bool focused)
+    {
+        if (focused)
+        {
+            OnOverlayGotFocus();
+        }
+        else
+        {
+            OnOverlayLostFocus();
+        }
+    }
+
     public void PressPointer(OverlayPointerArgs args)
     {
         InvokeHandleEvent(HandleEventType.PointerPressedOverlay, args);
@@ -306,6 +318,14 @@ public abstract class Overlay : Decorator, IOverlay // TODO: Maybe make it not a
     {
     }
 
+    protected virtual void OnOverlayLostFocus()
+    {
+    }
+
+    protected virtual void OnOverlayGotFocus()
+    {
+    }
+
     private static void OnZoomScaleChanged(AvaloniaPropertyChangedEventArgs<double> e)
     {
         if (e.Sender is Overlay overlay)

+ 14 - 0
src/PixiEditor/Views/Overlays/TextOverlay/TextOverlay.cs

@@ -1,5 +1,6 @@
 using Avalonia;
 using Avalonia.Input;
+using Avalonia.Interactivity;
 using Avalonia.Threading;
 using Drawie.Backend.Core.ColorsImpl;
 using Drawie.Backend.Core.Numerics;
@@ -215,6 +216,19 @@ internal class TextOverlay : Overlay
         Refresh();
     }
 
+    protected override void OnOverlayLostFocus()
+    {
+        ShortcutController.UnblockShortcutExecution(nameof(TextOverlay));
+    }
+
+    protected override void OnOverlayGotFocus()
+    {
+        if (IsEditing)
+        {
+            ShortcutController.BlockShortcutExecution(nameof(TextOverlay));
+        }
+    }
+
     private void RenderCaret(Canvas context)
     {
         caret.CaretPosition = CursorPosition;

+ 26 - 0
src/PixiEditor/Views/Rendering/Scene.cs

@@ -502,6 +502,32 @@ internal class Scene : Zoombox.Zoombox, ICustomHitTest
         Focus();
     }
 
+    protected override void OnGotFocus(GotFocusEventArgs e)
+    {
+        base.OnGotFocus(e);
+        if (AllOverlays != null)
+        {
+            foreach (Overlay overlay in AllOverlays)
+            {
+                if (!overlay.IsVisible) continue;
+                overlay.FocusChanged(true);
+            }
+        }
+    }
+
+    protected override void OnLostFocus(RoutedEventArgs e)
+    {
+        base.OnLostFocus(e);
+        if (AllOverlays != null)
+        {
+            foreach (Overlay overlay in AllOverlays)
+            {
+                if (!overlay.IsVisible) continue;
+                overlay.FocusChanged(false);
+            }
+        }
+    }
+
     private VecD ToCanvasSpace(Point scenePosition)
     {
         Matrix transform = CalculateTransformMatrix();

+ 3 - 15
src/PixiEditor/Views/Tools/ToolSettings/Settings/FontFamilySettingView.axaml

@@ -7,25 +7,13 @@
              xmlns:ui="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
              xmlns:helpers="clr-namespace:PixiEditor.Helpers"
              xmlns:converters="clr-namespace:PixiEditor.Helpers.Converters"
+             xmlns:properties="clr-namespace:PixiEditor.Views.Nodes.Properties"
              mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
              x:Class="PixiEditor.Views.Tools.ToolSettings.Settings.FontFamilySettingView">
     <Design.DataContext>
         <settings:FontFamilySettingViewModel />
     </Design.DataContext>
 
-    <StackPanel Orientation="Horizontal" Spacing="5">
-        <Button Classes="pixi-icon" FontSize="16"
-                ui:Translator.TooltipKey="CUSTOM_FONT"
-                Command="{Binding UploadFontCommand}" Content="{DynamicResource icon-upload}"/>
-        <ComboBox VerticalAlignment="Center"
-                  MinWidth="85"
-                  ItemsSource="{Binding Fonts}"
-                  SelectedIndex="{Binding FontIndex, Mode=TwoWay}">
-            <ComboBox.ItemTemplate>
-                <DataTemplate>
-                    <TextBlock Text="{Binding Name}" FontFamily="{Binding Converter={converters:FontFamilyNameToAvaloniaFontFamily}}" />
-                </DataTemplate>
-            </ComboBox.ItemTemplate>
-        </ComboBox>
-    </StackPanel>
+    <properties:FontFamilyPicker FontIndex="{Binding FontIndex, Mode=TwoWay}"
+                                 Fonts="{Binding Fonts, Mode=OneWayToSource}"/>
 </UserControl>

+ 1 - 1
src/PixiParser

@@ -1 +1 @@
-Subproject commit 5b632b4c649f8e88ec8616a383fac56afd728e34
+Subproject commit f3475837fbd05e8ba11bcc5f4fc8cca1458d73d9