Browse Source

Merge pull request #933 from PixiEditor/api/ui-elements

Some FlyUI elements and api changes
Krzysztof Krysiński 3 months ago
parent
commit
fe08eaac38
63 changed files with 912 additions and 266 deletions
  1. 5 3
      samples/Sample7_FlyUI/WindowContentElement.cs
  2. 17 15
      src/PixiEditor.Beta/WelcomeMessageState.cs
  3. 6 3
      src/PixiEditor.Extensions.CommonApi/FlyUI/ByteMap.cs
  4. 72 0
      src/PixiEditor.Extensions.CommonApi/FlyUI/Cursor.cs
  5. 12 1
      src/PixiEditor.Extensions.CommonApi/FlyUI/Properties/Color.cs
  6. 0 8
      src/PixiEditor.Extensions.CommonApi/FlyUI/Properties/FontStyle.cs
  7. 148 0
      src/PixiEditor.Extensions.CommonApi/FlyUI/Properties/TextStyle.cs
  8. 4 0
      src/PixiEditor.Extensions.CommonApi/Palettes/FilteringSettings.Impl.cs
  9. 6 7
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/Align.cs
  10. 2 1
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/AxisAlignment.cs
  11. 22 17
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/Border.cs
  12. 3 4
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/Button.cs
  13. 3 4
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/Center.cs
  14. 3 4
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/CheckBox.cs
  15. 10 8
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/Column.cs
  16. 5 5
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/Container.cs
  17. 26 7
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/ControlDefinition.cs
  18. 6 8
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/Hyperlink.cs
  19. 29 0
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/Icon.cs
  20. 6 6
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/Image.cs
  21. 3 4
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/Layout.cs
  22. 44 8
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/LayoutElement.cs
  23. 2 2
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/LayoutElementsStore.cs
  24. 6 5
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/MultiChildLayoutElement.cs
  25. 9 8
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/Padding.cs
  26. 10 8
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/Row.cs
  27. 6 3
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/SingleChildLayoutElement.cs
  28. 2 2
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/State.cs
  29. 15 9
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/StatefulElement.cs
  30. 16 2
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/StatelessElement.cs
  31. 10 14
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/Text.cs
  32. 6 6
      src/PixiEditor.Extensions.Sdk/Api/Window/WindowProvider.cs
  33. 1 1
      src/PixiEditor.Extensions.Sdk/Bridge/Native.cs
  34. BIN
      src/PixiEditor.Extensions.Sdk/build/PixiEditor.Api.CGlueMSBuild.dll
  35. BIN
      src/PixiEditor.Extensions.Sdk/build/PixiEditor.Extensions.MSPackageBuilder.dll
  36. 28 0
      src/PixiEditor.Extensions/FlyUI/Converters/IconLookupConverter.cs
  37. 5 5
      src/PixiEditor.Extensions/FlyUI/Elements/Align.cs
  38. 4 4
      src/PixiEditor.Extensions/FlyUI/Elements/Border.cs
  39. 2 2
      src/PixiEditor.Extensions/FlyUI/Elements/Button.cs
  40. 1 1
      src/PixiEditor.Extensions/FlyUI/Elements/Center.cs
  41. 1 1
      src/PixiEditor.Extensions/FlyUI/Elements/CheckBox.cs
  42. 3 3
      src/PixiEditor.Extensions/FlyUI/Elements/Column.cs
  43. 3 3
      src/PixiEditor.Extensions/FlyUI/Elements/Container.cs
  44. 8 10
      src/PixiEditor.Extensions/FlyUI/Elements/Hyperlink.cs
  45. 66 0
      src/PixiEditor.Extensions/FlyUI/Elements/Icon.cs
  46. 4 4
      src/PixiEditor.Extensions/FlyUI/Elements/Image.cs
  47. 1 1
      src/PixiEditor.Extensions/FlyUI/Elements/Layout.cs
  48. 5 1
      src/PixiEditor.Extensions/FlyUI/Elements/LayoutBuilder.cs
  49. 91 5
      src/PixiEditor.Extensions/FlyUI/Elements/LayoutElement.cs
  50. 3 1
      src/PixiEditor.Extensions/FlyUI/Elements/MainAxisAlignment.cs
  51. 0 1
      src/PixiEditor.Extensions/FlyUI/Elements/MultiChildLayoutElement.cs
  52. 3 3
      src/PixiEditor.Extensions/FlyUI/Elements/Padding.cs
  53. 3 3
      src/PixiEditor.Extensions/FlyUI/Elements/Row.cs
  54. 0 1
      src/PixiEditor.Extensions/FlyUI/Elements/SingleChildLayoutElement.cs
  55. 21 7
      src/PixiEditor.Extensions/FlyUI/Elements/StatefulElement.cs
  56. 5 0
      src/PixiEditor.Extensions/FlyUI/Elements/StatelessElement.cs
  57. 59 20
      src/PixiEditor.Extensions/FlyUI/Elements/Text.cs
  58. 1 1
      src/PixiEditor.Extensions/FlyUI/IPropertyDeserializable.cs
  59. 16 1
      src/PixiEditor.Extensions/UI/Panels/ColumnPanel.cs
  60. 10 2
      src/PixiEditor.Extensions/UI/Panels/RowPanel.cs
  61. 1 2
      src/PixiEditor/Helpers/Behaviours/ClearFocusOnClickBehavior.cs
  62. 36 11
      tests/PixiEditor.Extensions.Sdk.Tests/NativeControlSerializationTest.cs
  63. 17 0
      tests/PixiEditor.Extensions.Sdk.Tests/WindowContentElement.cs

+ 5 - 3
samples/Sample7_FlyUI/WindowContentElement.cs

@@ -1,4 +1,5 @@
 using System.Diagnostics.CodeAnalysis;
+using PixiEditor.Extensions.CommonApi.FlyUI.Events;
 using PixiEditor.Extensions.CommonApi.FlyUI.Properties;
 using PixiEditor.Extensions.Sdk;
 using PixiEditor.Extensions.Sdk.Api.FlyUI;
@@ -11,7 +12,7 @@ public class WindowContentElement : StatelessElement
 {
     public PopupWindow Window { get; set; }
 
-    public override CompiledControl BuildNative()
+    public override ControlDefinition BuildNative()
     {
         Layout layout = new Layout(body:
             new Container(margin: Edges.All(25), child:
@@ -24,11 +25,11 @@ public class WindowContentElement : StatelessElement
                             new Text(
                                 "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed vitae neque nibh. Duis sed pharetra dolor. Donec dui sapien, aliquam id sodales in, ornare et urna. Mauris nunc odio, sagittis eget lectus at, imperdiet ornare quam. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam euismod pellentesque blandit. Vestibulum sagittis, ligula non finibus lobortis, dolor lacus consectetur turpis, id facilisis ligula dolor vitae augue.",
                                 wrap: TextWrap.Wrap,
-                                fontSize: 16)
+                                textStyle: new TextStyle(fontSize: 16))
                         ),
                         new Align(
                             alignment: Alignment.CenterRight,
-                            child: new Text("- Paulo Coelho, The Alchemist (1233)", fontStyle: FontStyle.Italic)
+                            child: new Text("- Paulo Coelho, The Alchemist (1233)", textStyle: new TextStyle(fontStyle: FontStyle.Italic))
                         ),
                         new Container(
                             margin: Edges.Symmetric(25, 0),
@@ -56,4 +57,5 @@ public class WindowContentElement : StatelessElement
 
         return layout.BuildNative();
     }
+
 }

+ 17 - 15
src/PixiEditor.Beta/WelcomeMessageState.cs

@@ -13,9 +13,11 @@ We are extremely excited to share this version with you, early testers. Before y
 
 - App is not production ready! Expect bugs, crashes, unfinished features, placeholders and other signs of development.
 ";
-    
-private const string Body2 = "- Your feedback is the most important thing of this beta, please take a moment to report any issues and suggestions on PixiEditor Forum.";
-private const string Body3 = @"
+
+    private const string Body2 =
+        "- Your feedback is the most important thing of this beta, please take a moment to report any issues and suggestions on PixiEditor Forum.";
+
+    private const string Body3 = @"
 - We are collecting anonymous usage data to fix bugs, crashes and performance issues. This data will help us to improve the app. During the beta 
 there is no option to opt-out. No personal data is collected.
 
@@ -35,25 +37,25 @@ I understand that:
                 Alignment.TopCenter,
                 new Column(
                     new Center(new Text("Welcome to the open beta of PixiEditor 2.0!", TextWrap.Wrap,
-                        FontStyle.Normal,
-                        fontSize: 24)),
-                    new Text(Body1, TextWrap.Wrap, fontSize: 16),
+                        new TextStyle(fontSize: 24))),
+                    new Text(Body1, TextWrap.Wrap, new TextStyle(fontSize: 16)),
                     new Hyperlink("https://forum.pixieditor.net", Body2,
-                        fontSize: 16, textWrap: TextWrap.Wrap),
-                    new Text(Body3, TextWrap.Wrap, fontSize: 16),
+                        textStyle: new TextStyle(fontSize: 16), textWrap: TextWrap.Wrap),
+                    new Text(Body3, TextWrap.Wrap, new TextStyle(fontSize: 16)),
                     new CheckBox(
-                        new Text("The app may be unstable, crash or freeze", fontSize: 16,
-                            fontStyle: FontStyle.Italic),
+                        new Text("The app may be unstable, crash or freeze",
+                            textStyle: new TextStyle(fontStyle: FontStyle.Italic, fontSize: 16)),
                         onCheckedChanged: (args) => CheckboxChanged(args.Sender as CheckBox, 0)),
                     new CheckBox(
-                        new Text("I may encounter unfinished features and placeholders", fontSize: 16,
-                            fontStyle: FontStyle.Italic),
+                        new Text("I may encounter unfinished features and placeholders",
+                            textStyle: new TextStyle(fontSize: 16, fontStyle: FontStyle.Italic)),
                         onCheckedChanged: (args) => CheckboxChanged(args.Sender as CheckBox, 1)),
-                    new CheckBox(new Text("I may lose my work due to bugs", fontSize: 16, fontStyle: FontStyle.Italic),
+                    new CheckBox(new Text("I may lose my work due to bugs",
+                        textStyle: new TextStyle(fontSize: 16, fontStyle: FontStyle.Italic)),
                         onCheckedChanged: (args) => CheckboxChanged(args.Sender as CheckBox, 2)),
                     new CheckBox(
-                        new Text("I will have a lot of fun testing the app", fontSize: 16,
-                            fontStyle: FontStyle.Italic),
+                        new Text("I will have a lot of fun testing the app",
+                            textStyle: new TextStyle(fontSize: 16, fontStyle: FontStyle.Italic)),
                         onCheckedChanged: (args) => CheckboxChanged(args.Sender as CheckBox, 3)),
                     new Container(
                         margin: new Edges(0, 5, 0, 0),

+ 6 - 3
src/PixiEditor.Extensions.CommonApi/FlyUI/ByteMap.cs

@@ -1,11 +1,13 @@
-using PixiEditor.Extensions.CommonApi.FlyUI.Properties;
-
-namespace PixiEditor.Extensions.CommonApi.FlyUI;
+namespace PixiEditor.Extensions.CommonApi.FlyUI;
 
 public static class ByteMap
 {
     public static byte GetTypeByteId(Type type)
     {
+        if (type == null)
+        {
+            return 255;
+        }
         if (type == typeof(int))
         {
             return 0;
@@ -64,6 +66,7 @@ public static class ByteMap
             7 => typeof(char),
             8 => typeof(string),
             9 => typeof(byte[]),
+            255 => null,
             _ => throw new Exception($"Unknown unmanaged type id: {id}")
         };
     }

+ 72 - 0
src/PixiEditor.Extensions.CommonApi/FlyUI/Cursor.cs

@@ -0,0 +1,72 @@
+using PixiEditor.Extensions.CommonApi.FlyUI.Properties;
+
+namespace PixiEditor.Extensions.CommonApi.FlyUI;
+
+public struct Cursor : IStructProperty
+{
+    private const int Version = 1; // Serialization version, increment when changing the struct
+    public BuiltInCursor? BuiltInCursor { get; set; }
+    public bool IsCustom => BuiltInCursor == null;
+
+    public Cursor(BuiltInCursor builtInCursor)
+    {
+        BuiltInCursor = builtInCursor;
+    }
+
+    byte[] IStructProperty.Serialize()
+    {
+        var data = new List<byte>();
+        data.Add(Version);
+        data.Add(BuiltInCursor != null ? (byte)1 : (byte)0);
+        if (BuiltInCursor != null)
+        {
+            data.Add((byte)BuiltInCursor.Value);
+        }
+
+        return data.ToArray();
+    }
+
+    void IStructProperty.Deserialize(byte[] data)
+    {
+        int version = data[0];
+
+        int index = 1;
+        if (data[index] == 1)
+        {
+            index++;
+            BuiltInCursor = (BuiltInCursor)data[index];
+        }
+        else
+        {
+            BuiltInCursor = null;
+        }
+    }
+}
+
+public enum BuiltInCursor
+{
+    Arrow,
+    IBeam,
+    Wait,
+    Cross,
+    UpArrow,
+    SizeWestEast,
+    SizeNorthSouth,
+    SizeAll,
+    No,
+    Hand,
+    AppStarting,
+    Help,
+    TopSide,
+    BottomSide,
+    LeftSide,
+    RightSide,
+    TopLeftCorner,
+    TopRightCorner,
+    BottomLeftCorner,
+    BottomRightCorner,
+    DragMove,
+    DragCopy,
+    DragLink,
+    None,
+}

+ 12 - 1
src/PixiEditor.Extensions.CommonApi/FlyUI/Properties/Color.cs

@@ -22,7 +22,7 @@ public struct Color : IStructProperty
 
     byte[] IStructProperty.Serialize()
     {
-        return new byte[] { R, G, B, A };
+        return [R, G, B, A];
     }
 
     void IStructProperty.Deserialize(byte[] data)
@@ -32,4 +32,15 @@ public struct Color : IStructProperty
         B = data[2];
         A = data[3];
     }
+
+    public static Color FromBytes(byte[] data)
+    {
+        if (data.Length < 4)
+        {
+            throw new ArgumentException("Data array must contain at least 4 bytes.");
+        }
+
+
+        return new Color(data[0], data[1], data[2], data[3]);
+    }
 }

+ 0 - 8
src/PixiEditor.Extensions.CommonApi/FlyUI/Properties/FontStyle.cs

@@ -1,8 +0,0 @@
-namespace PixiEditor.Extensions.CommonApi.FlyUI.Properties;
-
-public enum FontStyle
-{
-    Normal,
-    Italic,
-    Oblique
-}

+ 148 - 0
src/PixiEditor.Extensions.CommonApi/FlyUI/Properties/TextStyle.cs

@@ -0,0 +1,148 @@
+using System.Text;
+
+namespace PixiEditor.Extensions.CommonApi.FlyUI.Properties;
+
+public struct TextStyle : IStructProperty
+{
+    // IMPORTANT: If you change this struct, you must also change the version below and handle
+    // deserialization in the FlyUI deserializer.
+    public const int Version = 1;
+    public string? FontFamily { get; set; }
+    public double? FontSize { get; set; }
+    public FontStyle? FontStyle { get; set; }
+    public FontWeight? FontWeight { get; set; }
+    public Color? Color { get; set; }
+
+    public static TextStyle Default => new TextStyle(null, null, null, null, null);
+
+    public TextStyle(string? fontFamily = null, double? fontSize = null, FontStyle? fontStyle = null, FontWeight? fontWeight = null, Color? color = null)
+    {
+        FontFamily = fontFamily;
+        FontSize = fontSize;
+        FontStyle = fontStyle;
+        FontWeight = fontWeight;
+        Color = color;
+    }
+
+    public byte[] Serialize()
+    {
+        var data = new List<byte>();
+        data.AddRange(BitConverter.GetBytes(Version));
+        data.AddRange(BitConverter.GetBytes(FontFamily?.Length ?? 0));
+        if (FontFamily != null)
+        {
+            data.AddRange(Encoding.UTF8.GetBytes(FontFamily));
+        }
+
+        data.Add(FontSize != null ? (byte)1 : (byte)0);
+        if (FontSize != null)
+        {
+            data.AddRange(BitConverter.GetBytes(FontSize.Value));
+        }
+
+        data.Add(FontStyle != null ? (byte)1 : (byte)0);
+        if (FontStyle != null)
+        {
+            data.Add((byte)FontStyle.Value);
+        }
+
+        data.Add(FontWeight != null ? (byte)1 : (byte)0);
+        if (FontWeight != null)
+        {
+            data.AddRange(BitConverter.GetBytes((int)FontWeight.Value));
+        }
+
+        data.Add(Color != null ? (byte)1 : (byte)0);
+        if (Color != null)
+        {
+            data.AddRange(((IStructProperty)Color).Serialize());
+        }
+
+        return data.ToArray();
+    }
+
+    public void Deserialize(byte[] data)
+    {
+        int index = 0;
+        int version = BitConverter.ToInt32(data, index);
+        index += 4;
+        int fontFamilyLength = BitConverter.ToInt32(data, index);
+        index += 4;
+        if (fontFamilyLength > 0)
+        {
+            FontFamily = Encoding.UTF8.GetString(data, index, fontFamilyLength);
+            index += fontFamilyLength;
+        }
+        else
+        {
+            FontFamily = null;
+        }
+
+        bool hasFontSize = data[index] == 1;
+        index++;
+        if (hasFontSize)
+        {
+            FontSize = BitConverter.ToDouble(data, index);
+            index += 8;
+        }
+        else
+        {
+            FontSize = null;
+        }
+
+        bool hasFontStyle = data[index] == 1;
+        index++;
+        if (hasFontStyle)
+        {
+            FontStyle = (FontStyle)data[index];
+            index++;
+        }
+        else
+        {
+            FontStyle = null;
+        }
+
+        bool hasFontWeight = data[index] == 1;
+        index++;
+        if (hasFontWeight)
+        {
+            FontWeight = (FontWeight)BitConverter.ToInt32(data, index);
+            index += 4;
+        }
+        else
+        {
+            FontWeight = null;
+        }
+
+        bool hasColor = data[index] == 1;
+        index++;
+        if (hasColor)
+        {
+            Color = Properties.Color.FromBytes(data[index..]);
+        }
+        else
+        {
+            Color = null;
+        }
+    }
+}
+
+public enum FontStyle
+{
+    Normal,
+    Italic,
+    Oblique
+}
+
+public enum FontWeight
+{
+    Thin = 100,
+    ExtraLight = 200,
+    Light = 300,
+    Normal = 400,
+    Medium = 500,
+    SemiBold = 600,
+    Bold = 700,
+    ExtraBold = 800,
+    Black = 900
+}

+ 4 - 0
src/PixiEditor.Extensions.CommonApi/Palettes/FilteringSettings.Impl.cs

@@ -2,6 +2,10 @@
 
 public partial class FilteringSettings
 {
+    public FilteringSettings()
+    {
+
+    }
     public FilteringSettings(ColorsNumberMode colorsNumberMode, int colorsCount, string name, bool showOnlyFavourites, List<string> favourites)
     {
         ColorsNumberMode = colorsNumberMode;

+ 6 - 7
src/PixiEditor.Extensions.Sdk/Api/FlyUI/Align.cs

@@ -7,21 +7,20 @@ public class Align : SingleChildLayoutElement
 {
     public Alignment Alignment { get; set; }
 
-    public Align(Alignment alignment = Alignment.TopLeft, LayoutElement child = null)
+    public Align(Alignment alignment = Alignment.TopLeft, LayoutElement child = null, Cursor? cursor = null) : base(cursor)
     {
         Child = child;
         Alignment = alignment;
     }
 
-    public override CompiledControl BuildNative()
+    protected override ControlDefinition CreateControl()
     {
-        CompiledControl control = new CompiledControl(UniqueId, "Align");
-        control.AddProperty((int)Alignment);
+        ControlDefinition controlDefinition = new ControlDefinition(UniqueId, "Align");
+        controlDefinition.AddProperty((int)Alignment);
         
         if (Child != null)
-            control.AddChild(Child.BuildNative());
+            controlDefinition.AddChild(Child.BuildNative());
 
-        BuildPendingEvents(control);
-        return control;
+        return controlDefinition;
     }
 }

+ 2 - 1
src/PixiEditor.Extensions.Sdk/Api/FlyUI/AxisAlignment.cs

@@ -14,5 +14,6 @@ public enum CrossAxisAlignment
 {
     Start,
     Center,
-    End
+    End,
+    Stretch,
 }

+ 22 - 17
src/PixiEditor.Extensions.Sdk/Api/FlyUI/Border.cs

@@ -1,4 +1,5 @@
-using PixiEditor.Extensions.CommonApi.FlyUI.Properties;
+using PixiEditor.Extensions.CommonApi.FlyUI;
+using PixiEditor.Extensions.CommonApi.FlyUI.Properties;
 
 namespace PixiEditor.Extensions.Sdk.Api.FlyUI;
 
@@ -20,8 +21,9 @@ public class Border : SingleChildLayoutElement
     public double Height { get; set; }
 
     public Border(LayoutElement child = null, Color color = default, Edges thickness = default,
-        Edges cornerRadius = default, Edges padding = default, Edges margin = default, double width = -1, double height = -1,
-        Color backgroundColor = default)
+        Edges cornerRadius = default, Edges padding = default, Edges margin = default, double width = -1,
+        double height = -1,
+        Color backgroundColor = default, Cursor? cursor = null) : base(cursor)
     {
         Child = child;
         Color = color;
@@ -34,20 +36,23 @@ public class Border : SingleChildLayoutElement
         BackgroundColor = backgroundColor;
     }
 
-    public override CompiledControl BuildNative()
+    protected override ControlDefinition CreateControl()
     {
-        CompiledControl control = new(UniqueId, "Border");
-        control.Children.Add(Child.BuildNative());
-
-        control.AddProperty(Color);
-        control.AddProperty(Thickness);
-        control.AddProperty(CornerRadius);
-        control.AddProperty(Padding);
-        control.AddProperty(Margin);
-        control.AddProperty(Width);
-        control.AddProperty(Height);
-        control.AddProperty(BackgroundColor);
-
-        return control;
+        ControlDefinition controlDefinition = new(UniqueId, "Border");
+        if (Child != null)
+        {
+            controlDefinition.Children.Add(Child.BuildNative());
+        }
+
+        controlDefinition.AddProperty(Color);
+        controlDefinition.AddProperty(Thickness);
+        controlDefinition.AddProperty(CornerRadius);
+        controlDefinition.AddProperty(Padding);
+        controlDefinition.AddProperty(Margin);
+        controlDefinition.AddProperty(Width);
+        controlDefinition.AddProperty(Height);
+        controlDefinition.AddProperty(BackgroundColor);
+
+        return controlDefinition;
     }
 }

+ 3 - 4
src/PixiEditor.Extensions.Sdk/Api/FlyUI/Button.cs

@@ -11,20 +11,19 @@ public class Button : SingleChildLayoutElement
         remove => RemoveEvent(nameof(Click), value);
     }
 
-    public Button(ILayoutElement<CompiledControl> child = null, ElementEventHandler onClick = null)
+    public Button(ILayoutElement<ControlDefinition> child = null, ElementEventHandler onClick = null, Cursor? cursor = null) : base(cursor)
     {
         Child = child;
         if (onClick != null)
             Click += onClick;
     }
 
-    public override CompiledControl BuildNative()
+    protected override ControlDefinition CreateControl()
     {
-        CompiledControl button = new CompiledControl(UniqueId, "Button");
+        ControlDefinition button = new ControlDefinition(UniqueId, "Button");
         if (Child != null)
             button.AddChild(Child.BuildNative());
 
-        BuildPendingEvents(button);
         return button;
     }
 }

+ 3 - 4
src/PixiEditor.Extensions.Sdk/Api/FlyUI/Center.cs

@@ -4,19 +4,18 @@ namespace PixiEditor.Extensions.Sdk.Api.FlyUI;
 
 public class Center : SingleChildLayoutElement
 {
-    public Center(ILayoutElement<CompiledControl> child)
+    public Center(ILayoutElement<ControlDefinition> child, Cursor? cursor = null) : base(cursor)
     {
         Child = child;
     }
 
-    public override CompiledControl BuildNative()
+    protected override ControlDefinition CreateControl()
     {
-        CompiledControl center = new CompiledControl(UniqueId, "Center");
+        ControlDefinition center = new ControlDefinition(UniqueId, "Center");
 
         if (Child != null)
             center.AddChild(Child.BuildNative());
 
-        BuildPendingEvents(center);
         return center;
     }
 }

+ 3 - 4
src/PixiEditor.Extensions.Sdk/Api/FlyUI/CheckBox.cs

@@ -13,7 +13,7 @@ public class CheckBox : SingleChildLayoutElement
 
     public bool IsChecked { get; set; }
 
-    public CheckBox(ILayoutElement<CompiledControl> child = null, ElementEventHandler onCheckedChanged = null)
+    public CheckBox(ILayoutElement<ControlDefinition> child = null, ElementEventHandler onCheckedChanged = null, Cursor? cursor = null) : base(cursor)
     {
         Child = child;
         
@@ -32,13 +32,12 @@ public class CheckBox : SingleChildLayoutElement
     }
 
 
-    public override CompiledControl BuildNative()
+    protected override ControlDefinition CreateControl()
     {
-        CompiledControl checkbox = new CompiledControl(UniqueId, "CheckBox");
+        ControlDefinition checkbox = new ControlDefinition(UniqueId, "CheckBox");
         if (Child != null)
             checkbox.AddChild(Child.BuildNative());
 
-        BuildPendingEvents(checkbox);
         return checkbox;
     }
 }

+ 10 - 8
src/PixiEditor.Extensions.Sdk/Api/FlyUI/Column.cs

@@ -1,4 +1,6 @@
-namespace PixiEditor.Extensions.Sdk.Api.FlyUI;
+using PixiEditor.Extensions.CommonApi.FlyUI;
+
+namespace PixiEditor.Extensions.Sdk.Api.FlyUI;
 
 public class Column : MultiChildLayoutElement
 {
@@ -8,7 +10,7 @@ public class Column : MultiChildLayoutElement
     public Column(
         MainAxisAlignment mainAxisAlignment = MainAxisAlignment.Start,
         CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.Start,
-        LayoutElement[] children = null)
+        LayoutElement[] children = null, Cursor? cursor = null) : base(cursor)
     {
         MainAxisAlignment = mainAxisAlignment;
         CrossAxisAlignment = crossAxisAlignment;
@@ -22,13 +24,13 @@ public class Column : MultiChildLayoutElement
         Children = new List<LayoutElement>(children);
     }
 
-    public override CompiledControl BuildNative()
+    protected override ControlDefinition CreateControl()
     {
-        CompiledControl control = new CompiledControl(UniqueId, "Column");
-        control.AddProperty(MainAxisAlignment);
-        control.AddProperty(CrossAxisAlignment);
-        control.Children.AddRange(Children.Where(x => x != null).Select(x => x.BuildNative()));
+        ControlDefinition controlDefinition = new ControlDefinition(UniqueId, "Column");
+        controlDefinition.AddProperty(MainAxisAlignment);
+        controlDefinition.AddProperty(CrossAxisAlignment);
+        controlDefinition.Children.AddRange(Children.Where(x => x != null).Select(x => x.BuildNative()));
 
-        return control;
+        return controlDefinition;
     }
 }

+ 5 - 5
src/PixiEditor.Extensions.Sdk/Api/FlyUI/Container.cs

@@ -1,4 +1,5 @@
-using PixiEditor.Extensions.CommonApi.FlyUI.Properties;
+using PixiEditor.Extensions.CommonApi.FlyUI;
+using PixiEditor.Extensions.CommonApi.FlyUI.Properties;
 
 namespace PixiEditor.Extensions.Sdk.Api.FlyUI;
 
@@ -10,7 +11,7 @@ public class Container : SingleChildLayoutElement
     public double Width { get; set; }
     public double Height { get; set; }
 
-    public Container(LayoutElement child = null, Edges margin = default, Color backgroundColor = default, double width = -1, double height = -1)
+    public Container(LayoutElement child = null, Edges margin = default, Color backgroundColor = default, double width = -1, double height = -1, Cursor? cursor = null) : base(cursor)
     {
         Margin = margin;
         BackgroundColor = backgroundColor;
@@ -19,9 +20,9 @@ public class Container : SingleChildLayoutElement
         Child = child;
     }
     
-    public override CompiledControl BuildNative()
+    protected override ControlDefinition CreateControl()
     {
-        CompiledControl container = new CompiledControl(UniqueId, "Container");
+        ControlDefinition container = new ControlDefinition(UniqueId, "Container");
         container.AddProperty(Margin);
 
         container.AddProperty(BackgroundColor);
@@ -34,7 +35,6 @@ public class Container : SingleChildLayoutElement
             container.AddChild(Child.BuildNative());
         }
 
-        BuildPendingEvents(container);
         return container;
     }
 }

+ 26 - 7
src/PixiEditor.Extensions.Sdk/Api/FlyUI/CompiledControl.cs → src/PixiEditor.Extensions.Sdk/Api/FlyUI/ControlDefinition.cs

@@ -6,17 +6,17 @@ using PixiEditor.Extensions.CommonApi.FlyUI.Properties;
 
 namespace PixiEditor.Extensions.Sdk.Api.FlyUI;
 
-public class CompiledControl
+public class ControlDefinition
 {
     public string ControlTypeId { get; set; }
     public List<(object value, Type type)> Properties { get; set; } = new();
-    public List<CompiledControl> Children { get; set; } = new();
+    public List<ControlDefinition> Children { get; set; } = new();
     public int UniqueId { get; set; }
     internal List<string> QueuedEvents => _buildQueuedEvents;
 
     private List<string> _buildQueuedEvents = new List<string>();
 
-    public CompiledControl(int uniqueId, string controlTypeId)
+    public ControlDefinition(int uniqueId, string controlTypeId)
     {
         ControlTypeId = controlTypeId;
         UniqueId = uniqueId;
@@ -27,8 +27,27 @@ public class CompiledControl
         InternalAddProperty(value);
     }
 
+    public void InsertProperty<T>(int at, T value)
+    {
+        InternalAddProperty(value);
+        if (at < 0 || at >= Properties.Count)
+        {
+            throw new ArgumentOutOfRangeException(nameof(at), "Index out of range");
+        }
+
+        var property = Properties[^1];
+        Properties.RemoveAt(Properties.Count - 1);
+        Properties.Insert(at, property);
+    }
+
     private void InternalAddProperty(object value)
     {
+        if (value is null)
+        {
+            Properties.Add((null, null));
+            return;
+        }
+
         if (value is string s)
         {
             AddStringProperty(s);
@@ -53,7 +72,7 @@ public class CompiledControl
         Properties.Add((value, typeof(string)));
     }
 
-    public void AddChild(CompiledControl child)
+    public void AddChild(ControlDefinition child)
     {
         Children.Add(child);
     }
@@ -89,7 +108,7 @@ public class CompiledControl
 
     private void SerializeChildren(List<byte> bytes)
     {
-        foreach (CompiledControl child in Children)
+        foreach (ControlDefinition child in Children)
         {
             child.Serialize(bytes);
         }
@@ -132,10 +151,10 @@ public class CompiledControl
         bytes.AddRange(Encoding.UTF8.GetBytes(structProperty.GetType().Name));
 
         byte[] structBytes = structProperty.Serialize();
-        
+
         bytes.AddRange(BitConverter.GetBytes(structBytes.Length));
         bytes.AddRange(structBytes);
-        
+
         return bytes;
     }
 

+ 6 - 8
src/PixiEditor.Extensions.Sdk/Api/FlyUI/Hyperlink.cs

@@ -1,4 +1,5 @@
-using PixiEditor.Extensions.CommonApi.FlyUI.Properties;
+using PixiEditor.Extensions.CommonApi.FlyUI;
+using PixiEditor.Extensions.CommonApi.FlyUI.Properties;
 
 namespace PixiEditor.Extensions.Sdk.Api.FlyUI;
 
@@ -6,22 +7,19 @@ public class Hyperlink : Text
 {
     public string Url { get; set; }
 
-    public Hyperlink(string url, string text, TextWrap textWrap = TextWrap.None, FontStyle fontStyle = FontStyle.Normal,
-        double fontSize = 12) : base(text, textWrap, fontStyle, fontSize)
+    public Hyperlink(string url, string text, TextWrap textWrap = TextWrap.None, TextStyle? textStyle = null, Cursor? cursor = null) : base(text, textWrap, textStyle, cursor)
     {
         Url = url;
     }
 
-    public override CompiledControl BuildNative()
+    protected override ControlDefinition CreateControl()
     {
-        CompiledControl hyperlink = new CompiledControl(UniqueId, "Hyperlink");
+        ControlDefinition hyperlink = new ControlDefinition(UniqueId, "Hyperlink");
         hyperlink.AddProperty(Value);
         hyperlink.AddProperty(TextWrap);
-        hyperlink.AddProperty(FontStyle);
-        hyperlink.AddProperty(FontSize);
+        hyperlink.AddProperty(TextStyle);
         hyperlink.AddProperty(Url);
 
-        BuildPendingEvents(hyperlink);
         return hyperlink;
     }
 }

+ 29 - 0
src/PixiEditor.Extensions.Sdk/Api/FlyUI/Icon.cs

@@ -0,0 +1,29 @@
+using PixiEditor.Extensions.CommonApi.FlyUI;
+using PixiEditor.Extensions.CommonApi.FlyUI.Properties;
+
+namespace PixiEditor.Extensions.Sdk.Api.FlyUI;
+
+public class Icon : LayoutElement
+{
+    public string IconName { get; set; }
+    public double Size { get; set; } = 16;
+    public Color Color { get; set; } = Colors.White;
+
+    public Icon(string iconName, double size = 16, Color? color = null, Cursor? cursor = null) : base(cursor)
+    {
+        IconName = iconName;
+        Size = size;
+        if (color != null)
+            Color = color.Value;
+    }
+
+    protected override ControlDefinition CreateControl()
+    {
+        ControlDefinition icon = new ControlDefinition(UniqueId, "Icon");
+        icon.AddProperty(IconName);
+        icon.AddProperty(Size);
+        icon.AddProperty(Color);
+
+        return icon;
+    }
+}

+ 6 - 6
src/PixiEditor.Extensions.Sdk/Api/FlyUI/Image.cs

@@ -1,9 +1,10 @@
-using PixiEditor.Extensions.CommonApi.FlyUI.Properties;
+using PixiEditor.Extensions.CommonApi.FlyUI;
+using PixiEditor.Extensions.CommonApi.FlyUI.Properties;
 using PixiEditor.Extensions.Sdk.Bridge;
 
 namespace PixiEditor.Extensions.Sdk.Api.FlyUI;
 
-public class Image : StatelessElement
+public class Image : LayoutElement
 {
     private string source = null!;
 
@@ -28,7 +29,7 @@ public class Image : StatelessElement
     public FillMode FillMode { get; set; }
     public FilterQuality FilterQuality { get; set; }
 
-    public Image(string source, double width = -1, double height = -1, FillMode fillMode = FillMode.Uniform, FilterQuality filterQuality = FilterQuality.Unspecified)
+    public Image(string source, double width = -1, double height = -1, FillMode fillMode = FillMode.Uniform, FilterQuality filterQuality = FilterQuality.Unspecified, Cursor? cursor = null) : base(cursor)
     {
         Source = source;
         Width = width;
@@ -37,9 +38,9 @@ public class Image : StatelessElement
         FilterQuality = filterQuality;
     }
 
-    public override CompiledControl BuildNative()
+    protected override ControlDefinition CreateControl()
     {
-        CompiledControl image = new CompiledControl(UniqueId, "Image");
+        ControlDefinition image = new ControlDefinition(UniqueId, "Image");
         
         image.AddProperty(Source);
         image.AddProperty(Width);
@@ -47,7 +48,6 @@ public class Image : StatelessElement
         image.AddProperty(FillMode);
         image.AddProperty(FilterQuality);
         
-        BuildPendingEvents(image);
         return image;
     }
 }

+ 3 - 4
src/PixiEditor.Extensions.Sdk/Api/FlyUI/Layout.cs

@@ -4,19 +4,18 @@ namespace PixiEditor.Extensions.Sdk.Api.FlyUI;
 
 public sealed class Layout : SingleChildLayoutElement
 {
-    public Layout(ILayoutElement<CompiledControl> body = null)
+    public Layout(ILayoutElement<ControlDefinition> body = null)
     {
         Child = body;
     }
 
-    public override CompiledControl BuildNative()
+    protected override ControlDefinition CreateControl()
     {
-        CompiledControl layout = new CompiledControl(UniqueId, "Layout");
+        ControlDefinition layout = new ControlDefinition(UniqueId, "Layout");
 
         if (Child != null)
             layout.AddChild(Child.BuildNative());
 
-        BuildPendingEvents(layout);
         return layout;
     }
 

+ 44 - 8
src/PixiEditor.Extensions.Sdk/Api/FlyUI/LayoutElement.cs

@@ -3,20 +3,56 @@ using PixiEditor.Extensions.CommonApi.FlyUI.Events;
 
 namespace PixiEditor.Extensions.Sdk.Api.FlyUI;
 
-public abstract class LayoutElement : ILayoutElement<CompiledControl>
+public abstract class LayoutElement : ILayoutElement<ControlDefinition>
 {
     private Dictionary<string, List<ElementEventHandler>> _events;
     public List<string> BuildQueuedEvents = new List<string>();
     public int UniqueId { get; set; }
 
-    public abstract CompiledControl BuildNative();
+    public event ElementEventHandler PointerEnter
+    {
+        add => AddEvent(nameof(PointerEnter), value);
+        remove => RemoveEvent(nameof(PointerEnter), value);
+    }
+
+    public event ElementEventHandler PointerLeave
+    {
+        add => AddEvent(nameof(PointerLeave), value);
+        remove => RemoveEvent(nameof(PointerLeave), value);
+    }
 
-    public LayoutElement()
+    public event ElementEventHandler PointerPressed
     {
+        add => AddEvent(nameof(PointerPressed), value);
+        remove => RemoveEvent(nameof(PointerPressed), value);
+    }
+
+    public event ElementEventHandler PointerReleased
+    {
+        add => AddEvent(nameof(PointerReleased), value);
+        remove => RemoveEvent(nameof(PointerReleased), value);
+    }
+
+    public Cursor? Cursor { get; set; }
+
+    public LayoutElement(Cursor? cursor)
+    {
+        Cursor = cursor;
         UniqueId = LayoutElementIdGenerator.GetNextId();
         LayoutElementsStore.AddElement(UniqueId, this);
     }
 
+    public virtual ControlDefinition BuildNative()
+    {
+        ControlDefinition control = CreateControl();
+
+        control.InsertProperty(0, Cursor);
+        BuildPendingEvents(control);
+        return control;
+    }
+
+    protected abstract ControlDefinition CreateControl();
+
     ~LayoutElement()
     {
         LayoutElementsStore.RemoveElement(UniqueId);
@@ -37,7 +73,7 @@ public abstract class LayoutElement : ILayoutElement<CompiledControl>
         _events[eventName].Add(eventHandler);
         BuildQueuedEvents.Add(eventName);
     }
-    
+
     /*public void AddEvent<TEventArgs>(string eventName, ElementEventHandler<TEventArgs> eventHandler) where TEventArgs : ElementEventArgs<TEventArgs>
     {
         if (_events == null)
@@ -50,7 +86,7 @@ public abstract class LayoutElement : ILayoutElement<CompiledControl>
             _events.Add(eventName, new List<ElementEventHandler>());
         }
 
-        _events[eventName].Add((args => eventHandler((TEventArgs)args))); 
+        _events[eventName].Add((args => eventHandler((TEventArgs)args)));
         BuildQueuedEvents.Add(eventName);
     }*/
 
@@ -68,7 +104,7 @@ public abstract class LayoutElement : ILayoutElement<CompiledControl>
 
         _events[eventName].Remove(eventHandler);
     }
-    
+
     /*public void RemoveEvent<TEventArgs>(string eventName, ElementEventHandler<TEventArgs> eventHandler) where TEventArgs : ElementEventArgs<TEventArgs>
     {
         if (_events == null)
@@ -102,11 +138,11 @@ public abstract class LayoutElement : ILayoutElement<CompiledControl>
         }
     }
 
-    protected void BuildPendingEvents(CompiledControl control)
+    protected void BuildPendingEvents(ControlDefinition controlDefinition)
     {
         foreach (string eventName in BuildQueuedEvents)
         {
-            control.QueuedEvents.Add(eventName);
+            controlDefinition.QueuedEvents.Add(eventName);
         }
     }
 }

+ 2 - 2
src/PixiEditor.Extensions.Sdk/Api/FlyUI/LayoutElementsStore.cs

@@ -4,9 +4,9 @@ namespace PixiEditor.Extensions.Sdk.Api.FlyUI;
 
 internal static class LayoutElementsStore
 {
-    public static Dictionary<int, ILayoutElement<CompiledControl>> LayoutElements { get; } = new();
+    public static Dictionary<int, ILayoutElement<ControlDefinition>> LayoutElements { get; } = new();
 
-    public static void AddElement(int internalId, ILayoutElement<CompiledControl> element)
+    public static void AddElement(int internalId, ILayoutElement<ControlDefinition> element)
     {
         LayoutElements.Add(internalId, element);
     }

+ 6 - 5
src/PixiEditor.Extensions.Sdk/Api/FlyUI/MultiChildLayoutElement.cs

@@ -2,16 +2,17 @@
 
 namespace PixiEditor.Extensions.Sdk.Api.FlyUI;
 
-public abstract class MultiChildLayoutElement : LayoutElement, IMultiChildLayoutElement<CompiledControl>
+public abstract class MultiChildLayoutElement : LayoutElement, IMultiChildLayoutElement<ControlDefinition>
 {
-    List<ILayoutElement<CompiledControl>> IMultiChildLayoutElement<CompiledControl>.Children
+    List<ILayoutElement<ControlDefinition>> IMultiChildLayoutElement<ControlDefinition>.Children
     {
-        get => Children.Cast<ILayoutElement<CompiledControl>>().ToList();
+        get => Children.Cast<ILayoutElement<ControlDefinition>>().ToList();
         set => Children = value.Cast<LayoutElement>().ToList();
     }
 
     public List<LayoutElement> Children { get; set; }
 
-    public abstract override CompiledControl BuildNative();
-
+    public MultiChildLayoutElement(Cursor? cursor = null) : base(cursor)
+    {
+    }
 }

+ 9 - 8
src/PixiEditor.Extensions.Sdk/Api/FlyUI/Padding.cs

@@ -1,4 +1,5 @@
-using PixiEditor.Extensions.CommonApi.FlyUI.Properties;
+using PixiEditor.Extensions.CommonApi.FlyUI;
+using PixiEditor.Extensions.CommonApi.FlyUI.Properties;
 
 namespace PixiEditor.Extensions.Sdk.Api.FlyUI;
 
@@ -6,19 +7,19 @@ public class Padding : SingleChildLayoutElement
 {
     public Edges Edges { get; set; } = Edges.All(0);
     
-    public Padding(LayoutElement child = null, Edges edges = default)
+    public Padding(LayoutElement child = null, Edges edges = default, Cursor? cursor = null) : base(cursor)
     {
         Edges = edges;
         Child = child;
     }
-    
-    public override CompiledControl BuildNative()
+
+    protected override ControlDefinition CreateControl()
     {
-        CompiledControl control = new CompiledControl(UniqueId, "Padding");
-        control.Children.Add(Child.BuildNative());
+        ControlDefinition controlDefinition = new ControlDefinition(UniqueId, "Padding");
+        controlDefinition.Children.Add(Child.BuildNative());
         
-        control.AddProperty(Edges);
+        controlDefinition.AddProperty(Edges);
 
-        return control;
+        return controlDefinition;
     }
 }

+ 10 - 8
src/PixiEditor.Extensions.Sdk/Api/FlyUI/Row.cs

@@ -1,4 +1,6 @@
-namespace PixiEditor.Extensions.Sdk.Api.FlyUI;
+using PixiEditor.Extensions.CommonApi.FlyUI;
+
+namespace PixiEditor.Extensions.Sdk.Api.FlyUI;
 
 public class Row : MultiChildLayoutElement
 {
@@ -15,20 +17,20 @@ public class Row : MultiChildLayoutElement
     public Row(
         MainAxisAlignment mainAxisAlignment = MainAxisAlignment.Start,
         CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.Start,
-        LayoutElement[] children = null)
+        LayoutElement[] children = null, Cursor? cursor = null) : base(cursor)
     {
         MainAxisAlignment = mainAxisAlignment;
         CrossAxisAlignment = crossAxisAlignment;
         Children = new List<LayoutElement>(children);
     }
 
-    public override CompiledControl BuildNative()
+    protected override ControlDefinition CreateControl()
     {
-        CompiledControl control = new CompiledControl(UniqueId, "Row");
-        control.AddProperty(MainAxisAlignment);
-        control.AddProperty(CrossAxisAlignment);
-        control.Children.AddRange(Children.Select(x => x.BuildNative()));
+        ControlDefinition controlDefinition = new ControlDefinition(UniqueId, "Row");
+        controlDefinition.AddProperty(MainAxisAlignment);
+        controlDefinition.AddProperty(CrossAxisAlignment);
+        controlDefinition.Children.AddRange(Children.Select(x => x.BuildNative()));
 
-        return control;
+        return controlDefinition;
     }
 }

+ 6 - 3
src/PixiEditor.Extensions.Sdk/Api/FlyUI/SingleChildLayoutElement.cs

@@ -2,8 +2,11 @@
 
 namespace PixiEditor.Extensions.Sdk.Api.FlyUI;
 
-public abstract class SingleChildLayoutElement : LayoutElement, ISingleChildLayoutElement<CompiledControl>
+public abstract class SingleChildLayoutElement : LayoutElement, ISingleChildLayoutElement<ControlDefinition>
 {
-    public ILayoutElement<CompiledControl> Child { get; set; }
-    public abstract override CompiledControl BuildNative();
+    public ILayoutElement<ControlDefinition> Child { get; set; }
+
+    public SingleChildLayoutElement(Cursor? cursor = null) : base(cursor)
+    {
+    }
 }

+ 2 - 2
src/PixiEditor.Extensions.Sdk/Api/FlyUI/State.cs

@@ -3,9 +3,9 @@ using PixiEditor.Extensions.CommonApi.FlyUI.State;
 
 namespace PixiEditor.Extensions.Sdk.Api.FlyUI;
 
-public abstract class State : IState<CompiledControl>
+public abstract class State : IState<ControlDefinition>
 {
-    ILayoutElement<CompiledControl> IState<CompiledControl>.Build()
+    ILayoutElement<ControlDefinition> IState<ControlDefinition>.Build()
     {
         return BuildElement();
     }

+ 15 - 9
src/PixiEditor.Extensions.Sdk/Api/FlyUI/StatefulElement.cs

@@ -1,12 +1,17 @@
-using PixiEditor.Extensions.CommonApi.FlyUI.State;
+using PixiEditor.Extensions.CommonApi.FlyUI;
+using PixiEditor.Extensions.CommonApi.FlyUI.State;
 
 namespace PixiEditor.Extensions.Sdk.Api.FlyUI;
 
-public abstract class StatefulElement<TState> : LayoutElement, IStatefulElement<CompiledControl, TState> where TState : IState<CompiledControl>
+public abstract class StatefulElement<TState> : LayoutElement, IStatefulElement<ControlDefinition, TState> where TState : IState<ControlDefinition>
 {
     private TState state;
 
-    IState<CompiledControl> IStatefulElement<CompiledControl>.State
+    protected StatefulElement() : base(null)
+    {
+    }
+
+    IState<ControlDefinition> IStatefulElement<ControlDefinition>.State
     {
         get
         {
@@ -15,7 +20,7 @@ public abstract class StatefulElement<TState> : LayoutElement, IStatefulElement<
                 state = CreateState();
                 state.StateChanged += () =>
                 {
-                    CompiledControl newLayout = BuildNative();
+                    ControlDefinition newLayout = BuildNative();
                     PixiEditorExtension.Api.WindowProvider.LayoutStateChanged(UniqueId, newLayout);
                 };
             }
@@ -24,13 +29,14 @@ public abstract class StatefulElement<TState> : LayoutElement, IStatefulElement<
         }
     }
 
-    public TState State => (TState)((IStatefulElement<CompiledControl>)this).State;
+    public TState State => (TState)((IStatefulElement<ControlDefinition>)this).State;
 
-    public override CompiledControl BuildNative()
+    protected override ControlDefinition CreateControl()
     {
-        CompiledControl control = State.Build().BuildNative();
-        CompiledControl statefulContainer = new CompiledControl(UniqueId, "StatefulContainer");
-        statefulContainer.Children.Add(control);
+        ControlDefinition controlDefinition = State.Build().BuildNative();
+        ControlDefinition statefulContainer = new ControlDefinition(UniqueId, "StatefulContainer");
+        statefulContainer.Children.Add(controlDefinition);
+
         return statefulContainer;
     }
 

+ 16 - 2
src/PixiEditor.Extensions.Sdk/Api/FlyUI/StatelessElement.cs

@@ -3,10 +3,24 @@ using PixiEditor.Extensions.CommonApi.FlyUI.State;
 
 namespace PixiEditor.Extensions.Sdk.Api.FlyUI;
 
-public abstract class StatelessElement : LayoutElement, IStatelessElement<CompiledControl>
+public abstract class StatelessElement : LayoutElement, IStatelessElement<ControlDefinition>
 {
-    public ILayoutElement<CompiledControl> Build()
+    protected StatelessElement() : base(null)
+    {
+    }
+
+    public virtual ILayoutElement<ControlDefinition> Build()
     {
         return this;
     }
+
+    public override ControlDefinition BuildNative()
+    {
+        return CreateControl();
+    }
+
+    protected override ControlDefinition CreateControl()
+    {
+        return Build().BuildNative();
+    }
 }

+ 10 - 14
src/PixiEditor.Extensions.Sdk/Api/FlyUI/Text.cs

@@ -1,34 +1,30 @@
-using PixiEditor.Extensions.CommonApi.FlyUI.Properties;
+using PixiEditor.Extensions.CommonApi.FlyUI;
+using PixiEditor.Extensions.CommonApi.FlyUI.Properties;
 
 namespace PixiEditor.Extensions.Sdk.Api.FlyUI;
 
-public class Text : StatelessElement
+public class Text : LayoutElement
 {
     public string Value { get; set; }
     
     public TextWrap TextWrap { get; set; }
+
+    public TextStyle TextStyle { get; set; }
     
-    public FontStyle FontStyle { get; set; }
-    
-    public double FontSize { get; set; }
-    
-    public Text(string value, TextWrap wrap = TextWrap.None, FontStyle fontStyle = FontStyle.Normal, double fontSize = 12)
+    public Text(string value, TextWrap wrap = TextWrap.None, TextStyle? textStyle = null, Cursor? cursor = null) : base(cursor)
     {
         Value = value;
         TextWrap = wrap;
-        FontStyle = fontStyle;
-        FontSize = fontSize;
+        TextStyle = textStyle ?? TextStyle.Default;
     }
 
-    public override CompiledControl BuildNative()
+    protected override ControlDefinition CreateControl()
     {
-        CompiledControl text = new CompiledControl(UniqueId, "Text");
+        ControlDefinition text = new ControlDefinition(UniqueId, "Text");
         text.AddProperty(Value);
         text.AddProperty(TextWrap);
-        text.AddProperty(FontStyle);
-        text.AddProperty(FontSize);
+        text.AddProperty(TextStyle);
 
-        BuildPendingEvents(text);
         return text;
     }
 }

+ 6 - 6
src/PixiEditor.Extensions.Sdk/Api/Window/WindowProvider.cs

@@ -9,18 +9,18 @@ public class WindowProvider : IWindowProvider
 {
     public PopupWindow CreatePopupWindow(string title, LayoutElement body)
     {
-        CompiledControl compiledControl = body.BuildNative();
-        byte[] bytes = compiledControl.Serialize().ToArray();
+        ControlDefinition controlDefinition = body.BuildNative();
+        byte[] bytes = controlDefinition.Serialize().ToArray();
         IntPtr ptr = Marshal.AllocHGlobal(bytes.Length);
         Marshal.Copy(bytes, 0, ptr, bytes.Length);
         int handle = Native.create_popup_window(title, ptr, bytes.Length);
         Marshal.FreeHGlobal(ptr);
         
-        SubscribeToEvents(compiledControl);
+        SubscribeToEvents(controlDefinition);
         return new PopupWindow(handle);
     }
 
-    internal void LayoutStateChanged(int uniqueId, CompiledControl newLayout)
+    internal void LayoutStateChanged(int uniqueId, ControlDefinition newLayout)
     {
         byte[] bytes = newLayout.Serialize().ToArray();
         IntPtr ptr = Marshal.AllocHGlobal(bytes.Length);
@@ -31,9 +31,9 @@ public class WindowProvider : IWindowProvider
         SubscribeToEvents(newLayout);
     }
 
-    private void SubscribeToEvents(CompiledControl body)
+    private void SubscribeToEvents(ControlDefinition body)
     {
-        foreach (CompiledControl child in body.Children)
+        foreach (ControlDefinition child in body.Children)
         {
             SubscribeToEvents(child);
         }

+ 1 - 1
src/PixiEditor.Extensions.Sdk/Bridge/Native.cs

@@ -52,7 +52,7 @@ internal static partial class Native
     [ApiExport("raise_element_event")]
     internal static void EventRaised(int internalControlId, string eventName) //TOOD: Args
     {
-        if (LayoutElementsStore.LayoutElements.TryGetValue((int)internalControlId, out ILayoutElement<CompiledControl> element))
+        if (LayoutElementsStore.LayoutElements.TryGetValue((int)internalControlId, out ILayoutElement<ControlDefinition> element))
         {
             element.RaiseEvent(eventName ?? "", new ElementEventArgs { Sender = element });
         }

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


BIN
src/PixiEditor.Extensions.Sdk/build/PixiEditor.Extensions.MSPackageBuilder.dll


+ 28 - 0
src/PixiEditor.Extensions/FlyUI/Converters/IconLookupConverter.cs

@@ -0,0 +1,28 @@
+using System.Globalization;
+using Avalonia;
+using Avalonia.Data.Converters;
+
+namespace PixiEditor.Extensions.FlyUI.Converters;
+
+public class IconLookupConverter : IValueConverter
+{
+    public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+    {
+        if (value is not string iconName)
+        {
+            return null;
+        }
+
+        if (Application.Current.Styles.TryGetResource(iconName, null, out object resource))
+        {
+            return resource;
+        }
+
+        return iconName;
+    }
+
+    public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+    {
+        throw new NotImplementedException();
+    }
+}

+ 5 - 5
src/PixiEditor.Extensions/FlyUI/Elements/Align.cs

@@ -6,7 +6,7 @@ using PixiEditor.Extensions.CommonApi.FlyUI.Properties;
 
 namespace PixiEditor.Extensions.FlyUI.Elements;
 
-public class Align : SingleChildLayoutElement, IPropertyDeserializable
+public class Align : SingleChildLayoutElement
 {
     private Panel _panel; 
     public Alignment Alignment { get; set; }
@@ -17,7 +17,7 @@ public class Align : SingleChildLayoutElement, IPropertyDeserializable
         Alignment = alignment;
     }
 
-    public override Control BuildNative()
+    protected override Control CreateNativeControl()
     {
         _panel = new Panel
         {
@@ -76,12 +76,12 @@ public class Align : SingleChildLayoutElement, IPropertyDeserializable
         };
     }
 
-    public void DeserializeProperties(ImmutableList<object> values)
+    protected override void DeserializeControlProperties(List<object> values)
     {
         Alignment = (Alignment)values.FirstOrDefault();
     }
-    
-    IEnumerable<object> IPropertyDeserializable.GetProperties()
+
+    protected override IEnumerable<object> GetControlProperties()
     {
         yield return Alignment;
     }

+ 4 - 4
src/PixiEditor.Extensions/FlyUI/Elements/Border.cs

@@ -9,7 +9,7 @@ using PixiEditor.Extensions.FlyUI.Converters;
 
 namespace PixiEditor.Extensions.FlyUI.Elements;
 
-public class Border : SingleChildLayoutElement, IPropertyDeserializable
+public class Border : SingleChildLayoutElement
 {
     private Avalonia.Controls.Border border;
 
@@ -32,7 +32,7 @@ public class Border : SingleChildLayoutElement, IPropertyDeserializable
     public double Width { get => width; set => SetField(ref width, value); }
     public double Height { get => height; set => SetField(ref height, value); }
 
-    public override Control BuildNative()
+    protected override Control CreateNativeControl()
     {
         border = new Avalonia.Controls.Border();
 
@@ -99,7 +99,7 @@ public class Border : SingleChildLayoutElement, IPropertyDeserializable
         border.Child = null;
     }
 
-    public IEnumerable<object> GetProperties()
+    protected override IEnumerable<object> GetControlProperties()
     {
         yield return Color;
         yield return Thickness;
@@ -111,7 +111,7 @@ public class Border : SingleChildLayoutElement, IPropertyDeserializable
         yield return Height;
     }
 
-    public void DeserializeProperties(ImmutableList<object> values)
+    protected override void DeserializeControlProperties(List<object> values)
     {
         Color = (Color)values.ElementAtOrDefault(0, default(Color));
         Thickness = (Edges)values.ElementAtOrDefault(1, default(Edges));

+ 2 - 2
src/PixiEditor.Extensions/FlyUI/Elements/Button.cs

@@ -28,11 +28,11 @@ public class Button : SingleChildLayoutElement
         }
     }
 
-    public override Control BuildNative()
+    protected override Control CreateNativeControl()
     {
         _button = new Avalonia.Controls.Button();
         Binding binding = new Binding(nameof(Child)) { Source = this, Converter = LayoutElementToNativeControlConverter.Instance };
-        _button.Bind(Avalonia.Controls.Button.ContentProperty, binding);
+        _button.Bind(Avalonia.Controls.ContentControl.ContentProperty, binding);
 
         _button.Click += (sender, args) => RaiseEvent(nameof(Click), new ElementEventArgs() { Sender = this });
 

+ 1 - 1
src/PixiEditor.Extensions/FlyUI/Elements/Center.cs

@@ -16,7 +16,7 @@ public class Center : SingleChildLayoutElement
         Child = child;
     }
 
-    public override Control BuildNative()
+    protected override Control CreateNativeControl()
     {
         panel = new Panel()
         {

+ 1 - 1
src/PixiEditor.Extensions/FlyUI/Elements/CheckBox.cs

@@ -13,7 +13,7 @@ public class CheckBox : SingleChildLayoutElement
         remove => RemoveEvent(nameof(CheckedChanged), value);
     }
 
-    public override Control BuildNative()
+    protected override Control CreateNativeControl()
     {
         checkbox = new Avalonia.Controls.CheckBox();
         Binding binding =

+ 3 - 3
src/PixiEditor.Extensions/FlyUI/Elements/Column.cs

@@ -59,7 +59,7 @@ public class Column : MultiChildLayoutElement, IPropertyDeserializable
         });
     }
 
-    public override Control BuildNative()
+    protected override Control CreateNativeControl()
     {
         panel = new ColumnPanel() { MainAxisAlignment = MainAxisAlignment, CrossAxisAlignment = CrossAxisAlignment };
 
@@ -68,13 +68,13 @@ public class Column : MultiChildLayoutElement, IPropertyDeserializable
         return panel;
     }
 
-    public IEnumerable<object> GetProperties()
+    protected override IEnumerable<object> GetControlProperties()
     {
         yield return MainAxisAlignment;
         yield return CrossAxisAlignment;
     }
 
-    public void DeserializeProperties(ImmutableList<object> values)
+    protected override void DeserializeControlProperties(List<object> values)
     {
         if (values.Count < 2)
             return;

+ 3 - 3
src/PixiEditor.Extensions/FlyUI/Elements/Container.cs

@@ -22,7 +22,7 @@ public class Container : SingleChildLayoutElement, IPropertyDeserializable
     public double Width { get => _width; set => SetField(ref _width, value); }
     public double Height { get => _height; set => SetField(ref _height, value); }
     
-    public override Control BuildNative()
+    protected override Control CreateNativeControl()
     {
         _panel = new Panel();
         
@@ -77,7 +77,7 @@ public class Container : SingleChildLayoutElement, IPropertyDeserializable
         _panel.Children.Clear();
     }
 
-    public IEnumerable<object> GetProperties()
+    protected override IEnumerable<object> GetControlProperties()
     {
         yield return Margin;
 
@@ -87,7 +87,7 @@ public class Container : SingleChildLayoutElement, IPropertyDeserializable
         yield return Height;
     }
 
-    public void DeserializeProperties(ImmutableList<object> values)
+    protected override void DeserializeControlProperties(List<object> values)
     {
         Margin = (Edges)values.ElementAtOrDefault(0, default(Edges));
         BackgroundColor = (Color)values.ElementAtOrDefault(1, default(Color));

+ 8 - 10
src/PixiEditor.Extensions/FlyUI/Elements/Hyperlink.cs

@@ -11,15 +11,14 @@ public class Hyperlink : Text
 {
     public string Url { get; set; }
 
-    public Hyperlink(string text, string url, TextWrap textWrap = TextWrap.None, FontStyle fontStyle = FontStyle.Normal,
-        double fontSize = 12) : base(text, textWrap, fontStyle, fontSize)
+    public Hyperlink(string text, string url, TextWrap textWrap = TextWrap.None, TextStyle textStyle = default) : base(text, textWrap, textStyle)
     {
         Url = url;
     }
 
-    public override Control BuildNative()
+    protected override Control CreateNativeControl()
     {
-        TextBlock hyperlink = (TextBlock)base.BuildNative();
+        TextBlock hyperlink = (TextBlock)base.CreateNativeControl();
 
         Binding urlBinding = new Binding() { Source = this, Path = nameof(Url), };
 
@@ -28,18 +27,17 @@ public class Hyperlink : Text
         return hyperlink;
     }
 
-    public override IEnumerable<object> GetProperties()
+    protected override IEnumerable<object> GetControlProperties()
     {
         yield return Value;
         yield return TextWrap;
-        yield return FontStyle;
-        yield return FontSize;
+        yield return TextStyle;
         yield return Url;
     }
 
-    public override void DeserializeProperties(ImmutableList<object> values)
+    protected override void DeserializeControlProperties(List<object> values)
     {
-        base.DeserializeProperties(values);
-        Url = (string)values[4];
+        base.DeserializeControlProperties(values);
+        Url = (string)values[3];
     }
 }

+ 66 - 0
src/PixiEditor.Extensions/FlyUI/Elements/Icon.cs

@@ -0,0 +1,66 @@
+using System.Collections.Immutable;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Data;
+using Avalonia.Markup.Xaml.MarkupExtensions;
+using Avalonia.Media;
+using PixiEditor.Extensions.FlyUI.Converters;
+using Color = PixiEditor.Extensions.CommonApi.FlyUI.Properties.Color;
+using Colors = PixiEditor.Extensions.CommonApi.FlyUI.Properties.Colors;
+
+namespace PixiEditor.Extensions.FlyUI.Elements;
+
+public class Icon : LayoutElement
+{
+    private double size = 16;
+    private string iconName = string.Empty;
+    private Color color;
+
+    public double Size { get => size; set => SetField(ref size, value); }
+    public string IconName { get => iconName; set => SetField(ref iconName, value); }
+    public Color Color { get => color; set => SetField(ref color, value); }
+
+    public Icon(string iconName, double size = 16, Color? color = null)
+    {
+        IconName = iconName;
+        Size = size;
+        Color = color ?? Colors.White;
+    }
+
+    protected override Control CreateNativeControl()
+    {
+        TextBlock textBlock = new TextBlock();
+        textBlock.Classes.Add("pixi-icon");
+
+        Binding iconNameBinding = new Binding()
+        {
+            Source = this, Path = nameof(IconName),
+            Converter = new IconLookupConverter()
+        };
+        Binding sizeBinding = new Binding() { Source = this, Path = nameof(Size), };
+        Binding colorBinding = new Binding()
+        {
+            Source = this, Path = nameof(Color), Converter = new ColorToAvaloniaBrushConverter()
+        };
+
+        textBlock.Bind(TextBlock.TextProperty, iconNameBinding);
+        textBlock.Bind(TextBlock.FontSizeProperty, sizeBinding);
+        textBlock.Bind(TextBlock.ForegroundProperty, colorBinding);
+
+        return textBlock;
+    }
+
+    protected override IEnumerable<object> GetControlProperties()
+    {
+        yield return IconName;
+        yield return Size;
+        yield return Color;
+    }
+
+    protected override void DeserializeControlProperties(List<object> values)
+    {
+        IconName = (string)values[0];
+        Size = (double)values[1];
+        Color = (Color)values[2];
+    }
+}

+ 4 - 4
src/PixiEditor.Extensions/FlyUI/Elements/Image.cs

@@ -12,7 +12,7 @@ using PixiEditor.Extensions.UI;
 
 namespace PixiEditor.Extensions.FlyUI.Elements;
 
-public class Image : StatelessElement, IPropertyDeserializable
+public class Image : LayoutElement
 {
     private string _source = null!;
     private double _width = -1;
@@ -38,7 +38,7 @@ public class Image : StatelessElement, IPropertyDeserializable
         }
     }
     
-    public override Control BuildNative()
+    protected override Control CreateNativeControl()
     {
         _image = new();
         
@@ -78,7 +78,7 @@ public class Image : StatelessElement, IPropertyDeserializable
     }
 
 
-    public IEnumerable<object> GetProperties()
+    protected override IEnumerable<object> GetControlProperties()
     {
         yield return Source;
         
@@ -89,7 +89,7 @@ public class Image : StatelessElement, IPropertyDeserializable
         yield return FilterQuality;
     }
 
-    public void DeserializeProperties(ImmutableList<object> values)
+    protected override void DeserializeControlProperties(List<object> values)
     {
         var valuesList = values.ToList();
         Source = (string)valuesList.ElementAtOrDefault(0);

+ 1 - 1
src/PixiEditor.Extensions/FlyUI/Elements/Layout.cs

@@ -15,7 +15,7 @@ public class Layout : SingleChildLayoutElement
         Child = body;
     }
 
-    public override Control BuildNative()
+    protected override Control CreateNativeControl()
     {
         panel = new Panel();
         if (Child != null)

+ 5 - 1
src/PixiEditor.Extensions/FlyUI/Elements/LayoutBuilder.cs

@@ -93,6 +93,10 @@ public class LayoutBuilder
                 
                 properties.Add(prop);
             }
+            else if (type == null)
+            {
+                properties.Add(null);
+            }
             else
             {
                 var property = SpanUtility.Read(type, layoutSpan, ref offset);
@@ -127,7 +131,7 @@ public class LayoutBuilder
 
         if (element is IPropertyDeserializable deserializableProperties)
         {
-            deserializableProperties.DeserializeProperties(properties.ToImmutableList());
+            deserializableProperties.DeserializeProperties(properties);
         }
 
         if (element is IChildHost customChildrenDeserializable)

+ 91 - 5
src/PixiEditor.Extensions/FlyUI/Elements/LayoutElement.cs

@@ -1,17 +1,80 @@
-using System.ComponentModel;
+using System.Collections.Immutable;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
 using System.Runtime.CompilerServices;
 using Avalonia.Controls;
+using Avalonia.Data;
+using Avalonia.Input;
 using PixiEditor.Extensions.CommonApi.FlyUI;
 using PixiEditor.Extensions.CommonApi.FlyUI.Events;
+using PixiEditor.Extensions.FlyUI.Converters;
+using Cursor = PixiEditor.Extensions.CommonApi.FlyUI.Cursor;
 
 namespace PixiEditor.Extensions.FlyUI.Elements;
 
-public abstract class LayoutElement : ILayoutElement<Control>, INotifyPropertyChanged
+public abstract class LayoutElement : ILayoutElement<Control>, INotifyPropertyChanged, IPropertyDeserializable
 {
     public int UniqueId { get; set; }
 
+    public event ElementEventHandler PointerEnter
+    {
+        add => AddEvent(nameof(PointerEnter), value);
+        remove => RemoveEvent(nameof(PointerEnter), value);
+    }
+
+    public event ElementEventHandler PointerLeave
+    {
+        add => AddEvent(nameof(PointerLeave), value);
+        remove => RemoveEvent(nameof(PointerLeave), value);
+    }
+
+    public event ElementEventHandler PointerPressed
+    {
+        add => AddEvent(nameof(PointerPressed), value);
+        remove => RemoveEvent(nameof(PointerPressed), value);
+    }
+
+    public event ElementEventHandler PointerReleased
+    {
+        add => AddEvent(nameof(PointerReleased), value);
+        remove => RemoveEvent(nameof(PointerReleased), value);
+    }
+
+    public Cursor? Cursor { get; set; }
+
     private Dictionary<string, List<ElementEventHandler>>? _events;
-    public abstract Control BuildNative();
+
+    public virtual Control BuildNative()
+    {
+        Control control = CreateNativeControl();
+
+        BuildCore(control);
+        return control;
+    }
+
+    protected void BuildCore(Control control)
+    {
+        if (Cursor != null)
+        {
+            control.Cursor = new Avalonia.Input.Cursor((StandardCursorType)(Cursor.Value.BuiltInCursor ?? BuiltInCursor.None));
+        }
+
+        SubscribeBasicEvents(control);
+    }
+
+    private void SubscribeBasicEvents(Control control)
+    {
+        control.PointerEntered += (sender, args) =>
+            RaiseEvent(nameof(PointerEnter), new ElementEventArgs() { Sender = this });
+        control.PointerExited += (sender, args) =>
+            RaiseEvent(nameof(PointerLeave), new ElementEventArgs() { Sender = this });
+        control.PointerPressed += (sender, args) =>
+            RaiseEvent(nameof(PointerPressed), new ElementEventArgs() { Sender = this });
+        control.PointerReleased += (sender, args) =>
+            RaiseEvent(nameof(PointerReleased), new ElementEventArgs() { Sender = this });
+    }
+
+    protected abstract Control CreateNativeControl();
 
     public void AddEvent(string eventName, ElementEventHandler eventHandler)
     {
@@ -27,7 +90,7 @@ public abstract class LayoutElement : ILayoutElement<Control>, INotifyPropertyCh
 
         _events[eventName].Add(eventHandler);
     }
-    
+
     public void AddEvent<T>(string eventName, ElementEventHandler<T> eventHandler) where T : ElementEventArgs<T>
     {
         if (_events == null)
@@ -57,7 +120,7 @@ public abstract class LayoutElement : ILayoutElement<Control>, INotifyPropertyCh
 
         _events[eventName].Remove(eventHandler);
     }
-    
+
     public void RemoveEvent<T>(string eventName, ElementEventHandler<T> eventHandler) where T : ElementEventArgs<T>
     {
         if (_events == null)
@@ -105,4 +168,27 @@ public abstract class LayoutElement : ILayoutElement<Control>, INotifyPropertyCh
         OnPropertyChanged(propertyName);
         return true;
     }
+
+    public IEnumerable<object> GetProperties()
+    {
+        yield return Cursor;
+        foreach (var property in GetControlProperties())
+        {
+            yield return property;
+        }
+    }
+
+    protected virtual IEnumerable<object> GetControlProperties()
+    {
+        yield break;
+    }
+
+    public void DeserializeProperties(List<object> values)
+    {
+        Cursor = (Cursor?)values.ElementAtOrDefault(0);
+        var subValues = values[1..];
+        DeserializeControlProperties(subValues);
+    }
+
+    protected virtual void DeserializeControlProperties(List<object> values) { }
 }

+ 3 - 1
src/PixiEditor.Extensions/FlyUI/Elements/MainAxisAlignment.cs

@@ -14,5 +14,7 @@ public enum CrossAxisAlignment
 {
     Start,
     Center,
-    End
+    End,
+    Stretch,
+    //Baseline // Not implemented
 }

+ 0 - 1
src/PixiEditor.Extensions/FlyUI/Elements/MultiChildLayoutElement.cs

@@ -24,7 +24,6 @@ public abstract class MultiChildLayoutElement : LayoutElement, IMultiChildLayout
         }
     }
 
-    public abstract override Control BuildNative();
     /*public abstract void AddChild(Control child);
     public abstract void RemoveChild(int atIndex);*/
 

+ 3 - 3
src/PixiEditor.Extensions/FlyUI/Elements/Padding.cs

@@ -14,7 +14,7 @@ public class Padding : SingleChildLayoutElement, IPropertyDeserializable
     private Edges _edges = Edges.All(0);
     
     public Edges Edges { get => _edges; set => SetField(ref _edges, value); }
-    public override Control BuildNative()
+    protected override Control CreateNativeControl()
     {
         _decorator = new();
         
@@ -45,12 +45,12 @@ public class Padding : SingleChildLayoutElement, IPropertyDeserializable
         _decorator.Child = null;
     }
 
-    public IEnumerable<object> GetProperties()
+    protected override IEnumerable<object> GetControlProperties()
     {
         yield return Edges;
     }
 
-    public void DeserializeProperties(ImmutableList<object> values)
+    protected override void DeserializeControlProperties(List<object> values)
     {
         Edges = (Edges)values.ElementAtOrDefault(0, default(Edges));
     }

+ 3 - 3
src/PixiEditor.Extensions/FlyUI/Elements/Row.cs

@@ -60,7 +60,7 @@ public class Row : MultiChildLayoutElement, IPropertyDeserializable
         });
     }
 
-    public override Control BuildNative()
+    protected override Control CreateNativeControl()
     {
         panel = new RowPanel()
         {
@@ -73,13 +73,13 @@ public class Row : MultiChildLayoutElement, IPropertyDeserializable
         return panel;
     }
 
-    public IEnumerable<object> GetProperties()
+    protected override IEnumerable<object> GetControlProperties()
     {
         yield return MainAxisAlignment;
         yield return CrossAxisAlignment;
     }
 
-    public void DeserializeProperties(ImmutableList<object> values)
+    protected override void DeserializeControlProperties(List<object> values)
     {
         if (values.Count < 2)
             return;

+ 0 - 1
src/PixiEditor.Extensions/FlyUI/Elements/SingleChildLayoutElement.cs

@@ -23,7 +23,6 @@ public abstract class SingleChildLayoutElement : LayoutElement, ISingleChildLayo
         }
     }
 
-    public abstract override Control BuildNative();
     protected abstract void AddChild(Control child);
     protected abstract void RemoveChild();
 

+ 21 - 7
src/PixiEditor.Extensions/FlyUI/Elements/StatefulElement.cs

@@ -6,7 +6,8 @@ using PixiEditor.Extensions.CommonApi.FlyUI.State;
 
 namespace PixiEditor.Extensions.FlyUI.Elements;
 
-public abstract class StatefulElement<TState> : LayoutElement, /*IPropertyDeserializable,*/ IStatefulElement<Control, TState> where TState : IState<Control>
+public abstract class StatefulElement<TState> : LayoutElement,
+    IStatefulElement<Control, TState> where TState : IState<Control>
 {
     private TState? _state;
     private ContentPresenter _presenter = null!;
@@ -34,11 +35,21 @@ public abstract class StatefulElement<TState> : LayoutElement, /*IPropertyDeseri
 
     // TODO: Move actual Avalonia implementation to PixiEditor itself.
     public override Control BuildNative()
+    {
+        CreateNativeControl();
+        return _presenter;
+    }
+
+    protected override Control CreateNativeControl()
     {
         _presenter ??= new ContentPresenter();
         _content = State.Build();
-        _presenter.Content = _content.BuildNative();
-        return _presenter;
+        Control control = _content.BuildNative();
+
+        BuildCore(control);
+        _presenter.Content = control;
+
+        return control;
     }
 
     public abstract TState CreateState();
@@ -50,7 +61,8 @@ public abstract class StatefulElement<TState> : LayoutElement, /*IPropertyDeseri
         PerformDiff(_content, newTree);
     }
 
-    private void PerformDiff(ILayoutElement<Control> oldNode, ILayoutElement<Control> newNode, IChildHost? parent = null)
+    private void PerformDiff(ILayoutElement<Control> oldNode, ILayoutElement<Control> newNode,
+        IChildHost? parent = null)
     {
         // Check if the node types are the same
         bool isSameType = oldNode.GetType() == newNode.GetType();
@@ -90,11 +102,13 @@ public abstract class StatefulElement<TState> : LayoutElement, /*IPropertyDeseri
                 oldDeserializable.AddChild(newChildren.Current);
             }
 
-            if (oldChildren.Current == null && newChildren.Current != null && oldDeserializable.Count() < newDeserializable.Count())
+            if (oldChildren.Current == null && newChildren.Current != null &&
+                oldDeserializable.Count() < newDeserializable.Count())
             {
                 oldDeserializable.AddChild(newChildren.Current);
             }
-            else if (oldChildren.Current != null && newChildren.Current == null && oldDeserializable.Count() > newDeserializable.Count())
+            else if (oldChildren.Current != null && newChildren.Current == null &&
+                     oldDeserializable.Count() > newDeserializable.Count())
             {
                 oldDeserializable.RemoveChild(oldChildren.Current);
             }
@@ -107,7 +121,7 @@ public abstract class StatefulElement<TState> : LayoutElement, /*IPropertyDeseri
         {
             // TODO: Find a way to only apply changed properties, current solution shouldn't be a problem for most cases, but this
             // might cause unnecessary redraws, binding fires and other stuff that might be expensive if we have a lot of elements
-            propertyDeserializable.DeserializeProperties(fromProps.GetProperties().ToImmutableList());
+            propertyDeserializable.DeserializeProperties(fromProps.GetProperties().ToList());
         }
     }
 

+ 5 - 0
src/PixiEditor.Extensions/FlyUI/Elements/StatelessElement.cs

@@ -7,6 +7,11 @@ namespace PixiEditor.Extensions.FlyUI.Elements;
 public abstract class StatelessElement : LayoutElement, IStatelessElement<Control>
 {
     public override Control BuildNative()
+    {
+        return CreateNativeControl();
+    }
+
+    protected override Control CreateNativeControl()
     {
         return Build().BuildNative();
     }

+ 59 - 20
src/PixiEditor.Extensions/FlyUI/Elements/Text.cs

@@ -7,34 +7,32 @@ using PixiEditor.Extensions.CommonApi.FlyUI.Properties;
 using PixiEditor.Extensions.Extensions;
 using PixiEditor.Extensions.FlyUI.Converters;
 using FontStyle = PixiEditor.Extensions.CommonApi.FlyUI.Properties.FontStyle;
+using FontWeight = PixiEditor.Extensions.CommonApi.FlyUI.Properties.FontWeight;
 
 namespace PixiEditor.Extensions.FlyUI.Elements;
 
-public class Text : StatelessElement, IPropertyDeserializable
+public class Text : LayoutElement
 {
     private TextWrap _textWrap = TextWrap.None;
     private string _value = null!;
-    private FontStyle _fontStyle = FontStyle.Normal;
-    private double _fontSize = 12;
- 
+    private TextStyle textStyle = TextStyle.Default;
+
     public string Value { get => _value; set => SetField(ref _value, value); }
     public TextWrap TextWrap { get => _textWrap; set => SetField(ref _textWrap, value); }
-    public FontStyle FontStyle { get => _fontStyle; set => SetField(ref _fontStyle, value); }
-    public double FontSize { get => _fontSize; set => SetField(ref _fontSize, value); }
 
+    public TextStyle TextStyle { get => textStyle; set => SetField(ref textStyle, value); }
     public Text()
     {
     }
 
-    public Text(string value = "", TextWrap textWrap = TextWrap.None, FontStyle fontStyle = FontStyle.Normal, double fontSize = 12)
+    public Text(string value = "", TextWrap textWrap = TextWrap.None, TextStyle? textStyle = null)
     {
         Value = value;
         TextWrap = textWrap;
-        FontStyle = fontStyle;
-        FontSize = fontSize;
+        TextStyle = textStyle ?? TextStyle.Default;
     }
 
-    public override Control BuildNative()
+    protected override Control CreateNativeControl()
     {
         TextBlock textBlock = new();
         Binding valueBinding = new()
@@ -53,36 +51,77 @@ public class Text : StatelessElement, IPropertyDeserializable
         Binding fontStyleBinding = new()
         {
             Source = this,
-            Path = nameof(FontStyle),
+            Path = "TextStyle.FontStyle",
             Converter = new EnumToEnumConverter<FontStyle, Avalonia.Media.FontStyle>(),
         };
         
         Binding fontSizeBinding = new()
         {
             Source = this,
-            Path = nameof(FontSize),
+            Path = "TextStyle.FontSize",
+        };
+
+        Binding fontWeightBinding = new()
+        {
+            Source = this,
+            Path = "TextStyle.FontWeight",
+            Converter = new EnumToEnumConverter<FontWeight, Avalonia.Media.FontWeight>(),
+        };
+
+        Binding fontFamilyBinding = new()
+        {
+            Source = this,
+            Path = "TextStyle.FontFamily",
+        };
+
+        Binding colorBinding = new()
+        {
+            Source = this,
+            Path = "TextStyle.Color",
+            Converter = new ColorToAvaloniaBrushConverter(),
         };
         
         textBlock.Bind(TextBlock.TextProperty, valueBinding);
         textBlock.Bind(TextBlock.TextWrappingProperty, textWrapBinding);
-        textBlock.Bind(TextBlock.FontStyleProperty, fontStyleBinding);
-        textBlock.Bind(TextBlock.FontSizeProperty, fontSizeBinding);
+        if (TextStyle.FontStyle != null)
+        {
+            textBlock.Bind(TextBlock.FontStyleProperty, fontStyleBinding);
+        }
+
+        if (TextStyle.FontSize != null)
+        {
+            textBlock.Bind(TextBlock.FontSizeProperty, fontSizeBinding);
+        }
+
+        if (TextStyle.FontWeight != null)
+        {
+            textBlock.Bind(TextBlock.FontWeightProperty, fontWeightBinding);
+        }
+
+        if (TextStyle.FontFamily != null)
+        {
+            textBlock.Bind(TextBlock.FontFamilyProperty, fontFamilyBinding);
+        }
+
+        if (TextStyle.Color != null)
+        {
+            textBlock.Bind(TextBlock.ForegroundProperty, colorBinding);
+        }
+
         return textBlock;
     }
 
-    public virtual IEnumerable<object> GetProperties()
+    protected override IEnumerable<object> GetControlProperties()
     {
         yield return Value;
         yield return TextWrap;
-        yield return FontStyle;
-        yield return FontSize;
+        yield return TextStyle;
     }
 
-    public virtual void DeserializeProperties(ImmutableList<object> values)
+    protected override void DeserializeControlProperties(List<object> values)
     {
         Value = (string)values.ElementAtOrDefault(0);
         TextWrap = (TextWrap)values.ElementAtOrDefault(1);
-        FontStyle = (FontStyle)values.ElementAtOrDefault(2);
-        FontSize = (double)values.ElementAtOrDefault(3, 12.0);
+        TextStyle = (TextStyle)values.ElementAtOrDefault(2, TextStyle.Default);
     }
 }

+ 1 - 1
src/PixiEditor.Extensions/FlyUI/IPropertyDeserializable.cs

@@ -5,5 +5,5 @@ namespace PixiEditor.Extensions.FlyUI;
 public interface IPropertyDeserializable
 {
     public IEnumerable<object> GetProperties();
-    public void DeserializeProperties(ImmutableList<object> values);
+    public void DeserializeProperties(List<object> values);
 }

+ 16 - 1
src/PixiEditor.Extensions/UI/Panels/ColumnPanel.cs

@@ -17,7 +17,15 @@ public class ColumnPanel : Panel
         {
             child.Measure(availableSize);
             size += new Size(0, child.DesiredSize.Height);
-            size = new Size(Math.Max(size.Width, child.DesiredSize.Width), size.Height);
+
+            if (CrossAxisAlignment == CrossAxisAlignment.Stretch)
+            {
+                size = new Size(availableSize.Width, size.Height);
+            }
+            else
+            {
+                size = new Size(Math.Max(size.Width, child.DesiredSize.Width), size.Height);
+            }
         }
 
         if (MainAxisAlignment == MainAxisAlignment.SpaceBetween)
@@ -86,6 +94,13 @@ public class ColumnPanel : Panel
             {
                 xOffset = finalSize.Width - child.DesiredSize.Width;
             }
+            else if (CrossAxisAlignment == CrossAxisAlignment.Stretch)
+            {
+                xOffset = 0;
+                child.Arrange(new Rect(0, yOffset, finalSize.Width, child.DesiredSize.Height));
+                yOffset += child.DesiredSize.Height + spaceBetween;
+                continue;
+            }
 
             child.Arrange(new Rect(xOffset, yOffset, child.DesiredSize.Width, child.DesiredSize.Height));
             yOffset += child.DesiredSize.Height + spaceBetween;

+ 10 - 2
src/PixiEditor.Extensions/UI/Panels/RowPanel.cs

@@ -17,7 +17,15 @@ public class RowPanel : Panel
         {
             child.Measure(availableSize);
             size += new Size(child.DesiredSize.Width, 0);
-            size = new Size(size.Width, Math.Max(size.Height, child.DesiredSize.Height));
+
+            if (CrossAxisAlignment == CrossAxisAlignment.Stretch)
+            {
+                size = new Size(size.Width, availableSize.Height);
+            }
+            else
+            {
+                size = new Size(size.Width, Math.Max(size.Height, child.DesiredSize.Height));
+            }
         }
 
         if (MainAxisAlignment == MainAxisAlignment.SpaceBetween)
@@ -30,7 +38,7 @@ public class RowPanel : Panel
         }
         else if (MainAxisAlignment == MainAxisAlignment.SpaceEvenly)
         {
-            size = new Size (availableSize.Width, size.Height);
+            size = new Size(availableSize.Width, size.Height);
         }
 
         return size;

+ 1 - 2
src/PixiEditor/Helpers/Behaviours/ClearFocusOnClickBehavior.cs

@@ -17,7 +17,6 @@ internal class ClearFocusOnClickBehavior : Behavior<Control>
 
     private void AssociatedObject_LostFocus(object? sender, RoutedEventArgs? e)
     {
-
     }
 
     protected override void OnDetaching()
@@ -27,7 +26,7 @@ internal class ClearFocusOnClickBehavior : Behavior<Control>
 
     private void AssociatedObject_MouseDown(object? sender, PointerPressedEventArgs? e)
     {
-        AssociatedObject.Focus();
+        AssociatedObject?.Focus();
         ShortcutController.UnblockShortcutExecutionAll();
     }
 }

+ 36 - 11
tests/PixiEditor.Extensions.Sdk.Tests/NativeControlSerializationTest.cs

@@ -1,5 +1,6 @@
 using System.Text;
 using PixiEditor.Extensions.CommonApi.FlyUI;
+using PixiEditor.Extensions.CommonApi.FlyUI.Properties;
 using PixiEditor.Extensions.Sdk.Api.FlyUI;
 
 namespace PixiEditor.Extensions.Sdk.Tests;
@@ -9,7 +10,7 @@ public class NativeControlSerializationTest
     [Fact]
     public void TestThatNoChildLayoutSerializesCorrectBytes()
     {
-        CompiledControl layout = new CompiledControl(0, "Layout");
+        ControlDefinition layout = new ControlDefinition(0, "Layout");
         layout.AddProperty("Title");
 
         int uniqueId = 0;
@@ -45,8 +46,8 @@ public class NativeControlSerializationTest
     [Fact]
     public void TestThatChildLayoutSerializesCorrectBytes()
     {
-        CompiledControl layout = new CompiledControl(0, "Layout");
-        layout.AddChild(new CompiledControl(1, "Center"));
+        ControlDefinition layout = new ControlDefinition(0, "Layout");
+        layout.AddChild(new ControlDefinition(1, "Center"));
 
         int uniqueId = 0;
         byte[] uniqueIdBytes = BitConverter.GetBytes(uniqueId);
@@ -87,12 +88,36 @@ public class NativeControlSerializationTest
         Assert.Equal(expectedBytes.ToArray(), layout.Serialize().ToArray());
     }
 
+    [Fact]
+    public void TestThatBuildNativeBuildsPropertyBytesCorrectly()
+    {
+        Layout layout = new Layout();
+        var definition = layout.BuildNative();
+
+        Assert.Single(definition.Properties); // Cursor
+
+        byte[] serialized = definition.SerializeBytes();
+
+        Assert.Equal(23, serialized.Length);
+    }
+
+    [Fact]
+    public void TestThatStatelessElementSerializesBytesProperly()
+    {
+        WindowContentElement layout = new WindowContentElement();
+
+        var definition = layout.BuildNative();
+        var serialized = definition.SerializeBytes();
+
+        Assert.Equal(23, serialized.Length);
+    }
+
     [Fact]
     public void TestThatChildNestedLayoutSerializesCorrectBytes()
     {
-        CompiledControl layout = new CompiledControl(0, "Layout");
-        CompiledControl center = new CompiledControl(1, "Center");
-        CompiledControl text = new CompiledControl(2, "Text");
+        ControlDefinition layout = new ControlDefinition(0, "Layout");
+        ControlDefinition center = new ControlDefinition(1, "Center");
+        ControlDefinition text = new ControlDefinition(2, "Text");
         text.AddProperty("Hello world");
         center.AddChild(text);
         layout.AddChild(center);
@@ -171,18 +196,18 @@ public class NativeControlSerializationTest
             new Center(
                 child: new Text("hello sexy.")));
 
-        CompiledControl compiledControl = layout.BuildNative();
+        ControlDefinition compiledControl = layout.BuildNative();
 
         Assert.Equal("Layout", compiledControl.ControlTypeId);
-        Assert.Empty(compiledControl.Properties);
+        Assert.Single(compiledControl.Properties);
         Assert.Single(compiledControl.Children);
 
         Assert.Equal("Center", compiledControl.Children[0].ControlTypeId);
-        Assert.Empty(compiledControl.Children[0].Properties);
+        Assert.Single(compiledControl.Children[0].Properties);
 
         Assert.Equal("Text", compiledControl.Children[0].Children[0].ControlTypeId);
         Assert.True(compiledControl.Children[0].Children[0].Properties.Count > 0);
-        Assert.Equal("hello sexy.", compiledControl.Children[0].Children[0].Properties[0].value);
+        Assert.Equal("hello sexy.", compiledControl.Children[0].Children[0].Properties[1].value);
     }
 
     [Fact]
@@ -196,4 +221,4 @@ public class NativeControlSerializationTest
 
         Assert.Contains(button.BuildQueuedEvents, x => x == "Click");
     }
-}
+}

+ 17 - 0
tests/PixiEditor.Extensions.Sdk.Tests/WindowContentElement.cs

@@ -0,0 +1,17 @@
+using System.Diagnostics.CodeAnalysis;
+using PixiEditor.Extensions.CommonApi.FlyUI;
+using PixiEditor.Extensions.CommonApi.FlyUI.Properties;
+using PixiEditor.Extensions.Sdk.Api.FlyUI;
+using PixiEditor.Extensions.Sdk.Api.Window;
+
+namespace PixiEditor.Extensions.Sdk.Tests;
+
+[SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1118:Parameter should not span multiple lines", Justification = "FlyUI style")]
+public class WindowContentElement : StatelessElement
+{
+    public override ILayoutElement<ControlDefinition> Build()
+    {
+        Layout layout = new Layout();
+        return layout;
+    }
+}