Browse Source

Events are working

Krzysztof Krysiński 1 year ago
parent
commit
830dcba0c9
27 changed files with 270 additions and 141 deletions
  1. 1 0
      src/PixiEditor.Extensions.CommonApi/LayoutBuilding/ILayoutElement.cs
  2. 2 1
      src/PixiEditor.Extensions.CommonApi/LayoutBuilding/LayoutSerializationSpec.md
  3. 1 1
      src/PixiEditor.Extensions.CommonApi/PixiEditor.Extensions.CommonApi.csproj
  4. 27 15
      src/PixiEditor.Extensions.Wasm.Tests/NativeControlSerializationTest.cs
  5. 4 3
      src/PixiEditor.Extensions.Wasm/Api/LayoutBuilding/Button.cs
  6. 5 3
      src/PixiEditor.Extensions.Wasm/Api/LayoutBuilding/Center.cs
  7. 19 9
      src/PixiEditor.Extensions.Wasm/Api/LayoutBuilding/CompiledControl.cs
  8. 5 3
      src/PixiEditor.Extensions.Wasm/Api/LayoutBuilding/Layout.cs
  9. 16 7
      src/PixiEditor.Extensions.Wasm/Api/LayoutBuilding/LayoutElement.cs
  10. 2 2
      src/PixiEditor.Extensions.Wasm/Api/LayoutBuilding/LayoutElementsStore.cs
  11. 3 3
      src/PixiEditor.Extensions.Wasm/Api/LayoutBuilding/SingleChildLayoutElement.cs
  12. 4 2
      src/PixiEditor.Extensions.Wasm/Api/LayoutBuilding/Text.cs
  13. 2 2
      src/PixiEditor.Extensions.Wasm/Api/LayoutBuilding/TextElement.cs
  14. 2 1
      src/PixiEditor.Extensions.Wasm/Api/Window/IWindowProvider.cs
  15. 20 2
      src/PixiEditor.Extensions.Wasm/Api/Window/WindowProvider.cs
  16. 5 2
      src/PixiEditor.Extensions.Wasm/Interop.cs
  17. 1 0
      src/PixiEditor.Extensions.Wasm/native/api.h
  18. 4 0
      src/PixiEditor.Extensions.Wasm/native/api_interop.c
  19. 14 4
      src/PixiEditor.Extensions.Wasm/native/layout_builder_api.c
  20. 0 41
      src/PixiEditor.Extensions.WasmRuntime/MemoryUtility.cs
  21. 0 25
      src/PixiEditor.Extensions.WasmRuntime/PixiEditorApiLinkerExtensions.cs
  22. 49 7
      src/PixiEditor.Extensions.WasmRuntime/WasmExtensionInstance.cs
  23. 59 0
      src/PixiEditor.Extensions.WasmRuntime/WasmMemoryUtility.cs
  24. 1 0
      src/PixiEditor.Extensions.WasmRuntime/WasmRuntime.cs
  25. 21 7
      src/PixiEditor.Extensions/LayoutBuilding/Elements/LayoutBuilder.cs
  26. 2 0
      src/PixiEditor.Extensions/LayoutBuilding/Elements/LayoutElement.cs
  27. 1 1
      src/WasmSampleExtension/SampleExtension.cs

+ 1 - 0
src/PixiEditor.Extensions.CommonApi/LayoutBuilding/ILayoutElement.cs

@@ -4,6 +4,7 @@ namespace PixiEditor.Extensions.CommonApi.LayoutBuilding;
 
 public interface ILayoutElement<out TBuildResult>
 {
+    public int UniqueId { get; set; }
     public TBuildResult Build();
     public void AddEvent(string eventName, ElementEventHandler eventHandler);
     public void RemoveEvent(string eventName, ElementEventHandler eventHandler);

+ 2 - 1
src/PixiEditor.Extensions.CommonApi/LayoutBuilding/LayoutSerializationSpec.md

@@ -11,7 +11,8 @@ Layout byte span is a recursive structure containing elements and properties dat
 
 Byte sequence:
 ```
-    4 bytes - id of control,
+    4 bytes - unique id of the control,
+    4 bytes - type id of the control,
     4 bytes - length of properties data,
     n bytes - properties data,
         - 1 byte - property type,

+ 1 - 1
src/PixiEditor.Extensions.CommonApi/PixiEditor.Extensions.CommonApi.csproj

@@ -3,7 +3,7 @@
     <PropertyGroup>
         <TargetFramework>net8.0</TargetFramework>
         <ImplicitUsings>enable</ImplicitUsings>
-        <Nullable>enable</Nullable>
+        <Nullable>disable</Nullable>
     </PropertyGroup>
 
 </Project>

+ 27 - 15
src/PixiEditor.Extensions.Wasm.Tests/NativeControlSerializationTest.cs

@@ -9,7 +9,7 @@ public class NativeControlSerializationTest
     [Fact]
     public void TestThatNoChildLayoutSerializesCorrectBytes()
     {
-        NativeControl layout = new NativeControl("Layout");
+        CompiledControl layout = new CompiledControl(0, "Layout");
         layout.AddProperty("Title");
 
         int controlId = ByteMap.ControlMap["Layout"];
@@ -40,8 +40,8 @@ public class NativeControlSerializationTest
     [Fact]
     public void TestThatChildLayoutSerializesCorrectBytes()
     {
-        NativeControl layout = new NativeControl("Layout");
-        layout.AddChild(new NativeControl("Center"));
+        CompiledControl layout = new CompiledControl(0, "Layout");
+        layout.AddChild(new CompiledControl(0, "Center"));
 
         int controlId = ByteMap.ControlMap["Layout"];
         byte[] controlIdBytes = BitConverter.GetBytes(controlId);
@@ -75,9 +75,9 @@ public class NativeControlSerializationTest
     [Fact]
     public void TestThatChildNestedLayoutSerializesCorrectBytes()
     {
-        NativeControl layout = new NativeControl("Layout");
-        NativeControl center = new NativeControl("Center");
-        NativeControl text = new NativeControl("Text");
+        CompiledControl layout = new CompiledControl(0, "Layout");
+        CompiledControl center = new CompiledControl(1, "Center");
+        CompiledControl text = new CompiledControl(2, "Text");
         text.AddProperty("Hello world");
         center.AddChild(text);
         layout.AddChild(center);
@@ -141,17 +141,29 @@ public class NativeControlSerializationTest
             new Center(
                 child: new Text("hello sexy.")));
 
-        NativeControl nativeControl = layout.Build();
+        CompiledControl compiledControl = layout.Build();
 
-        Assert.Equal("Layout", nativeControl.ControlId);
-        Assert.Empty(nativeControl.Properties);
-        Assert.Single(nativeControl.Children);
+        Assert.Equal("Layout", compiledControl.ControlTypeId);
+        Assert.Empty(compiledControl.Properties);
+        Assert.Single(compiledControl.Children);
 
-        Assert.Equal("Center", nativeControl.Children[0].ControlId);
-        Assert.Empty(nativeControl.Children[0].Properties);
+        Assert.Equal("Center", compiledControl.Children[0].ControlTypeId);
+        Assert.Empty(compiledControl.Children[0].Properties);
 
-        Assert.Equal("Text", nativeControl.Children[0].Children[0].ControlId);
-        Assert.Single(nativeControl.Children[0].Children[0].Properties);
-        Assert.Equal("hello sexy.", nativeControl.Children[0].Children[0].Properties[0]);
+        Assert.Equal("Text", compiledControl.Children[0].Children[0].ControlTypeId);
+        Assert.Single(compiledControl.Children[0].Children[0].Properties);
+        Assert.Equal("hello sexy.", compiledControl.Children[0].Children[0].Properties[0]);
+    }
+
+    [Fact]
+    public void TestThatBuildButtonQueuesEvents()
+    {
+        Button button = new Button(
+            child: new Text("hello sexy."),
+            onClick: _ => { });
+
+        button.Build();
+
+        Assert.Contains(button.BuildQueuedEvents, x => x == "Click");
     }
 }

+ 4 - 3
src/PixiEditor.Extensions.Wasm/Api/LayoutBuilding/Button.cs

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

+ 5 - 3
src/PixiEditor.Extensions.Wasm/Api/LayoutBuilding/Center.cs

@@ -4,17 +4,19 @@ namespace PixiEditor.Extensions.Wasm.Api.LayoutBuilding;
 
 public class Center : SingleChildLayoutElement
 {
-    public Center(ILayoutElement<NativeControl> child)
+    public Center(ILayoutElement<CompiledControl> child)
     {
         Child = child;
     }
 
-    public override NativeControl Build()
+    public override CompiledControl Build()
     {
-        NativeControl center = new NativeControl("Center");
+        CompiledControl center = new CompiledControl(UniqueId, "Center");
 
         if (Child != null)
             center.AddChild(Child.Build());
+
+        BuildPendingEvents(center);
         return center;
     }
 }

+ 19 - 9
src/PixiEditor.Extensions.Wasm/Api/LayoutBuilding/NativeControl.cs → src/PixiEditor.Extensions.Wasm/Api/LayoutBuilding/CompiledControl.cs

@@ -3,16 +3,19 @@ using PixiEditor.Extensions.CommonApi.LayoutBuilding;
 
 namespace PixiEditor.Extensions.Wasm.Api.LayoutBuilding;
 
-public class NativeControl
+public class CompiledControl
 {
-    public string ControlId { get; set; }
-
+    public string ControlTypeId { get; set; }
     public List<object> Properties { get; set; } = new();
-    public List<NativeControl> Children { get; set; } = new();
+    public List<CompiledControl> Children { get; set; } = new();
+    public int UniqueId { get; set; }
+    internal List<string> QueuedEvents => _buildQueuedEvents;
 
-    public NativeControl(string controlId)
+    private List<string> _buildQueuedEvents = new List<string>();
+    public CompiledControl(int uniqueId, string controlTypeId)
     {
-        ControlId = controlId;
+        ControlTypeId = controlTypeId;
+        UniqueId = uniqueId;
     }
 
     public void AddProperty<T>(T value) where T : unmanaged
@@ -25,7 +28,7 @@ public class NativeControl
         Properties.Add(value);
     }
 
-    public void AddChild(NativeControl child)
+    public void AddChild(CompiledControl child)
     {
         Children.Add(child);
     }
@@ -39,7 +42,9 @@ public class NativeControl
     {
         // TODO: Make it more efficient
 
-        byte[] idBytes = BitConverter.GetBytes(ByteMap.ControlMap[ControlId]);
+        byte[] uniqueIdBytes = BitConverter.GetBytes(UniqueId);
+        bytes.AddRange(uniqueIdBytes);
+        byte[] idBytes = BitConverter.GetBytes(ByteMap.ControlMap[ControlTypeId]);
         bytes.AddRange(idBytes);
         bytes.AddRange(BitConverter.GetBytes(Properties.Count));
         bytes.AddRange(SerializeProperties());
@@ -50,7 +55,7 @@ public class NativeControl
 
     private void SerializeChildren(List<byte> bytes)
     {
-        foreach (NativeControl child in Children)
+        foreach (CompiledControl child in Children)
         {
             child.Serialize(bytes);
         }
@@ -84,4 +89,9 @@ public class NativeControl
 
         return result;
     }
+
+    internal void AddEvent(string eventName)
+    {
+        _buildQueuedEvents.Add(eventName);
+    }
 }

+ 5 - 3
src/PixiEditor.Extensions.Wasm/Api/LayoutBuilding/Layout.cs

@@ -4,17 +4,19 @@ namespace PixiEditor.Extensions.Wasm.Api.LayoutBuilding;
 
 public sealed class Layout : SingleChildLayoutElement
 {
-    public Layout(ILayoutElement<NativeControl> body = null)
+    public Layout(ILayoutElement<CompiledControl> body = null)
     {
         Child = body;
     }
 
-    public override NativeControl Build()
+    public override CompiledControl Build()
     {
-        NativeControl layout = new NativeControl("Layout");
+        CompiledControl layout = new CompiledControl(UniqueId, "Layout");
 
         if (Child != null)
             layout.AddChild(Child.Build());
+
+        BuildPendingEvents(layout);
         return layout;
     }
 

+ 16 - 7
src/PixiEditor.Extensions.Wasm/Api/LayoutBuilding/LayoutElement.cs

@@ -3,23 +3,23 @@ using PixiEditor.Extensions.CommonApi.LayoutBuilding.Events;
 
 namespace PixiEditor.Extensions.Wasm.Api.LayoutBuilding;
 
-public abstract class LayoutElement : ILayoutElement<NativeControl>
+public abstract class LayoutElement : ILayoutElement<CompiledControl>
 {
     private Dictionary<string, List<ElementEventHandler>> _events;
+    public List<string> BuildQueuedEvents = new List<string>();
+    public int UniqueId { get; set; }
 
-    private int InternalControlId { get; set; }
-
-    public abstract NativeControl Build();
+    public abstract CompiledControl Build();
 
     public LayoutElement()
     {
-        InternalControlId = LayoutElementIdGenerator.GetNextId();
-        LayoutElementsStore.AddElement(InternalControlId, this);
+        UniqueId = LayoutElementIdGenerator.GetNextId();
+        LayoutElementsStore.AddElement(UniqueId, this);
     }
 
     ~LayoutElement()
     {
-        LayoutElementsStore.RemoveElement(InternalControlId);
+        LayoutElementsStore.RemoveElement(UniqueId);
     }
 
     public void AddEvent(string eventName, ElementEventHandler eventHandler)
@@ -35,6 +35,7 @@ public abstract class LayoutElement : ILayoutElement<NativeControl>
         }
 
         _events[eventName].Add(eventHandler);
+        BuildQueuedEvents.Add(eventName);
     }
 
     public void RemoveEvent(string eventName, ElementEventHandler eventHandler)
@@ -69,4 +70,12 @@ public abstract class LayoutElement : ILayoutElement<NativeControl>
             eventHandler.Invoke(args);
         }
     }
+
+    protected void BuildPendingEvents(CompiledControl control)
+    {
+        foreach (string eventName in BuildQueuedEvents)
+        {
+            control.QueuedEvents.Add(eventName);
+        }
+    }
 }

+ 2 - 2
src/PixiEditor.Extensions.Wasm/Api/LayoutBuilding/LayoutElementsStore.cs

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

+ 3 - 3
src/PixiEditor.Extensions.Wasm/Api/LayoutBuilding/SingleChildLayoutElement.cs

@@ -2,8 +2,8 @@
 
 namespace PixiEditor.Extensions.Wasm.Api.LayoutBuilding;
 
-public abstract class SingleChildLayoutElement : LayoutElement, ISingleChildLayoutElement<NativeControl>
+public abstract class SingleChildLayoutElement : LayoutElement, ISingleChildLayoutElement<CompiledControl>
 {
-    public ILayoutElement<NativeControl> Child { get; set; }
-    public abstract override NativeControl Build();
+    public ILayoutElement<CompiledControl> Child { get; set; }
+    public abstract override CompiledControl Build();
 }

+ 4 - 2
src/PixiEditor.Extensions.Wasm/Api/LayoutBuilding/Text.cs

@@ -9,10 +9,12 @@ public class Text : TextElement
         Value = value;
     }
 
-    public override NativeControl Build()
+    public override CompiledControl Build()
     {
-        NativeControl text = new NativeControl("Text");
+        CompiledControl text = new CompiledControl(UniqueId, "Text");
         text.AddProperty(Value);
+
+        BuildPendingEvents(text);
         return text;
     }
 }

+ 2 - 2
src/PixiEditor.Extensions.Wasm/Api/LayoutBuilding/TextElement.cs

@@ -2,9 +2,9 @@
 
 namespace PixiEditor.Extensions.Wasm.Api.LayoutBuilding;
 
-public abstract class TextElement(string value = "") : LayoutElement, ITextElement<NativeControl>
+public abstract class TextElement(string value = "") : LayoutElement, ITextElement<CompiledControl>
 {
     public string Value { get; set; } = value;
 
-    public abstract override NativeControl Build();
+    public abstract override CompiledControl Build();
 }

+ 2 - 1
src/PixiEditor.Extensions.Wasm/Api/Window/IWindowProvider.cs

@@ -1,8 +1,9 @@
+using PixiEditor.Extensions.CommonApi.LayoutBuilding;
 using PixiEditor.Extensions.Wasm.Api.LayoutBuilding;
 
 namespace PixiEditor.Extensions.Wasm.Api.Window;
 
 public interface IWindowProvider
 {
-    public void CreatePopupWindow(string title, NativeControl body);
+    public void CreatePopupWindow(string title, LayoutElement body);
 }

+ 20 - 2
src/PixiEditor.Extensions.Wasm/Api/Window/WindowProvider.cs

@@ -1,4 +1,5 @@
 using System.Runtime.InteropServices;
+using PixiEditor.Extensions.CommonApi.LayoutBuilding;
 using PixiEditor.Extensions.Wasm.Api.LayoutBuilding;
 using PixiEditor.Extensions.Wasm.Utilities;
 
@@ -6,11 +7,28 @@ namespace PixiEditor.Extensions.Wasm.Api.Window;
 
 public class WindowProvider : IWindowProvider
 {
-    public void CreatePopupWindow(string title, NativeControl body)
+    public void CreatePopupWindow(string title, LayoutElement body)
     {
-        byte[] bytes = body.Serialize().ToArray();
+        CompiledControl compiledControl = body.Build();
+        byte[] bytes = compiledControl.Serialize().ToArray();
         IntPtr ptr = Marshal.AllocHGlobal(bytes.Length);
         Marshal.Copy(bytes, 0, ptr, bytes.Length);
         Interop.CreatePopupWindow(title, ptr, bytes.Length);
+        Marshal.FreeHGlobal(ptr);
+
+        SubscribeToEvents(compiledControl);
+    }
+
+    private void SubscribeToEvents(CompiledControl body)
+    {
+        foreach (CompiledControl child in body.Children)
+        {
+            SubscribeToEvents(child);
+        }
+
+        foreach (var queuedEvent in body.QueuedEvents)
+        {
+            Interop.SubscribeToEvent(body.UniqueId, queuedEvent);
+        }
     }
 }

+ 5 - 2
src/PixiEditor.Extensions.Wasm/Interop.cs

@@ -16,6 +16,9 @@ internal class Interop
     [MethodImpl(MethodImplOptions.InternalCall)]
     internal static extern void CreatePopupWindow(string title, IntPtr data, int length);
 
+    [MethodImpl(MethodImplOptions.InternalCall)]
+    internal static extern void SubscribeToEvent(int internalControlId, string eventName);
+
     internal static void Load()
     {
         Type extensionType = Assembly.GetEntryAssembly().ExportedTypes
@@ -38,9 +41,9 @@ internal class Interop
     internal static void EventRaised(int internalControlId, string eventName) //TOOD: Args
     {
         WasmExtension.Api.Logger.Log($"Event raised: {eventName} on {internalControlId}");
-        if (LayoutElementsStore.LayoutElements.TryGetValue(internalControlId, out ILayoutElement<NativeControl> element))
+        if (LayoutElementsStore.LayoutElements.TryGetValue((int)internalControlId, out ILayoutElement<CompiledControl> element))
         {
-            element.RaiseEvent(eventName, new ElementEventArgs());
+            element.RaiseEvent(eventName ?? "", new ElementEventArgs());
         }
     }
 }

+ 1 - 0
src/PixiEditor.Extensions.Wasm/native/api.h

@@ -2,5 +2,6 @@ void attach_logger_calls();
 void attach_window_calls();
 void attach_layout_builder_calls();
 void logger_log_message(MonoString* message);
+void log_message(const char* message, int32_t messageLength);
 MonoMethod* lookup_interop_method(const char* method_name);
 void invoke_interop_method(MonoMethod* method, void* params);

+ 4 - 0
src/PixiEditor.Extensions.Wasm/native/api_interop.c

@@ -17,6 +17,9 @@ void invoke_interop_method(MonoMethod* method, void* params)
     MonoObject* exception;
     mono_wasm_invoke_method(method, NULL, params, &exception);
     assert(!exception);
+
+    free(exception);
+    free(method);
 }
 
 __attribute((export_name("load")))
@@ -37,4 +40,5 @@ void attach_internal_calls()
 {
     attach_logger_calls();
     attach_window_calls();
+    attach_layout_builder_calls();
 }

+ 14 - 4
src/PixiEditor.Extensions.Wasm/native/layout_builder_api.c

@@ -4,16 +4,26 @@
 
 #include "api.h"
 
+__attribute((import_name("subscribe_to_event")))
+void subscribe_to_event(int32_t elementId, char* eventName, int32_t length);
+
+void internal_subscribe_to_event(int32_t elementId, MonoString* eventName)
+{
+    char* eventNameString = mono_wasm_string_get_utf8(eventName);
+    subscribe_to_event(elementId, eventNameString, strlen(eventNameString));
+}
+
 __attribute((export_name("raise_element_event")))
-void raise_element_event(int32_t elementId, char* eventName)
+void raise_element_event(int elementId, const char* eventName)
 {
     MonoMethod* method = lookup_interop_method("EventRaised");
-    MonoString* eventNameString = mono_wasm_string_from_js (eventName);
-    void* args[] = { elementId, eventNameString };
+    void* args[] = { &elementId, mono_wasm_string_from_js(eventName) };
     invoke_interop_method(method, args);
+
+    free(method);
 }
 
 void attach_layout_builder_calls()
 {
-
+    mono_add_internal_call("PixiEditor.Extensions.Wasm.Interop::SubscribeToEvent", internal_subscribe_to_event);
 }

+ 0 - 41
src/PixiEditor.Extensions.WasmRuntime/MemoryUtility.cs

@@ -1,41 +0,0 @@
-using System.Text;
-using Wasmtime;
-
-namespace PixiEditor.Extensions.WasmRuntime;
-
-public static class MemoryUtility
-{
-    public static string GetStringFromWasmMemory(int offset, int length, Memory memory)
-    {
-        //TODO: memory.ReadString is a thing
-        var span = memory.GetSpan<byte>(offset, length);
-        return Encoding.UTF8.GetString(span);
-    }
-
-    public static Span<T> GetSpanFromWasmMemory<T>(int bodyOffset, int bodyLength, Memory memory) where T : unmanaged
-    {
-        var span = memory.GetSpan<T>(bodyOffset, bodyLength);
-        return span;
-    }
-
-    public static int WriteInt32(Instance instance, Memory memory, int value)
-    {
-        // TODO: cache malloc function
-        var malloc = instance.GetFunction<int, int>("malloc");
-
-        const int length = 4;
-        var ptr = malloc.Invoke(length);
-        memory.WriteInt32(ptr, value);
-        return ptr;
-    }
-
-    public static int WriteString(Instance instance, Memory memory, string value)
-    {
-        var malloc = instance.GetFunction<int, int>("malloc");
-
-        var length = value.Length;
-        var ptr = malloc.Invoke(length);
-        memory.WriteString(ptr, value, Encoding.UTF8);
-        return ptr;
-    }
-}

+ 0 - 25
src/PixiEditor.Extensions.WasmRuntime/PixiEditorApiLinkerExtensions.cs

@@ -1,25 +0,0 @@
-using PixiEditor.Extensions.LayoutBuilding.Elements;
-using Wasmtime;
-
-namespace PixiEditor.Extensions.WasmRuntime;
-
-public static class PixiEditorApiLinkerExtensions
-{
-    public static void DefinePixiEditorApi(this Linker linker, WasmExtensionInstance instance)
-    {
-        linker.DefineFunction("env", "log_message",(int messageOffset, int messageLength) =>
-        {
-            string messageString = MemoryUtility.GetStringFromWasmMemory(messageOffset, messageLength, instance.Instance.GetMemory("memory"));
-            Console.WriteLine(messageString.ReplaceLineEndings());
-        });
-
-        linker.DefineFunction("env", "create_popup_window",(int titleOffset, int titleLength, int bodyOffset, int bodyLength) =>
-        {
-            string title = MemoryUtility.GetStringFromWasmMemory(titleOffset, titleLength, instance.Instance.GetMemory("memory"));
-            Span<byte> arr = MemoryUtility.GetSpanFromWasmMemory<byte>(bodyOffset, bodyLength, instance.Instance.GetMemory("memory"));
-
-            var body = LayoutConverter.Deserialize(arr);
-            instance.Api.WindowProvider.CreatePopupWindow(title, body.Build()).ShowDialog();
-        });
-    }
-}

+ 49 - 7
src/PixiEditor.Extensions.WasmRuntime/WasmExtensionInstance.cs

@@ -1,3 +1,9 @@
+using System.Runtime.InteropServices;
+using System.Text;
+using Avalonia.Controls;
+using Avalonia.Threading;
+using PixiEditor.Extensions.CommonApi.LayoutBuilding;
+using PixiEditor.Extensions.LayoutBuilding.Elements;
 using Wasmtime;
 
 namespace PixiEditor.Extensions.WasmRuntime;
@@ -12,32 +18,35 @@ public class WasmExtensionInstance : Extension
 
     private Memory memory = null!;
 
+    private Dictionary<int, ILayoutElement<Control>> managedElements = new();
+    private LayoutBuilder LayoutBuilder { get; }
+    private WasmMemoryUtility WasmMemoryUtility { get; set; }
+
+    private Action<int, int> raiseElementEvent;
+
     public WasmExtensionInstance(Linker linker, Store store, Module module)
     {
         Linker = linker;
         Store = store;
         Module = module;
+        LayoutBuilder = new LayoutBuilder(managedElements);
     }
 
     public void Instantiate()
     {
-        Linker.DefinePixiEditorApi(this);
+        DefinePixiEditorApi();
         Linker.DefineModule(Store, Module);
 
         Instance = Linker.Instantiate(Store, Module);
+        WasmMemoryUtility = new WasmMemoryUtility(Instance);
         Instance.GetFunction("_start").Invoke();
         memory = Instance.GetMemory("memory");
     }
 
     protected override void OnInitialized()
     {
+        raiseElementEvent = Instance.GetAction<int, int>("raise_element_event");
         Instance.GetAction("initialize").Invoke();
-        int testId = 69;
-        int ptr = MemoryUtility.WriteInt32(Instance, memory, testId);
-        int pt2 = MemoryUtility.WriteString(Instance, memory, "Test event");
-
-        Instance.GetAction<int, int>("raise_element_event").Invoke(ptr, pt2);
-
         base.OnInitialized();
     }
 
@@ -46,4 +55,37 @@ public class WasmExtensionInstance : Extension
         Instance.GetAction("load").Invoke();
         base.OnLoaded();
     }
+
+    private void DefinePixiEditorApi()
+    {
+        Linker.DefineFunction("env", "log_message",(int messageOffset, int messageLength) =>
+        {
+            string messageString = WasmMemoryUtility.GetString(messageOffset, messageLength);
+            Console.WriteLine(messageString.ReplaceLineEndings());
+        });
+
+        Linker.DefineFunction("env", "create_popup_window",(int titleOffset, int titleLength, int bodyOffset, int bodyLength) =>
+        {
+            string title = WasmMemoryUtility.GetString(titleOffset, titleLength);
+            Span<byte> arr = memory.GetSpan<byte>(bodyOffset, bodyLength);
+
+            var body = LayoutBuilder.Deserialize(arr);
+
+            Api.WindowProvider.CreatePopupWindow(title, body.Build()).ShowDialog();
+        });
+
+        Linker.DefineFunction("env", "subscribe_to_event", (int controlId, int eventNameOffset, int eventNameLengthOffset) =>
+        {
+            string eventName = WasmMemoryUtility.GetString(eventNameOffset, eventNameLengthOffset);
+
+            managedElements[controlId].AddEvent(eventName, (args) =>
+            {
+                var action = Instance.GetAction<int, int>("raise_element_event");
+                var ptr = WasmMemoryUtility.WriteString(eventName);
+
+                action.Invoke(controlId, ptr);
+                //WasmMemoryUtility.Free(nameOffset);
+            });
+        });
+    }
 }

+ 59 - 0
src/PixiEditor.Extensions.WasmRuntime/WasmMemoryUtility.cs

@@ -0,0 +1,59 @@
+using System.Runtime.InteropServices;
+using System.Text;
+using Wasmtime;
+
+namespace PixiEditor.Extensions.WasmRuntime;
+
+public class WasmMemoryUtility
+{
+    private Instance instance;
+    private readonly Memory memory;
+    private Func<int, int> malloc;
+    private Action<int> free;
+
+    public WasmMemoryUtility(Instance instance)
+    {
+        this.instance = instance;
+        memory = instance.GetMemory("memory");
+        malloc = instance.GetFunction<int, int>("malloc");
+        free = instance.GetAction<int>("free");
+    }
+
+    public string GetString(int offset, int length)
+    {
+        //TODO: memory.ReadString is a thing
+        var span = memory.GetSpan<byte>(offset, length);
+        return Encoding.UTF8.GetString(span);
+    }
+
+    public int WriteInt32(int value)
+    {
+        const int length = 4;
+        var ptr = malloc.Invoke(length);
+        memory.WriteInt32(ptr, value);
+        return ptr;
+    }
+
+    public int WriteString(string value)
+    {
+        string valueWithNullTerminator = value + '\0';
+        var ptr = malloc.Invoke(valueWithNullTerminator.Length);
+        memory.WriteString(ptr, valueWithNullTerminator);
+        return ptr;
+    }
+
+    public void Free(int address)
+    {
+        free.Invoke(address);
+    }
+
+    public int WriteSpan(byte[] bytes)
+    {
+        var length = bytes.Length;
+        var ptr = malloc.Invoke(length);
+
+        var span = memory.GetSpan<byte>(ptr, length);
+        bytes.CopyTo(span);
+        return ptr;
+    }
+}

+ 1 - 0
src/PixiEditor.Extensions.WasmRuntime/WasmRuntime.cs

@@ -21,6 +21,7 @@ public class WasmRuntime
 
         using var config = new Config().WithDebugInfo(true)
             .WithCraneliftDebugVerifier(true)
+            .WithReferenceTypes(true)
             .WithOptimizationLevel(OptimizationLevel.SpeedAndSize)
             .WithWasmThreads(true)
             .WithBulkMemory(true)

+ 21 - 7
src/PixiEditor.Extensions/LayoutBuilding/Elements/LayoutConverter.cs → src/PixiEditor.Extensions/LayoutBuilding/Elements/LayoutBuilder.cs

@@ -6,14 +6,24 @@ using PixiEditor.Extensions.Helpers;
 
 namespace PixiEditor.Extensions.LayoutBuilding.Elements;
 
-public static class LayoutConverter
+public class LayoutBuilder
 {
     private static int int32Size = sizeof(int);
 
-    public static ILayoutElement<Control> Deserialize(Span<byte> layoutSpan)
+    Dictionary<int, ILayoutElement<Control>> managedElements = new();
+    public LayoutBuilder(Dictionary<int, ILayoutElement<Control>> managedElements)
+    {
+        this.managedElements = managedElements;
+    }
+
+    public ILayoutElement<Control> Deserialize(Span<byte> layoutSpan)
     {
         int offset = 0;
-        int controlId = BitConverter.ToInt32(layoutSpan[..int32Size]);
+
+        int uniqueId = BitConverter.ToInt32(layoutSpan[offset..(offset + int32Size)]);
+        offset += int32Size;
+
+        int controlTypeId = BitConverter.ToInt32(layoutSpan[offset..(offset + int32Size)]);
         offset += int32Size;
 
         int propertiesCount = BitConverter.ToInt32(layoutSpan[offset..(offset + int32Size)]);
@@ -26,7 +36,7 @@ public static class LayoutConverter
 
         List<ILayoutElement<Control>> children = DeserializeChildren(layoutSpan, childrenCount, ref offset);
 
-        return BuildLayoutElement(controlId, properties, children);
+        return BuildLayoutElement(uniqueId, controlTypeId, properties, children);
     }
 
     private static List<object> DeserializeProperties(Span<byte> layoutSpan, int propertiesCount, ref int offset)
@@ -56,7 +66,7 @@ public static class LayoutConverter
         return properties;
     }
 
-    private static List<ILayoutElement<Control>> DeserializeChildren(Span<byte> layoutSpan, int childrenCount, ref int offset)
+    private List<ILayoutElement<Control>> DeserializeChildren(Span<byte> layoutSpan, int childrenCount, ref int offset)
     {
         var children = new List<ILayoutElement<Control>>();
         for (int i = 0; i < childrenCount; i++)
@@ -67,7 +77,8 @@ public static class LayoutConverter
         return children;
     }
 
-    private static ILayoutElement<Control> BuildLayoutElement(int controlId, List<object> properties, List<ILayoutElement<Control>> children)
+    private ILayoutElement<Control> BuildLayoutElement(int uniqueId, int controlId, List<object> properties,
+        List<ILayoutElement<Control>> children)
     {
         Func<ILayoutElement<Control>> factory = GlobalControlFactory.Map[controlId];
         var element = factory();
@@ -75,6 +86,8 @@ public static class LayoutConverter
         if(element is not { } layoutElement)
             throw new Exception("Element is not ILayoutElement<Control>");
 
+        element.UniqueId = uniqueId;
+
         if (element is IPropertyDeserializable deserializableProperties)
         {
             deserializableProperties.DeserializeProperties(properties);
@@ -88,7 +101,8 @@ public static class LayoutConverter
         {
             multiChildLayoutElement.Children = children;
         }
-        
+
+        managedElements.Add(uniqueId, layoutElement);
         return layoutElement;
     }
 }

+ 2 - 0
src/PixiEditor.Extensions/LayoutBuilding/Elements/LayoutElement.cs

@@ -6,6 +6,8 @@ namespace PixiEditor.Extensions.LayoutBuilding.Elements;
 
 public abstract class LayoutElement : ILayoutElement<Control>
 {
+    public int UniqueId { get; set; }
+
     private Dictionary<string, List<ElementEventHandler>>? _events;
     public abstract Control Build();
 

+ 1 - 1
src/WasmSampleExtension/SampleExtension.cs

@@ -23,7 +23,7 @@ public class SampleExtension : WasmExtension
                 )
             );
 
-        Api.WindowProvider.CreatePopupWindow("WASM SampleExtension", layout.Build());
+        Api.WindowProvider.CreatePopupWindow("WASM SampleExtension", layout);
     }
 }