Browse Source

Event passing wip

Krzysztof Krysiński 1 year ago
parent
commit
b191649882
28 changed files with 381 additions and 58 deletions
  1. 11 0
      src/PixiEditor.Extensions.CommonApi/LayoutBuilding/Events/ElementEventArgs.cs
  2. 4 0
      src/PixiEditor.Extensions.CommonApi/LayoutBuilding/Events/ElementEventHandler.cs
  3. 6 1
      src/PixiEditor.Extensions.CommonApi/LayoutBuilding/ILayoutElement.cs
  4. 12 0
      src/PixiEditor.Extensions.CommonApi/LayoutBuilding/LayoutElementIdGenerator.cs
  5. 28 0
      src/PixiEditor.Extensions.Tests/LayoutBuilderTests.cs
  6. 13 10
      src/PixiEditor.Extensions.Wasm/Api/LayoutBuilding/Button.cs
  7. 5 5
      src/PixiEditor.Extensions.Wasm/Api/LayoutBuilding/Center.cs
  8. 5 5
      src/PixiEditor.Extensions.Wasm/Api/LayoutBuilding/Layout.cs
  9. 72 0
      src/PixiEditor.Extensions.Wasm/Api/LayoutBuilding/LayoutElement.cs
  10. 18 0
      src/PixiEditor.Extensions.Wasm/Api/LayoutBuilding/LayoutElementsStore.cs
  11. 9 0
      src/PixiEditor.Extensions.Wasm/Api/LayoutBuilding/SingleChildLayoutElement.cs
  12. 2 4
      src/PixiEditor.Extensions.Wasm/Api/LayoutBuilding/Text.cs
  13. 10 0
      src/PixiEditor.Extensions.Wasm/Api/LayoutBuilding/TextElement.cs
  14. 12 0
      src/PixiEditor.Extensions.Wasm/Interop.cs
  15. 4 0
      src/PixiEditor.Extensions.Wasm/PixiEditor.Extensions.Wasm.csproj
  16. 3 0
      src/PixiEditor.Extensions.Wasm/native/api.h
  17. 4 5
      src/PixiEditor.Extensions.Wasm/native/api_interop.c
  18. 19 0
      src/PixiEditor.Extensions.Wasm/native/layout_builder_api.c
  19. 22 1
      src/PixiEditor.Extensions.WasmRuntime/MemoryUtility.cs
  20. 9 0
      src/PixiEditor.Extensions.WasmRuntime/WasmExtensionInstance.cs
  21. 15 13
      src/PixiEditor.Extensions/LayoutBuilding/Elements/Button.cs
  22. 2 4
      src/PixiEditor.Extensions/LayoutBuilding/Elements/Center.cs
  23. 9 5
      src/PixiEditor.Extensions/LayoutBuilding/Elements/Layout.cs
  24. 59 0
      src/PixiEditor.Extensions/LayoutBuilding/Elements/LayoutElement.cs
  25. 10 0
      src/PixiEditor.Extensions/LayoutBuilding/Elements/SingleChildLayoutElement.cs
  26. 2 4
      src/PixiEditor.Extensions/LayoutBuilding/Elements/Text.cs
  27. 10 0
      src/PixiEditor.Extensions/LayoutBuilding/Elements/TextElement.cs
  28. 6 1
      src/WasmSampleExtension/SampleExtension.cs

+ 11 - 0
src/PixiEditor.Extensions.CommonApi/LayoutBuilding/Events/ElementEventArgs.cs

@@ -0,0 +1,11 @@
+namespace PixiEditor.Extensions.CommonApi.LayoutBuilding.Events;
+
+public class ElementEventArgs
+{
+    public static ElementEventArgs Empty { get; } = new ElementEventArgs();
+}
+
+public class ElementEventArgs<TEventArgs> : ElementEventArgs where TEventArgs : ElementEventArgs
+{
+    public static new ElementEventArgs<TEventArgs> Empty { get; } = new ElementEventArgs<TEventArgs>();
+}

+ 4 - 0
src/PixiEditor.Extensions.CommonApi/LayoutBuilding/Events/ElementEventHandler.cs

@@ -0,0 +1,4 @@
+namespace PixiEditor.Extensions.CommonApi.LayoutBuilding.Events;
+
+public delegate void ElementEventHandler(ElementEventArgs args);
+public delegate void ElementEventHandler<in TEventArgs>(TEventArgs args) where TEventArgs : ElementEventArgs<TEventArgs>;

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

@@ -1,6 +1,11 @@
-namespace PixiEditor.Extensions.CommonApi.LayoutBuilding;
+using PixiEditor.Extensions.CommonApi.LayoutBuilding.Events;
+
+namespace PixiEditor.Extensions.CommonApi.LayoutBuilding;
 
 
 public interface ILayoutElement<out TBuildResult>
 public interface ILayoutElement<out TBuildResult>
 {
 {
     public TBuildResult Build();
     public TBuildResult Build();
+    public void AddEvent(string eventName, ElementEventHandler eventHandler);
+    public void RemoveEvent(string eventName, ElementEventHandler eventHandler);
+    public void RaiseEvent(string eventName, ElementEventArgs args);
 }
 }

+ 12 - 0
src/PixiEditor.Extensions.CommonApi/LayoutBuilding/LayoutElementIdGenerator.cs

@@ -0,0 +1,12 @@
+namespace PixiEditor.Extensions.CommonApi.LayoutBuilding;
+
+public static class LayoutElementIdGenerator
+{
+    private static int _lastId = -1;
+
+    public static int GetNextId()
+    {
+        _lastId++;
+        return _lastId;
+    }
+}

+ 28 - 0
src/PixiEditor.Extensions.Tests/LayoutBuilderTests.cs

@@ -1,6 +1,9 @@
 using Avalonia.Controls;
 using Avalonia.Controls;
+using Avalonia.Interactivity;
+using PixiEditor.Extensions.CommonApi.LayoutBuilding.Events;
 using PixiEditor.Extensions.LayoutBuilding;
 using PixiEditor.Extensions.LayoutBuilding;
 using PixiEditor.Extensions.LayoutBuilding.Elements;
 using PixiEditor.Extensions.LayoutBuilding.Elements;
+using Button = PixiEditor.Extensions.LayoutBuilding.Elements.Button;
 
 
 namespace PixiEditor.Extensions.Test;
 namespace PixiEditor.Extensions.Test;
 
 
@@ -32,4 +35,29 @@ public class LayoutBuilderTests
 
 
         Assert.Equal("Hello", textBlock.Text);
         Assert.Equal("Hello", textBlock.Text);
     }
     }
+
+    [Fact]
+    public void TestThatButtonClickEventFiresCallback()
+    {
+        Button button = new Button();
+        bool callbackFired = false;
+
+        button.Click += (e) => callbackFired = true;
+        button.RaiseEvent(nameof(Button.Click), ElementEventArgs.Empty);
+
+        Assert.True(callbackFired);
+    }
+
+    [Fact]
+    public void TestThatAvaloniaClickEventFiresElementCallback()
+    {
+        Button button = new Button();
+        bool callbackFired = false;
+
+        button.Click += (e) => callbackFired = true;
+
+        button.Build().RaiseEvent(new RoutedEventArgs(Avalonia.Controls.Button.ClickEvent));
+
+        Assert.True(callbackFired);
+    }
 }
 }

+ 13 - 10
src/PixiEditor.Extensions.Wasm/Api/LayoutBuilding/Button.cs

@@ -1,26 +1,29 @@
 using PixiEditor.Extensions.CommonApi.LayoutBuilding;
 using PixiEditor.Extensions.CommonApi.LayoutBuilding;
+using PixiEditor.Extensions.CommonApi.LayoutBuilding.Events;
 
 
 namespace PixiEditor.Extensions.Wasm.Api.LayoutBuilding;
 namespace PixiEditor.Extensions.Wasm.Api.LayoutBuilding;
 
 
-public class Button : ISingleChildLayoutElement<NativeControl>
+public class Button : SingleChildLayoutElement
 {
 {
-    ILayoutElement<NativeControl> ISingleChildLayoutElement<NativeControl>.Child
+    public event ElementEventHandler Click
     {
     {
-        get => Content;
-        set => Content = value;
+        add => AddEvent(nameof(Click), value);
+        remove => RemoveEvent(nameof(Click), value);
     }
     }
 
 
-    public ILayoutElement<NativeControl> Content { get; set; }
-
-    public Button(ILayoutElement<NativeControl> child = null)
+    public Button(ILayoutElement<NativeControl> child = null, ElementEventHandler onClick = null)
     {
     {
-        Content = child;
+        Child = child;
+        if (onClick != null)
+            Click += onClick;
     }
     }
 
 
-    public NativeControl Build()
+    public override NativeControl Build()
     {
     {
         NativeControl button = new NativeControl("Button");
         NativeControl button = new NativeControl("Button");
-        button.AddChild(Content.Build());
+        if (Child != null)
+            button.AddChild(Child.Build());
+
         return button;
         return button;
     }
     }
 }
 }

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

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

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

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

+ 72 - 0
src/PixiEditor.Extensions.Wasm/Api/LayoutBuilding/LayoutElement.cs

@@ -0,0 +1,72 @@
+using PixiEditor.Extensions.CommonApi.LayoutBuilding;
+using PixiEditor.Extensions.CommonApi.LayoutBuilding.Events;
+
+namespace PixiEditor.Extensions.Wasm.Api.LayoutBuilding;
+
+public abstract class LayoutElement : ILayoutElement<NativeControl>
+{
+    private Dictionary<string, List<ElementEventHandler>> _events;
+
+    private int InternalControlId { get; set; }
+
+    public abstract NativeControl Build();
+
+    public LayoutElement()
+    {
+        InternalControlId = LayoutElementIdGenerator.GetNextId();
+        LayoutElementsStore.AddElement(InternalControlId, this);
+    }
+
+    ~LayoutElement()
+    {
+        LayoutElementsStore.RemoveElement(InternalControlId);
+    }
+
+    public void AddEvent(string eventName, ElementEventHandler eventHandler)
+    {
+        if (_events == null)
+        {
+            _events = new Dictionary<string, List<ElementEventHandler>>();
+        }
+
+        if (!_events.ContainsKey(eventName))
+        {
+            _events.Add(eventName, new List<ElementEventHandler>());
+        }
+
+        _events[eventName].Add(eventHandler);
+    }
+
+    public void RemoveEvent(string eventName, ElementEventHandler eventHandler)
+    {
+        if (_events == null)
+        {
+            return;
+        }
+
+        if (!_events.ContainsKey(eventName))
+        {
+            return;
+        }
+
+        _events[eventName].Remove(eventHandler);
+    }
+
+    public void RaiseEvent(string eventName, ElementEventArgs args)
+    {
+        if (_events == null)
+        {
+            return;
+        }
+
+        if (!_events.ContainsKey(eventName))
+        {
+            return;
+        }
+
+        foreach (ElementEventHandler eventHandler in _events[eventName])
+        {
+            eventHandler.Invoke(args);
+        }
+    }
+}

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

@@ -0,0 +1,18 @@
+using PixiEditor.Extensions.CommonApi.LayoutBuilding;
+
+namespace PixiEditor.Extensions.Wasm.Api.LayoutBuilding;
+
+internal static class LayoutElementsStore
+{
+    public static Dictionary<int, ILayoutElement<NativeControl>> LayoutElements { get; } = new();
+
+    public static void AddElement(int internalId, ILayoutElement<NativeControl> element)
+    {
+        LayoutElements.Add(internalId, element);
+    }
+
+    public static void RemoveElement(int internalId)
+    {
+        LayoutElements.Remove(internalId);
+    }
+}

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

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

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

@@ -2,16 +2,14 @@
 
 
 namespace PixiEditor.Extensions.Wasm.Api.LayoutBuilding;
 namespace PixiEditor.Extensions.Wasm.Api.LayoutBuilding;
 
 
-public class Text : ITextElement<NativeControl>
+public class Text : TextElement
 {
 {
-    public string Value { get; set; }
-
     public Text(string value)
     public Text(string value)
     {
     {
         Value = value;
         Value = value;
     }
     }
 
 
-    NativeControl ILayoutElement<NativeControl>.Build()
+    public override NativeControl Build()
     {
     {
         NativeControl text = new NativeControl("Text");
         NativeControl text = new NativeControl("Text");
         text.AddProperty(Value);
         text.AddProperty(Value);

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

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

+ 12 - 0
src/PixiEditor.Extensions.Wasm/Interop.cs

@@ -2,6 +2,9 @@
 using System.Reflection;
 using System.Reflection;
 using System.Runtime.CompilerServices;
 using System.Runtime.CompilerServices;
 using System.Runtime.InteropServices;
 using System.Runtime.InteropServices;
+using PixiEditor.Extensions.CommonApi.LayoutBuilding;
+using PixiEditor.Extensions.CommonApi.LayoutBuilding.Events;
+using PixiEditor.Extensions.Wasm.Api.LayoutBuilding;
 
 
 namespace PixiEditor.Extensions.Wasm;
 namespace PixiEditor.Extensions.Wasm;
 
 
@@ -31,4 +34,13 @@ internal class Interop
     {
     {
         ExtensionContext.Active.OnInitialized();
         ExtensionContext.Active.OnInitialized();
     }
     }
+
+    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))
+        {
+            element.RaiseEvent(eventName, new ElementEventArgs());
+        }
+    }
 }
 }

+ 4 - 0
src/PixiEditor.Extensions.Wasm/PixiEditor.Extensions.Wasm.csproj

@@ -11,6 +11,10 @@
       <ProjectReference Include="..\PixiEditor.Extensions.CommonApi\PixiEditor.Extensions.CommonApi.csproj" />
       <ProjectReference Include="..\PixiEditor.Extensions.CommonApi\PixiEditor.Extensions.CommonApi.csproj" />
     </ItemGroup>
     </ItemGroup>
 
 
+    <ItemGroup>
+      <ClCompile Include="native\layout_builder_api.c" />
+    </ItemGroup>
+
   <Import Project="build\PixiEditor.Extensions.Wasm.targets"/>
   <Import Project="build\PixiEditor.Extensions.Wasm.targets"/>
   <Import Project="build\PixiEditor.Extensions.Wasm.props"/>
   <Import Project="build\PixiEditor.Extensions.Wasm.props"/>
 
 

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

@@ -1,3 +1,6 @@
 void attach_logger_calls();
 void attach_logger_calls();
 void attach_window_calls();
 void attach_window_calls();
+void attach_layout_builder_calls();
 void logger_log_message(MonoString* message);
 void logger_log_message(MonoString* message);
+MonoMethod* lookup_interop_method(const char* method_name);
+void invoke_interop_method(MonoMethod* method, void* params);

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

@@ -12,11 +12,10 @@ MonoMethod* lookup_interop_method(const char* method_name)
     return method;
     return method;
 }
 }
 
 
-void invoke_interop_method(MonoMethod* method)
+void invoke_interop_method(MonoMethod* method, void* params)
 {
 {
-    void* method_params[] = {  };
     MonoObject* exception;
     MonoObject* exception;
-    mono_wasm_invoke_method(method, NULL, method_params, &exception);
+    mono_wasm_invoke_method(method, NULL, params, &exception);
     assert(!exception);
     assert(!exception);
 }
 }
 
 
@@ -24,14 +23,14 @@ __attribute((export_name("load")))
 void load()
 void load()
 {
 {
     MonoMethod* metod = lookup_interop_method("Load");
     MonoMethod* metod = lookup_interop_method("Load");
-    invoke_interop_method(metod);
+    invoke_interop_method(metod, NULL);
 }
 }
 
 
 __attribute((export_name("initialize")))
 __attribute((export_name("initialize")))
 void initialize()
 void initialize()
 {
 {
     MonoMethod* metod = lookup_interop_method("Initialize");
     MonoMethod* metod = lookup_interop_method("Initialize");
-    invoke_interop_method(metod);
+    invoke_interop_method(metod, NULL);
 }
 }
 
 
 void attach_internal_calls()
 void attach_internal_calls()

+ 19 - 0
src/PixiEditor.Extensions.Wasm/native/layout_builder_api.c

@@ -0,0 +1,19 @@
+#include <mono-wasi/driver.h>
+#include <string.h>
+#include <assert.h>
+
+#include "api.h"
+
+__attribute((export_name("raise_element_event")))
+void raise_element_event(int32_t elementId, char* eventName)
+{
+    MonoMethod* method = lookup_interop_method("EventRaised");
+    MonoString* eventNameString = mono_wasm_string_from_js (eventName);
+    void* args[] = { elementId, eventNameString };
+    invoke_interop_method(method, args);
+}
+
+void attach_layout_builder_calls()
+{
+
+}

+ 22 - 1
src/PixiEditor.Extensions.WasmRuntime/MemoryUtility.cs

@@ -7,8 +7,8 @@ public static class MemoryUtility
 {
 {
     public static string GetStringFromWasmMemory(int offset, int length, Memory memory)
     public static string GetStringFromWasmMemory(int offset, int length, Memory memory)
     {
     {
+        //TODO: memory.ReadString is a thing
         var span = memory.GetSpan<byte>(offset, length);
         var span = memory.GetSpan<byte>(offset, length);
-        
         return Encoding.UTF8.GetString(span);
         return Encoding.UTF8.GetString(span);
     }
     }
 
 
@@ -17,4 +17,25 @@ public static class MemoryUtility
         var span = memory.GetSpan<T>(bodyOffset, bodyLength);
         var span = memory.GetSpan<T>(bodyOffset, bodyLength);
         return span;
         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;
+    }
 }
 }

+ 9 - 0
src/PixiEditor.Extensions.WasmRuntime/WasmExtensionInstance.cs

@@ -10,6 +10,8 @@ public class WasmExtensionInstance : Extension
     private Store Store { get; }
     private Store Store { get; }
     private Module Module { get; }
     private Module Module { get; }
 
 
+    private Memory memory = null!;
+
     public WasmExtensionInstance(Linker linker, Store store, Module module)
     public WasmExtensionInstance(Linker linker, Store store, Module module)
     {
     {
         Linker = linker;
         Linker = linker;
@@ -24,11 +26,18 @@ public class WasmExtensionInstance : Extension
 
 
         Instance = Linker.Instantiate(Store, Module);
         Instance = Linker.Instantiate(Store, Module);
         Instance.GetFunction("_start").Invoke();
         Instance.GetFunction("_start").Invoke();
+        memory = Instance.GetMemory("memory");
     }
     }
 
 
     protected override void OnInitialized()
     protected override void OnInitialized()
     {
     {
         Instance.GetAction("initialize").Invoke();
         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();
         base.OnInitialized();
     }
     }
 
 

+ 15 - 13
src/PixiEditor.Extensions/LayoutBuilding/Elements/Button.cs

@@ -1,30 +1,32 @@
 using Avalonia.Controls;
 using Avalonia.Controls;
+using Avalonia.Interactivity;
 using PixiEditor.Extensions.CommonApi.LayoutBuilding;
 using PixiEditor.Extensions.CommonApi.LayoutBuilding;
+using PixiEditor.Extensions.CommonApi.LayoutBuilding.Events;
 
 
 namespace PixiEditor.Extensions.LayoutBuilding.Elements;
 namespace PixiEditor.Extensions.LayoutBuilding.Elements;
 
 
-public class Button : ISingleChildLayoutElement<Control>
+public class Button : SingleChildLayoutElement
 {
 {
-    ILayoutElement<Control> ISingleChildLayoutElement<Control>.Child
+    public event ElementEventHandler Click
     {
     {
-        get => Content;
-        set => Content = value;
+        add => AddEvent(nameof(Click), value);
+        remove => RemoveEvent(nameof(Click), value);
     }
     }
 
 
-    public ILayoutElement<Control> Content { get; set; }
-
-    public Button(ILayoutElement<Control> content = null)
+    public Button(ILayoutElement<Control>? child = null)
     {
     {
-        Content = content;
+        Child = child;
     }
     }
 
 
-    public Control Build()
+    public override Control Build()
     {
     {
-        return new Avalonia.Controls.Button() { Content = Content.Build() };
-    }
+        Avalonia.Controls.Button btn = new Avalonia.Controls.Button()
+        {
+            Content = Child?.Build(),
+        };
 
 
-    public void DeserializeProperties(List<object> values)
-    {
+        btn.Click += (sender, args) => RaiseEvent(nameof(Click), new ElementEventArgs());
 
 
+        return btn;
     }
     }
 }
 }

+ 2 - 4
src/PixiEditor.Extensions/LayoutBuilding/Elements/Center.cs

@@ -3,16 +3,14 @@ using PixiEditor.Extensions.CommonApi.LayoutBuilding;
 
 
 namespace PixiEditor.Extensions.LayoutBuilding.Elements;
 namespace PixiEditor.Extensions.LayoutBuilding.Elements;
 
 
-public class Center : ISingleChildLayoutElement<Control>, IPropertyDeserializable
+public class Center : SingleChildLayoutElement, IPropertyDeserializable
 {
 {
-    public ILayoutElement<Control> Child { get; set; }
-
     public Center(ILayoutElement<Control> child = null)
     public Center(ILayoutElement<Control> child = null)
     {
     {
         Child = child;
         Child = child;
     }
     }
 
 
-    Control ILayoutElement<Control>.Build()
+    public override Control Build()
     {
     {
         return new Panel()
         return new Panel()
         {
         {

+ 9 - 5
src/PixiEditor.Extensions/LayoutBuilding/Elements/Layout.cs

@@ -3,18 +3,22 @@ using PixiEditor.Extensions.CommonApi.LayoutBuilding;
 
 
 namespace PixiEditor.Extensions.LayoutBuilding.Elements;
 namespace PixiEditor.Extensions.LayoutBuilding.Elements;
 
 
-public sealed class Layout : ISingleChildLayoutElement<Control>, IPropertyDeserializable
+public sealed class Layout : SingleChildLayoutElement, IPropertyDeserializable
 {
 {
-    public ILayoutElement<Control> Child { get; set; }
-
     public Layout(ILayoutElement<Control> body = null)
     public Layout(ILayoutElement<Control> body = null)
     {
     {
         Child = body;
         Child = body;
     }
     }
 
 
-    public Control Build()
+    public override Control Build()
     {
     {
-        return new Panel { Children = { Child.Build() } };
+        Panel panel = new Panel();
+        if (Child != null)
+        {
+            panel.Children.Add(Child.Build());
+        }
+
+        return panel;
     }
     }
 
 
     void IPropertyDeserializable.DeserializeProperties(List<object> values)
     void IPropertyDeserializable.DeserializeProperties(List<object> values)

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

@@ -0,0 +1,59 @@
+using Avalonia.Controls;
+using PixiEditor.Extensions.CommonApi.LayoutBuilding;
+using PixiEditor.Extensions.CommonApi.LayoutBuilding.Events;
+
+namespace PixiEditor.Extensions.LayoutBuilding.Elements;
+
+public abstract class LayoutElement : ILayoutElement<Control>
+{
+    private Dictionary<string, List<ElementEventHandler>>? _events;
+    public abstract Control Build();
+
+    public void AddEvent(string eventName, ElementEventHandler eventHandler)
+    {
+        if (_events == null)
+        {
+            _events = new Dictionary<string, List<ElementEventHandler>>();
+        }
+
+        if (!_events.ContainsKey(eventName))
+        {
+            _events.Add(eventName, new List<ElementEventHandler>());
+        }
+
+        _events[eventName].Add(eventHandler);
+    }
+
+    public void RemoveEvent(string eventName, ElementEventHandler eventHandler)
+    {
+        if (_events == null)
+        {
+            return;
+        }
+
+        if (!_events.ContainsKey(eventName))
+        {
+            return;
+        }
+
+        _events[eventName].Remove(eventHandler);
+    }
+
+    public void RaiseEvent(string eventName, ElementEventArgs args)
+    {
+        if (_events == null)
+        {
+            return;
+        }
+
+        if (!_events.ContainsKey(eventName))
+        {
+            return;
+        }
+
+        foreach (ElementEventHandler eventHandler in _events[eventName])
+        {
+            eventHandler.Invoke(args);
+        }
+    }
+}

+ 10 - 0
src/PixiEditor.Extensions/LayoutBuilding/Elements/SingleChildLayoutElement.cs

@@ -0,0 +1,10 @@
+using Avalonia.Controls;
+using PixiEditor.Extensions.CommonApi.LayoutBuilding;
+
+namespace PixiEditor.Extensions.LayoutBuilding.Elements;
+
+public abstract class SingleChildLayoutElement : LayoutElement, ISingleChildLayoutElement<Control>
+{
+    public ILayoutElement<Control>? Child { get; set; }
+    public abstract override Control Build();
+}

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

@@ -3,16 +3,14 @@ using PixiEditor.Extensions.CommonApi.LayoutBuilding;
 
 
 namespace PixiEditor.Extensions.LayoutBuilding.Elements;
 namespace PixiEditor.Extensions.LayoutBuilding.Elements;
 
 
-public class Text : ITextElement<Control>, IPropertyDeserializable
+public class Text : TextElement, IPropertyDeserializable
 {
 {
-    public string Value { get; set; }
-
     public Text(string value = "")
     public Text(string value = "")
     {
     {
         Value = value;
         Value = value;
     }
     }
 
 
-    Control ILayoutElement<Control>.Build()
+    public override Control Build()
     {
     {
         return new TextBlock { Text = Value };
         return new TextBlock { Text = Value };
     }
     }

+ 10 - 0
src/PixiEditor.Extensions/LayoutBuilding/Elements/TextElement.cs

@@ -0,0 +1,10 @@
+using Avalonia.Controls;
+using PixiEditor.Extensions.CommonApi.LayoutBuilding;
+
+namespace PixiEditor.Extensions.LayoutBuilding.Elements;
+
+public abstract class TextElement(string value = "") : LayoutElement, ITextElement<Control>
+{
+    public string Value { get; set; } = value;
+    public abstract override Control Build();
+}

+ 6 - 1
src/WasmSampleExtension/SampleExtension.cs

@@ -16,7 +16,12 @@ public class SampleExtension : WasmExtension
 
 
         Layout layout = new Layout(
         Layout layout = new Layout(
             new Center(
             new Center(
-                child: new Button(new Text("hello sexy."))));
+                child: new Button(
+                    child: new Text("hello sexy."),
+                    onClick: _ => Api.Logger.Log("button clicked!")
+                    )
+                )
+            );
 
 
         Api.WindowProvider.CreatePopupWindow("WASM SampleExtension", layout.Build());
         Api.WindowProvider.CreatePopupWindow("WASM SampleExtension", layout.Build());
     }
     }