Browse Source

Cursor and lower level control building

Krzysztof Krysiński 2 months ago
parent
commit
c6f942189a
43 changed files with 347 additions and 140 deletions
  1. 5 42
      samples/Sample7_FlyUI/WindowContentElement.cs
  2. 6 3
      src/PixiEditor.Extensions.CommonApi/FlyUI/ByteMap.cs
  3. 72 0
      src/PixiEditor.Extensions.CommonApi/FlyUI/Cursor.cs
  4. 1 1
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/Align.cs
  5. 3 2
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/Border.cs
  6. 1 1
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/Button.cs
  7. 1 1
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/Center.cs
  8. 1 1
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/CheckBox.cs
  9. 4 2
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/Column.cs
  10. 3 2
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/Container.cs
  11. 21 2
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/ControlDefinition.cs
  12. 4 4
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/Hyperlink.cs
  13. 3 2
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/Icon.cs
  14. 4 3
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/Image.cs
  15. 10 6
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/LayoutElement.cs
  16. 3 1
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/MultiChildLayoutElement.cs
  17. 3 2
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/Padding.cs
  18. 4 2
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/Row.cs
  19. 4 0
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/SingleChildLayoutElement.cs
  20. 6 8
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/StatefulElement.cs
  21. 15 1
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/StatelessElement.cs
  22. 4 3
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/Text.cs
  23. BIN
      src/PixiEditor.Extensions.Sdk/build/PixiEditor.Api.CGlueMSBuild.dll
  24. BIN
      src/PixiEditor.Extensions.Sdk/build/PixiEditor.Extensions.MSPackageBuilder.dll
  25. 25 0
      src/PixiEditor.Extensions/FlyUI/Converters/CursorToAvaloniaCursorConverter.cs
  26. 4 4
      src/PixiEditor.Extensions/FlyUI/Elements/Align.cs
  27. 3 3
      src/PixiEditor.Extensions/FlyUI/Elements/Border.cs
  28. 2 2
      src/PixiEditor.Extensions/FlyUI/Elements/Column.cs
  29. 2 2
      src/PixiEditor.Extensions/FlyUI/Elements/Container.cs
  30. 5 5
      src/PixiEditor.Extensions/FlyUI/Elements/Hyperlink.cs
  31. 3 3
      src/PixiEditor.Extensions/FlyUI/Elements/Icon.cs
  32. 3 3
      src/PixiEditor.Extensions/FlyUI/Elements/Image.cs
  33. 5 1
      src/PixiEditor.Extensions/FlyUI/Elements/LayoutBuilder.cs
  34. 48 4
      src/PixiEditor.Extensions/FlyUI/Elements/LayoutElement.cs
  35. 2 2
      src/PixiEditor.Extensions/FlyUI/Elements/Padding.cs
  36. 2 2
      src/PixiEditor.Extensions/FlyUI/Elements/Row.cs
  37. 3 4
      src/PixiEditor.Extensions/FlyUI/Elements/StatefulElement.cs
  38. 5 0
      src/PixiEditor.Extensions/FlyUI/Elements/StatelessElement.cs
  39. 3 3
      src/PixiEditor.Extensions/FlyUI/Elements/Text.cs
  40. 1 1
      src/PixiEditor.Extensions/FlyUI/IPropertyDeserializable.cs
  41. 0 1
      src/PixiEditor/Helpers/Behaviours/ClearFocusOnClickBehavior.cs
  42. 36 11
      tests/PixiEditor.Extensions.Sdk.Tests/NativeControlSerializationTest.cs
  43. 17 0
      tests/PixiEditor.Extensions.Sdk.Tests/WindowContentElement.cs

+ 5 - 42
samples/Sample7_FlyUI/WindowContentElement.cs

@@ -1,4 +1,5 @@
 using System.Diagnostics.CodeAnalysis;
+using PixiEditor.Extensions.CommonApi.FlyUI;
 using PixiEditor.Extensions.CommonApi.FlyUI.Events;
 using PixiEditor.Extensions.CommonApi.FlyUI.Properties;
 using PixiEditor.Extensions.Sdk;
@@ -12,50 +13,12 @@ public class WindowContentElement : StatelessElement
 {
     public PopupWindow Window { get; set; }
 
-    public override ControlDefinition BuildNative()
+    public override ILayoutElement<ControlDefinition> Build()
     {
-        Layout layout = new Layout(body:
-            new Container(margin: Edges.All(25), child:
-                new Column(
-                    crossAxisAlignment: CrossAxisAlignment.Center,
-                    mainAxisAlignment: MainAxisAlignment.SpaceEvenly,
-                    children:
-                    [
-                        new Center(
-                            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,
-                                textStyle: new TextStyle(fontSize: 16))
-                        ),
-                        new Align(
-                            alignment: Alignment.CenterRight,
-                            child: new Text("- Paulo Coelho, The Alchemist (1233)", textStyle: new TextStyle(fontStyle: FontStyle.Italic))
-                        ),
-                        new Container(
-                            margin: Edges.Symmetric(25, 0),
-                            backgroundColor: Color.FromRgba(25, 25, 25, 255),
-                            child: new Column(
-                                new Image(
-                                    "/Pizza.png",
-                                    filterQuality: FilterQuality.None,
-                                    width: 256, height: 256))
-                        ),
-                        new CheckBox(new Text("heloo"),
-                            onCheckedChanged: args =>
-                            {
-                                PixiEditorExtension.Api.Logger.Log(((CheckBox)args.Sender).IsChecked
-                                    ? "Checked"
-                                    : "Unchecked");
-                            }),
-                        new Center(
-                            new Button(
-                                child: new Text("Close"), onClick: _ => { Window.Close(); }))
-                    ]
-                )
-            )
+        Border layout = new Border(child:
+            new Container(margin: Edges.All(25))
         );
 
-        return layout.BuildNative();
+        return layout;
     }
-
 }

+ 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,
+}

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

@@ -7,7 +7,7 @@ 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;

+ 3 - 2
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;
 
@@ -22,7 +23,7 @@ public class Border : SingleChildLayoutElement
     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)
+        Color backgroundColor = default, Cursor? cursor = null) : base(cursor)
     {
         Child = child;
         Color = color;

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

@@ -11,7 +11,7 @@ public class Button : SingleChildLayoutElement
         remove => RemoveEvent(nameof(Click), value);
     }
 
-    public Button(ILayoutElement<ControlDefinition> child = null, ElementEventHandler onClick = null)
+    public Button(ILayoutElement<ControlDefinition> child = null, ElementEventHandler onClick = null, Cursor? cursor = null) : base(cursor)
     {
         Child = child;
         if (onClick != null)

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

@@ -4,7 +4,7 @@ namespace PixiEditor.Extensions.Sdk.Api.FlyUI;
 
 public class Center : SingleChildLayoutElement
 {
-    public Center(ILayoutElement<ControlDefinition> child)
+    public Center(ILayoutElement<ControlDefinition> child, Cursor? cursor = null) : base(cursor)
     {
         Child = child;
     }

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

@@ -13,7 +13,7 @@ public class CheckBox : SingleChildLayoutElement
 
     public bool IsChecked { get; set; }
 
-    public CheckBox(ILayoutElement<ControlDefinition> child = null, ElementEventHandler onCheckedChanged = null)
+    public CheckBox(ILayoutElement<ControlDefinition> child = null, ElementEventHandler onCheckedChanged = null, Cursor? cursor = null) : base(cursor)
     {
         Child = child;
         

+ 4 - 2
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;

+ 3 - 2
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;

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

@@ -27,8 +27,27 @@ public class ControlDefinition
         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);
@@ -132,10 +151,10 @@ public class ControlDefinition
         bytes.AddRange(Encoding.UTF8.GetBytes(structProperty.GetType().Name));
 
         byte[] structBytes = structProperty.Serialize();
-        
+
         bytes.AddRange(BitConverter.GetBytes(structBytes.Length));
         bytes.AddRange(structBytes);
-        
+
         return bytes;
     }
 

+ 4 - 4
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,12 +7,12 @@ public class Hyperlink : Text
 {
     public string Url { get; set; }
 
-    public Hyperlink(string url, string text, TextWrap textWrap = TextWrap.None, TextStyle? textStyle = null) : base(text, textWrap, textStyle)
+    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 ControlDefinition BuildNative()
+    protected override ControlDefinition CreateControl()
     {
         ControlDefinition hyperlink = new ControlDefinition(UniqueId, "Hyperlink");
         hyperlink.AddProperty(Value);
@@ -19,7 +20,6 @@ public class Hyperlink : Text
         hyperlink.AddProperty(TextStyle);
         hyperlink.AddProperty(Url);
 
-        BuildPendingEvents(hyperlink);
         return hyperlink;
     }
 }

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

@@ -1,14 +1,15 @@
+using PixiEditor.Extensions.CommonApi.FlyUI;
 using PixiEditor.Extensions.CommonApi.FlyUI.Properties;
 
 namespace PixiEditor.Extensions.Sdk.Api.FlyUI;
 
-public class Icon : StatelessElement
+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)
+    public Icon(string iconName, double size = 16, Color? color = null, Cursor? cursor = null) : base(cursor)
     {
         IconName = iconName;
         Size = size;

+ 4 - 3
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;

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

@@ -33,22 +33,26 @@ public abstract class LayoutElement : ILayoutElement<ControlDefinition>
         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();
 
-    public LayoutElement()
-    {
-        UniqueId = LayoutElementIdGenerator.GetNextId();
-        LayoutElementsStore.AddElement(UniqueId, this);
-    }
-
     ~LayoutElement()
     {
         LayoutElementsStore.RemoveElement(UniqueId);

+ 3 - 1
src/PixiEditor.Extensions.Sdk/Api/FlyUI/MultiChildLayoutElement.cs

@@ -12,5 +12,7 @@ public abstract class MultiChildLayoutElement : LayoutElement, IMultiChildLayout
 
     public List<LayoutElement> Children { get; set; }
 
-
+    public MultiChildLayoutElement(Cursor? cursor = null) : base(cursor)
+    {
+    }
 }

+ 3 - 2
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,7 +7,7 @@ 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;

+ 4 - 2
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,7 +17,7 @@ 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;

+ 4 - 0
src/PixiEditor.Extensions.Sdk/Api/FlyUI/SingleChildLayoutElement.cs

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

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

@@ -1,4 +1,5 @@
-using PixiEditor.Extensions.CommonApi.FlyUI.State;
+using PixiEditor.Extensions.CommonApi.FlyUI;
+using PixiEditor.Extensions.CommonApi.FlyUI.State;
 
 namespace PixiEditor.Extensions.Sdk.Api.FlyUI;
 
@@ -6,6 +7,10 @@ public abstract class StatefulElement<TState> : LayoutElement, IStatefulElement<
 {
     private TState state;
 
+    protected StatefulElement(Cursor? cursor) : base(cursor)
+    {
+    }
+
     IState<ControlDefinition> IStatefulElement<ControlDefinition>.State
     {
         get
@@ -26,13 +31,6 @@ public abstract class StatefulElement<TState> : LayoutElement, IStatefulElement<
 
     public TState State => (TState)((IStatefulElement<ControlDefinition>)this).State;
 
-    public override ControlDefinition BuildNative()
-    {
-        var statefulContainer = CreateControl();
-        BuildPendingEvents(statefulContainer);
-        return statefulContainer;
-    }
-
     protected override ControlDefinition CreateControl()
     {
         ControlDefinition controlDefinition = State.Build().BuildNative();

+ 15 - 1
src/PixiEditor.Extensions.Sdk/Api/FlyUI/StatelessElement.cs

@@ -5,8 +5,22 @@ namespace PixiEditor.Extensions.Sdk.Api.FlyUI;
 
 public abstract class StatelessElement : LayoutElement, IStatelessElement<ControlDefinition>
 {
-    public ILayoutElement<ControlDefinition> 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();
+    }
 }

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

@@ -1,8 +1,9 @@
-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; }
     
@@ -10,7 +11,7 @@ public class Text : StatelessElement
 
     public TextStyle TextStyle { get; set; }
     
-    public Text(string value, TextWrap wrap = TextWrap.None, TextStyle? textStyle = null)
+    public Text(string value, TextWrap wrap = TextWrap.None, TextStyle? textStyle = null, Cursor? cursor = null) : base(cursor)
     {
         Value = value;
         TextWrap = wrap;

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


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


+ 25 - 0
src/PixiEditor.Extensions/FlyUI/Converters/CursorToAvaloniaCursorConverter.cs

@@ -0,0 +1,25 @@
+using System.Globalization;
+using Avalonia.Data.Converters;
+using Avalonia.Input;
+using PixiEditor.Extensions.CommonApi.FlyUI;
+using Cursor = PixiEditor.Extensions.CommonApi.FlyUI.Cursor;
+
+namespace PixiEditor.Extensions.FlyUI.Converters;
+
+internal class CursorToAvaloniaCursorConverter : IValueConverter
+{
+    public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+    {
+        if (value is Cursor cursor)
+        {
+            return new Avalonia.Input.Cursor((StandardCursorType)(cursor.BuiltInCursor ?? BuiltInCursor.None));
+        }
+
+        return null;
+    }
+
+    public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+    {
+        throw new NotImplementedException();
+    }
+}

+ 4 - 4
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; }
@@ -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;
     }

+ 3 - 3
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;
 
@@ -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/Column.cs

@@ -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;

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

@@ -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));

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

@@ -16,9 +16,9 @@ public class Hyperlink : Text
         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), };
 
@@ -27,7 +27,7 @@ public class Hyperlink : Text
         return hyperlink;
     }
 
-    public override IEnumerable<object> GetProperties()
+    protected override IEnumerable<object> GetControlProperties()
     {
         yield return Value;
         yield return TextWrap;
@@ -35,9 +35,9 @@ public class Hyperlink : Text
         yield return Url;
     }
 
-    public override void DeserializeProperties(ImmutableList<object> values)
+    protected override void DeserializeControlProperties(List<object> values)
     {
-        base.DeserializeProperties(values);
+        base.DeserializeControlProperties(values);
         Url = (string)values[3];
     }
 }

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

@@ -10,7 +10,7 @@ using Colors = PixiEditor.Extensions.CommonApi.FlyUI.Properties.Colors;
 
 namespace PixiEditor.Extensions.FlyUI.Elements;
 
-public class Icon : StatelessElement, IPropertyDeserializable
+public class Icon : LayoutElement
 {
     private double size = 16;
     private string iconName = string.Empty;
@@ -50,14 +50,14 @@ public class Icon : StatelessElement, IPropertyDeserializable
         return textBlock;
     }
 
-    public IEnumerable<object> GetProperties()
+    protected override IEnumerable<object> GetControlProperties()
     {
         yield return IconName;
         yield return Size;
         yield return Color;
     }
 
-    public void DeserializeProperties(ImmutableList<object> values)
+    protected override void DeserializeControlProperties(List<object> values)
     {
         IconName = (string)values[0];
         Size = (double)values[1];

+ 3 - 3
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;
@@ -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);

+ 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)

+ 48 - 4
src/PixiEditor.Extensions/FlyUI/Elements/LayoutElement.cs

@@ -1,12 +1,18 @@
-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
@@ -33,17 +39,32 @@ public abstract class LayoutElement : ILayoutElement<Control>, INotifyPropertyCh
         remove => RemoveEvent(nameof(PointerReleased), value);
     }
 
+    public Cursor? Cursor { get; set; }
+
     private Dictionary<string, List<ElementEventHandler>>? _events;
 
     public virtual Control BuildNative()
     {
         Control control = CreateNativeControl();
 
-        SubscribeBasicEvents(control);
+        BuildCore(control);
         return control;
     }
 
-    protected void SubscribeBasicEvents(Control control)
+    protected void BuildCore(Control control)
+    {
+        Binding cursorBinding = new Binding()
+        {
+            Source = this,
+            Path = "Cursor",
+            Converter = new CursorToAvaloniaCursorConverter()
+        };
+
+        control.Bind(InputElement.CursorProperty, cursorBinding);
+        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 });
@@ -145,4 +166,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){}
 }

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

@@ -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));
     }

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

@@ -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;

+ 3 - 4
src/PixiEditor.Extensions/FlyUI/Elements/StatefulElement.cs

@@ -6,7 +6,7 @@ using PixiEditor.Extensions.CommonApi.FlyUI.State;
 
 namespace PixiEditor.Extensions.FlyUI.Elements;
 
-public abstract class StatefulElement<TState> : LayoutElement, /*IPropertyDeserializable,*/
+public abstract class StatefulElement<TState> : LayoutElement,
     IStatefulElement<Control, TState> where TState : IState<Control>
 {
     private TState? _state;
@@ -46,8 +46,7 @@ public abstract class StatefulElement<TState> : LayoutElement, /*IPropertyDeseri
         _content = State.Build();
         Control control = _content.BuildNative();
 
-        SubscribeBasicEvents(control);
-
+        BuildCore(control);
         _presenter.Content = control;
 
         return control;
@@ -122,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

@@ -6,6 +6,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();

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

@@ -11,7 +11,7 @@ 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!;
@@ -111,14 +111,14 @@ public class Text : StatelessElement, IPropertyDeserializable
         return textBlock;
     }
 
-    public virtual IEnumerable<object> GetProperties()
+    protected override IEnumerable<object> GetControlProperties()
     {
         yield return Value;
         yield return TextWrap;
         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);

+ 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);
 }

+ 0 - 1
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()

+ 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;
+    }
+}