Browse Source

Stateless and stateful controls wip

Krzysztof Krysiński 1 year ago
parent
commit
46b108dbfd
37 changed files with 391 additions and 63 deletions
  1. 1 1
      src/PixiEditor.Extensions.CommonApi/LayoutBuilding/ILayoutElement.cs
  2. 0 6
      src/PixiEditor.Extensions.CommonApi/LayoutBuilding/ITextElement.cs
  3. 8 0
      src/PixiEditor.Extensions.CommonApi/LayoutBuilding/State/IState.cs
  4. 11 0
      src/PixiEditor.Extensions.CommonApi/LayoutBuilding/State/IStatefulElement.cs
  5. 6 0
      src/PixiEditor.Extensions.CommonApi/LayoutBuilding/State/IStatelessElement.cs
  6. 27 2
      src/PixiEditor.Extensions.Tests/LayoutBuilderTests.cs
  7. 24 0
      src/PixiEditor.Extensions.Tests/TestState.cs
  8. 11 0
      src/PixiEditor.Extensions.Tests/TestStatefulElement.cs
  9. 27 3
      src/PixiEditor.Extensions.Wasm.Tests/NativeControlSerializationTest.cs
  10. 2 2
      src/PixiEditor.Extensions.Wasm/Api/LayoutBuilding/Button.cs
  11. 2 2
      src/PixiEditor.Extensions.Wasm/Api/LayoutBuilding/Center.cs
  12. 2 2
      src/PixiEditor.Extensions.Wasm/Api/LayoutBuilding/Layout.cs
  13. 1 1
      src/PixiEditor.Extensions.Wasm/Api/LayoutBuilding/LayoutElement.cs
  14. 1 1
      src/PixiEditor.Extensions.Wasm/Api/LayoutBuilding/SingleChildLayoutElement.cs
  15. 12 0
      src/PixiEditor.Extensions.Wasm/Api/LayoutBuilding/StatelessElement.cs
  16. 3 6
      src/PixiEditor.Extensions.Wasm/Api/LayoutBuilding/Text.cs
  17. 0 10
      src/PixiEditor.Extensions.Wasm/Api/LayoutBuilding/TextElement.cs
  18. 1 1
      src/PixiEditor.Extensions.Wasm/Api/Window/WindowProvider.cs
  19. 0 1
      src/PixiEditor.Extensions.Wasm/Interop.cs
  20. 1 1
      src/PixiEditor.Extensions.WasmRuntime/WasmExtensionInstance.cs
  21. 7 3
      src/PixiEditor.Extensions/LayoutBuilding/Elements/Button.cs
  22. 2 2
      src/PixiEditor.Extensions/LayoutBuilding/Elements/Center.cs
  23. 2 2
      src/PixiEditor.Extensions/LayoutBuilding/Elements/Layout.cs
  24. 1 1
      src/PixiEditor.Extensions/LayoutBuilding/Elements/LayoutElement.cs
  25. 1 1
      src/PixiEditor.Extensions/LayoutBuilding/Elements/SingleChildLayoutElement.cs
  26. 19 0
      src/PixiEditor.Extensions/LayoutBuilding/Elements/State.cs
  27. 37 0
      src/PixiEditor.Extensions/LayoutBuilding/Elements/StatefulElement.cs
  28. 18 0
      src/PixiEditor.Extensions/LayoutBuilding/Elements/StatelessElement.cs
  29. 3 3
      src/PixiEditor.Extensions/LayoutBuilding/Elements/Text.cs
  30. 0 10
      src/PixiEditor.Extensions/LayoutBuilding/Elements/TextElement.cs
  31. 45 0
      src/PixiEditor.sln
  32. 11 0
      src/SampleExtension.LayoutBuilder/ButtonTextElement.cs
  33. 24 0
      src/SampleExtension.LayoutBuilder/ButtonTextElementState.cs
  34. 24 0
      src/SampleExtension.LayoutBuilder/SampleExtension.LayoutBuilder.csproj
  35. 23 0
      src/SampleExtension.LayoutBuilder/SampleExtension.cs
  36. 30 0
      src/SampleExtension.LayoutBuilder/extension.json
  37. 4 2
      src/WasmSampleExtension/SampleExtension.cs

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

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

+ 0 - 6
src/PixiEditor.Extensions.CommonApi/LayoutBuilding/ITextElement.cs

@@ -1,6 +0,0 @@
-namespace PixiEditor.Extensions.CommonApi.LayoutBuilding;
-
-public interface ITextElement<out TBuildResult> : ILayoutElement<TBuildResult>
-{
-    public string Value { get; set; }
-}

+ 8 - 0
src/PixiEditor.Extensions.CommonApi/LayoutBuilding/State/IState.cs

@@ -0,0 +1,8 @@
+namespace PixiEditor.Extensions.CommonApi.LayoutBuilding.State;
+
+public interface IState<out TBuild>
+{
+    public ILayoutElement<TBuild> Build();
+    public void SetState(Action setAction);
+    public event Action StateChanged;
+}

+ 11 - 0
src/PixiEditor.Extensions.CommonApi/LayoutBuilding/State/IStatefulElement.cs

@@ -0,0 +1,11 @@
+namespace PixiEditor.Extensions.CommonApi.LayoutBuilding.State;
+
+public interface IStatefulElement<out TBuild> : ILayoutElement<TBuild>
+{
+    public IState<TBuild> State { get; }
+}
+
+public interface IStatefulElement<out TBuild, out TState> : IStatefulElement<TBuild> where TState : IState<TBuild>
+{
+    public TState CreateState();
+}

+ 6 - 0
src/PixiEditor.Extensions.CommonApi/LayoutBuilding/State/IStatelessElement.cs

@@ -0,0 +1,6 @@
+namespace PixiEditor.Extensions.CommonApi.LayoutBuilding.State;
+
+public interface IStatelessElement<out TBuild> : ILayoutElement<TBuild>
+{
+    public ILayoutElement<TBuild> Build();
+}

+ 27 - 2
src/PixiEditor.Extensions.Tests/LayoutBuilderTests.cs

@@ -1,4 +1,5 @@
 using Avalonia.Controls;
 using Avalonia.Controls;
+using Avalonia.Controls.Presenters;
 using Avalonia.Interactivity;
 using Avalonia.Interactivity;
 using PixiEditor.Extensions.CommonApi.LayoutBuilding.Events;
 using PixiEditor.Extensions.CommonApi.LayoutBuilding.Events;
 using PixiEditor.Extensions.LayoutBuilding;
 using PixiEditor.Extensions.LayoutBuilding;
@@ -16,7 +17,7 @@ public class LayoutBuilderTests
             body: new Center(
             body: new Center(
                 child: new Text("Hello")));
                 child: new Text("Hello")));
 
 
-        object result = layout.Build();
+        object result = layout.BuildNative();
 
 
         Assert.IsType<Panel>(result);
         Assert.IsType<Panel>(result);
         Panel grid = (Panel)result;
         Panel grid = (Panel)result;
@@ -56,8 +57,32 @@ public class LayoutBuilderTests
 
 
         button.Click += (e) => callbackFired = true;
         button.Click += (e) => callbackFired = true;
 
 
-        button.Build().RaiseEvent(new RoutedEventArgs(Avalonia.Controls.Button.ClickEvent));
+        button.BuildNative().RaiseEvent(new RoutedEventArgs(Avalonia.Controls.Button.ClickEvent));
 
 
         Assert.True(callbackFired);
         Assert.True(callbackFired);
     }
     }
+
+    [Fact]
+    public void TestStateChangesDataAndRebuildsControls()
+    {
+        TestStatefulElement testStatefulElement = new TestStatefulElement();
+        testStatefulElement.CreateState();
+        var native = testStatefulElement.BuildNative();
+
+        Assert.IsType<ContentPresenter>(native);
+        Assert.IsType<Avalonia.Controls.Button>((native as ContentPresenter).Content);
+
+        Assert.Equal(0, testStatefulElement.State.ClickedTimes);
+
+        ContentPresenter contentPresenter = native as ContentPresenter;
+        Avalonia.Controls.Button button = contentPresenter.Content as Avalonia.Controls.Button;
+
+        button.RaiseEvent(new RoutedEventArgs(Avalonia.Controls.Button.ClickEvent));
+
+        Assert.Equal(1, testStatefulElement.State.ClickedTimes);
+
+        Assert.IsType<ContentPresenter>(native);
+        Assert.IsType<Avalonia.Controls.Button>((native as ContentPresenter).Content);
+        Assert.NotEqual(button, (native as ContentPresenter).Content);
+    }
 }
 }

+ 24 - 0
src/PixiEditor.Extensions.Tests/TestState.cs

@@ -0,0 +1,24 @@
+using Avalonia.Controls;
+using PixiEditor.Extensions.CommonApi.LayoutBuilding;
+using PixiEditor.Extensions.CommonApi.LayoutBuilding.Events;
+using PixiEditor.Extensions.LayoutBuilding.Elements;
+using Button = PixiEditor.Extensions.LayoutBuilding.Elements.Button;
+
+namespace PixiEditor.Extensions.Test;
+
+public class TestState : State
+{
+    public int ClickedTimes { get; private set; } = 0;
+
+    public override ILayoutElement<Control> Build()
+    {
+        return new Button(
+            onClick: OnClick,
+            child: new Text($"Clicked: {ClickedTimes}"));
+    }
+
+    private void OnClick(ElementEventArgs args)
+    {
+        SetState(() => ClickedTimes++);
+    }
+}

+ 11 - 0
src/PixiEditor.Extensions.Tests/TestStatefulElement.cs

@@ -0,0 +1,11 @@
+using PixiEditor.Extensions.LayoutBuilding.Elements;
+
+namespace PixiEditor.Extensions.Test;
+
+public class TestStatefulElement : StatefulElement<TestState>
+{
+    public override TestState CreateState()
+    {
+        return new();
+    }
+}

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

@@ -12,6 +12,9 @@ public class NativeControlSerializationTest
         CompiledControl layout = new CompiledControl(0, "Layout");
         CompiledControl layout = new CompiledControl(0, "Layout");
         layout.AddProperty("Title");
         layout.AddProperty("Title");
 
 
+        int uniqueId = 0;
+        byte[] uniqueIdBytes = BitConverter.GetBytes(uniqueId);
+
         int controlId = ByteMap.ControlMap["Layout"];
         int controlId = ByteMap.ControlMap["Layout"];
         byte[] controlIdBytes = BitConverter.GetBytes(controlId);
         byte[] controlIdBytes = BitConverter.GetBytes(controlId);
 
 
@@ -27,6 +30,7 @@ public class NativeControlSerializationTest
         byte[] childCountBytes = BitConverter.GetBytes(childCount);
         byte[] childCountBytes = BitConverter.GetBytes(childCount);
 
 
         List<byte> expectedBytes = new();
         List<byte> expectedBytes = new();
+        expectedBytes.AddRange(uniqueIdBytes);
         expectedBytes.AddRange(controlIdBytes);
         expectedBytes.AddRange(controlIdBytes);
         expectedBytes.AddRange(propertiesCountBytes);
         expectedBytes.AddRange(propertiesCountBytes);
         expectedBytes.Add(ByteMap.GetTypeByteId(typeof(string)));
         expectedBytes.Add(ByteMap.GetTypeByteId(typeof(string)));
@@ -41,7 +45,10 @@ public class NativeControlSerializationTest
     public void TestThatChildLayoutSerializesCorrectBytes()
     public void TestThatChildLayoutSerializesCorrectBytes()
     {
     {
         CompiledControl layout = new CompiledControl(0, "Layout");
         CompiledControl layout = new CompiledControl(0, "Layout");
-        layout.AddChild(new CompiledControl(0, "Center"));
+        layout.AddChild(new CompiledControl(1, "Center"));
+
+        int uniqueId = 0;
+        byte[] uniqueIdBytes = BitConverter.GetBytes(uniqueId);
 
 
         int controlId = ByteMap.ControlMap["Layout"];
         int controlId = ByteMap.ControlMap["Layout"];
         byte[] controlIdBytes = BitConverter.GetBytes(controlId);
         byte[] controlIdBytes = BitConverter.GetBytes(controlId);
@@ -52,6 +59,9 @@ public class NativeControlSerializationTest
         int childCount = 1;
         int childCount = 1;
         byte[] childCountBytes = BitConverter.GetBytes(childCount);
         byte[] childCountBytes = BitConverter.GetBytes(childCount);
 
 
+        int childUniqueId = 1;
+        byte[] childUniqueIdBytes = BitConverter.GetBytes(childUniqueId);
+
         int childControlId = ByteMap.ControlMap["Center"];
         int childControlId = ByteMap.ControlMap["Center"];
         byte[] childControlIdBytes = BitConverter.GetBytes(childControlId);
         byte[] childControlIdBytes = BitConverter.GetBytes(childControlId);
 
 
@@ -62,9 +72,11 @@ public class NativeControlSerializationTest
         byte[] childChildCountBytes = BitConverter.GetBytes(childChildCount);
         byte[] childChildCountBytes = BitConverter.GetBytes(childChildCount);
 
 
         List<byte> expectedBytes = new();
         List<byte> expectedBytes = new();
+        expectedBytes.AddRange(uniqueIdBytes);
         expectedBytes.AddRange(controlIdBytes);
         expectedBytes.AddRange(controlIdBytes);
         expectedBytes.AddRange(propertiesCountBytes);
         expectedBytes.AddRange(propertiesCountBytes);
         expectedBytes.AddRange(childCountBytes);
         expectedBytes.AddRange(childCountBytes);
+        expectedBytes.AddRange(childUniqueIdBytes);
         expectedBytes.AddRange(childControlIdBytes);
         expectedBytes.AddRange(childControlIdBytes);
         expectedBytes.AddRange(childPropertiesCountBytes);
         expectedBytes.AddRange(childPropertiesCountBytes);
         expectedBytes.AddRange(childChildCountBytes);
         expectedBytes.AddRange(childChildCountBytes);
@@ -82,6 +94,9 @@ public class NativeControlSerializationTest
         center.AddChild(text);
         center.AddChild(text);
         layout.AddChild(center);
         layout.AddChild(center);
 
 
+        int uniqueId = 0;
+        byte[] uniqueIdBytes = BitConverter.GetBytes(uniqueId);
+
         int controlId = ByteMap.ControlMap["Layout"];
         int controlId = ByteMap.ControlMap["Layout"];
         byte[] controlIdBytes = BitConverter.GetBytes(controlId);
         byte[] controlIdBytes = BitConverter.GetBytes(controlId);
 
 
@@ -91,6 +106,9 @@ public class NativeControlSerializationTest
         int childCount = 1;
         int childCount = 1;
         byte[] childCountBytes = BitConverter.GetBytes(childCount);
         byte[] childCountBytes = BitConverter.GetBytes(childCount);
 
 
+        int childUniqueId = 1;
+        byte[] childUniqueIdBytes = BitConverter.GetBytes(childUniqueId);
+
         int childControlId = ByteMap.ControlMap["Center"];
         int childControlId = ByteMap.ControlMap["Center"];
         byte[] childControlIdBytes = BitConverter.GetBytes(childControlId);
         byte[] childControlIdBytes = BitConverter.GetBytes(childControlId);
 
 
@@ -100,6 +118,9 @@ public class NativeControlSerializationTest
         int childChildCount = 1;
         int childChildCount = 1;
         byte[] childChildCountBytes = BitConverter.GetBytes(childChildCount);
         byte[] childChildCountBytes = BitConverter.GetBytes(childChildCount);
 
 
+        int textUniqueId = 2;
+        byte[] textUniqueIdBytes = BitConverter.GetBytes(textUniqueId);
+
         int textControlId = ByteMap.ControlMap["Text"];
         int textControlId = ByteMap.ControlMap["Text"];
         byte[] textControlIdBytes = BitConverter.GetBytes(textControlId);
         byte[] textControlIdBytes = BitConverter.GetBytes(textControlId);
 
 
@@ -116,14 +137,17 @@ public class NativeControlSerializationTest
 
 
 
 
         List<byte> expectedBytes = new();
         List<byte> expectedBytes = new();
+        expectedBytes.AddRange(uniqueIdBytes);
         expectedBytes.AddRange(controlIdBytes);
         expectedBytes.AddRange(controlIdBytes);
         expectedBytes.AddRange(propertiesCountBytes);
         expectedBytes.AddRange(propertiesCountBytes);
         expectedBytes.AddRange(childCountBytes);
         expectedBytes.AddRange(childCountBytes);
 
 
+        expectedBytes.AddRange(childUniqueIdBytes);
         expectedBytes.AddRange(childControlIdBytes);
         expectedBytes.AddRange(childControlIdBytes);
         expectedBytes.AddRange(childPropertiesCountBytes);
         expectedBytes.AddRange(childPropertiesCountBytes);
         expectedBytes.AddRange(childChildCountBytes);
         expectedBytes.AddRange(childChildCountBytes);
 
 
+        expectedBytes.AddRange(textUniqueIdBytes);
         expectedBytes.AddRange(textControlIdBytes);
         expectedBytes.AddRange(textControlIdBytes);
         expectedBytes.AddRange(textPropertiesCountBytes);
         expectedBytes.AddRange(textPropertiesCountBytes);
         expectedBytes.Add(ByteMap.GetTypeByteId(typeof(string)));
         expectedBytes.Add(ByteMap.GetTypeByteId(typeof(string)));
@@ -141,7 +165,7 @@ public class NativeControlSerializationTest
             new Center(
             new Center(
                 child: new Text("hello sexy.")));
                 child: new Text("hello sexy.")));
 
 
-        CompiledControl compiledControl = layout.Build();
+        CompiledControl compiledControl = layout.BuildNative();
 
 
         Assert.Equal("Layout", compiledControl.ControlTypeId);
         Assert.Equal("Layout", compiledControl.ControlTypeId);
         Assert.Empty(compiledControl.Properties);
         Assert.Empty(compiledControl.Properties);
@@ -162,7 +186,7 @@ public class NativeControlSerializationTest
             child: new Text("hello sexy."),
             child: new Text("hello sexy."),
             onClick: _ => { });
             onClick: _ => { });
 
 
-        button.Build();
+        button.BuildNative();
 
 
         Assert.Contains(button.BuildQueuedEvents, x => x == "Click");
         Assert.Contains(button.BuildQueuedEvents, x => x == "Click");
     }
     }

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

@@ -18,11 +18,11 @@ public class Button : SingleChildLayoutElement
             Click += onClick;
             Click += onClick;
     }
     }
 
 
-    public override CompiledControl Build()
+    public override CompiledControl BuildNative()
     {
     {
         CompiledControl button = new CompiledControl(UniqueId, "Button");
         CompiledControl button = new CompiledControl(UniqueId, "Button");
         if (Child != null)
         if (Child != null)
-            button.AddChild(Child.Build());
+            button.AddChild(Child.BuildNative());
 
 
         BuildPendingEvents(button);
         BuildPendingEvents(button);
         return button;
         return button;

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

@@ -9,12 +9,12 @@ public class Center : SingleChildLayoutElement
         Child = child;
         Child = child;
     }
     }
 
 
-    public override CompiledControl Build()
+    public override CompiledControl BuildNative()
     {
     {
         CompiledControl center = new CompiledControl(UniqueId, "Center");
         CompiledControl center = new CompiledControl(UniqueId, "Center");
 
 
         if (Child != null)
         if (Child != null)
-            center.AddChild(Child.Build());
+            center.AddChild(Child.BuildNative());
 
 
         BuildPendingEvents(center);
         BuildPendingEvents(center);
         return center;
         return center;

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

@@ -9,12 +9,12 @@ public sealed class Layout : SingleChildLayoutElement
         Child = body;
         Child = body;
     }
     }
 
 
-    public override CompiledControl Build()
+    public override CompiledControl BuildNative()
     {
     {
         CompiledControl layout = new CompiledControl(UniqueId, "Layout");
         CompiledControl layout = new CompiledControl(UniqueId, "Layout");
 
 
         if (Child != null)
         if (Child != null)
-            layout.AddChild(Child.Build());
+            layout.AddChild(Child.BuildNative());
 
 
         BuildPendingEvents(layout);
         BuildPendingEvents(layout);
         return layout;
         return layout;

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

@@ -9,7 +9,7 @@ public abstract class LayoutElement : ILayoutElement<CompiledControl>
     public List<string> BuildQueuedEvents = new List<string>();
     public List<string> BuildQueuedEvents = new List<string>();
     public int UniqueId { get; set; }
     public int UniqueId { get; set; }
 
 
-    public abstract CompiledControl Build();
+    public abstract CompiledControl BuildNative();
 
 
     public LayoutElement()
     public LayoutElement()
     {
     {

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

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

+ 12 - 0
src/PixiEditor.Extensions.Wasm/Api/LayoutBuilding/StatelessElement.cs

@@ -0,0 +1,12 @@
+using PixiEditor.Extensions.CommonApi.LayoutBuilding;
+using PixiEditor.Extensions.CommonApi.LayoutBuilding.State;
+
+namespace PixiEditor.Extensions.Wasm.Api.LayoutBuilding;
+
+public abstract class StatelessElement : LayoutElement, IStatelessElement<CompiledControl>
+{
+    public ILayoutElement<CompiledControl> Build()
+    {
+        return this;
+    }
+}

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

@@ -2,14 +2,11 @@
 
 
 namespace PixiEditor.Extensions.Wasm.Api.LayoutBuilding;
 namespace PixiEditor.Extensions.Wasm.Api.LayoutBuilding;
 
 
-public class Text : TextElement
+public class Text(string value) : StatelessElement
 {
 {
-    public Text(string value)
-    {
-        Value = value;
-    }
+    public string Value { get; set; } = value;
 
 
-    public override CompiledControl Build()
+    public override CompiledControl BuildNative()
     {
     {
         CompiledControl text = new CompiledControl(UniqueId, "Text");
         CompiledControl text = new CompiledControl(UniqueId, "Text");
         text.AddProperty(Value);
         text.AddProperty(Value);

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

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

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

@@ -9,7 +9,7 @@ public class WindowProvider : IWindowProvider
 {
 {
     public void CreatePopupWindow(string title, LayoutElement body)
     public void CreatePopupWindow(string title, LayoutElement body)
     {
     {
-        CompiledControl compiledControl = body.Build();
+        CompiledControl compiledControl = body.BuildNative();
         byte[] bytes = compiledControl.Serialize().ToArray();
         byte[] bytes = compiledControl.Serialize().ToArray();
         IntPtr ptr = Marshal.AllocHGlobal(bytes.Length);
         IntPtr ptr = Marshal.AllocHGlobal(bytes.Length);
         Marshal.Copy(bytes, 0, ptr, bytes.Length);
         Marshal.Copy(bytes, 0, ptr, bytes.Length);

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

@@ -40,7 +40,6 @@ internal class Interop
 
 
     internal static void EventRaised(int internalControlId, string eventName) //TOOD: Args
     internal static void EventRaised(int internalControlId, string eventName) //TOOD: Args
     {
     {
-        WasmExtension.Api.Logger.Log($"Event raised: {eventName} on {internalControlId}");
         if (LayoutElementsStore.LayoutElements.TryGetValue((int)internalControlId, out ILayoutElement<CompiledControl> element))
         if (LayoutElementsStore.LayoutElements.TryGetValue((int)internalControlId, out ILayoutElement<CompiledControl> element))
         {
         {
             element.RaiseEvent(eventName ?? "", new ElementEventArgs());
             element.RaiseEvent(eventName ?? "", new ElementEventArgs());

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

@@ -71,7 +71,7 @@ public class WasmExtensionInstance : Extension
 
 
             var body = LayoutBuilder.Deserialize(arr);
             var body = LayoutBuilder.Deserialize(arr);
 
 
-            Api.WindowProvider.CreatePopupWindow(title, body.Build()).ShowDialog();
+            Api.WindowProvider.CreatePopupWindow(title, body.BuildNative()).Show();
         });
         });
 
 
         Linker.DefineFunction("env", "subscribe_to_event", (int controlId, int eventNameOffset, int eventNameLengthOffset) =>
         Linker.DefineFunction("env", "subscribe_to_event", (int controlId, int eventNameOffset, int eventNameLengthOffset) =>

+ 7 - 3
src/PixiEditor.Extensions/LayoutBuilding/Elements/Button.cs

@@ -13,16 +13,20 @@ public class Button : SingleChildLayoutElement
         remove => RemoveEvent(nameof(Click), value);
         remove => RemoveEvent(nameof(Click), value);
     }
     }
 
 
-    public Button(ILayoutElement<Control>? child = null)
+    public Button(ILayoutElement<Control>? child = null, ElementEventHandler? onClick = null)
     {
     {
         Child = child;
         Child = child;
+        if (onClick != null)
+        {
+            Click += onClick;
+        }
     }
     }
 
 
-    public override Control Build()
+    public override Control BuildNative()
     {
     {
         Avalonia.Controls.Button btn = new Avalonia.Controls.Button()
         Avalonia.Controls.Button btn = new Avalonia.Controls.Button()
         {
         {
-            Content = Child?.Build(),
+            Content = Child?.BuildNative(),
         };
         };
 
 
         btn.Click += (sender, args) => RaiseEvent(nameof(Click), new ElementEventArgs());
         btn.Click += (sender, args) => RaiseEvent(nameof(Click), new ElementEventArgs());

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

@@ -10,13 +10,13 @@ public class Center : SingleChildLayoutElement, IPropertyDeserializable
         Child = child;
         Child = child;
     }
     }
 
 
-    public override Control Build()
+    public override Control BuildNative()
     {
     {
         return new Panel()
         return new Panel()
         {
         {
             Children =
             Children =
             {
             {
-                Child.Build()
+                Child.BuildNative()
             },
             },
             HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
             HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center,
             VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center
             VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center

+ 2 - 2
src/PixiEditor.Extensions/LayoutBuilding/Elements/Layout.cs

@@ -10,12 +10,12 @@ public sealed class Layout : SingleChildLayoutElement, IPropertyDeserializable
         Child = body;
         Child = body;
     }
     }
 
 
-    public override Control Build()
+    public override Control BuildNative()
     {
     {
         Panel panel = new Panel();
         Panel panel = new Panel();
         if (Child != null)
         if (Child != null)
         {
         {
-            panel.Children.Add(Child.Build());
+            panel.Children.Add(Child.BuildNative());
         }
         }
 
 
         return panel;
         return panel;

+ 1 - 1
src/PixiEditor.Extensions/LayoutBuilding/Elements/LayoutElement.cs

@@ -9,7 +9,7 @@ public abstract class LayoutElement : ILayoutElement<Control>
     public int UniqueId { get; set; }
     public int UniqueId { get; set; }
 
 
     private Dictionary<string, List<ElementEventHandler>>? _events;
     private Dictionary<string, List<ElementEventHandler>>? _events;
-    public abstract Control Build();
+    public abstract Control BuildNative();
 
 
     public void AddEvent(string eventName, ElementEventHandler eventHandler)
     public void AddEvent(string eventName, ElementEventHandler eventHandler)
     {
     {

+ 1 - 1
src/PixiEditor.Extensions/LayoutBuilding/Elements/SingleChildLayoutElement.cs

@@ -6,5 +6,5 @@ namespace PixiEditor.Extensions.LayoutBuilding.Elements;
 public abstract class SingleChildLayoutElement : LayoutElement, ISingleChildLayoutElement<Control>
 public abstract class SingleChildLayoutElement : LayoutElement, ISingleChildLayoutElement<Control>
 {
 {
     public ILayoutElement<Control>? Child { get; set; }
     public ILayoutElement<Control>? Child { get; set; }
-    public abstract override Control Build();
+    public abstract override Control BuildNative();
 }
 }

+ 19 - 0
src/PixiEditor.Extensions/LayoutBuilding/Elements/State.cs

@@ -0,0 +1,19 @@
+using Avalonia.Controls;
+using PixiEditor.Extensions.CommonApi.LayoutBuilding;
+using PixiEditor.Extensions.CommonApi.LayoutBuilding.State;
+
+namespace PixiEditor.Extensions.LayoutBuilding.Elements;
+
+public abstract class State : IState<Control>
+{
+
+    public abstract ILayoutElement<Control> Build();
+
+    public void SetState(Action setAction)
+    {
+        setAction();
+        StateChanged?.Invoke();
+    }
+
+    public event Action? StateChanged;
+}

+ 37 - 0
src/PixiEditor.Extensions/LayoutBuilding/Elements/StatefulElement.cs

@@ -0,0 +1,37 @@
+using Avalonia.Controls;
+using Avalonia.Controls.Presenters;
+using Avalonia.VisualTree;
+using PixiEditor.Extensions.CommonApi.LayoutBuilding.State;
+
+namespace PixiEditor.Extensions.LayoutBuilding.Elements;
+
+public abstract class StatefulElement<TState> : LayoutElement, IStatefulElement<Control, TState> where TState : IState<Control>
+{
+    private TState? _state;
+    private ContentPresenter _presenter = null!;
+
+    IState<Control> IStatefulElement<Control>.State
+    {
+        get
+        {
+            if (_state == null)
+            {
+                _state = CreateState();
+                _state.StateChanged += () => BuildNative();
+            }
+
+            return _state;
+        }
+    }
+
+    public TState State => (TState)((IStatefulElement<Control>)this).State;
+
+    public override Control BuildNative()
+    {
+        _presenter ??= new ContentPresenter();
+        _presenter.Content = State.Build().BuildNative();
+        return _presenter;
+    }
+
+    public abstract TState CreateState();
+}

+ 18 - 0
src/PixiEditor.Extensions/LayoutBuilding/Elements/StatelessElement.cs

@@ -0,0 +1,18 @@
+using Avalonia.Controls;
+using PixiEditor.Extensions.CommonApi.LayoutBuilding;
+using PixiEditor.Extensions.CommonApi.LayoutBuilding.State;
+
+namespace PixiEditor.Extensions.LayoutBuilding.Elements;
+
+public abstract class StatelessElement : LayoutElement, IStatelessElement<Control>
+{
+    public override Control BuildNative()
+    {
+        return Build().BuildNative();
+    }
+
+    public virtual ILayoutElement<Control> Build()
+    {
+        return this;
+    }
+}

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

@@ -1,16 +1,16 @@
 using Avalonia.Controls;
 using Avalonia.Controls;
-using PixiEditor.Extensions.CommonApi.LayoutBuilding;
 
 
 namespace PixiEditor.Extensions.LayoutBuilding.Elements;
 namespace PixiEditor.Extensions.LayoutBuilding.Elements;
 
 
-public class Text : TextElement, IPropertyDeserializable
+public class Text : StatelessElement, IPropertyDeserializable
 {
 {
+    public string Value { get; set; }
     public Text(string value = "")
     public Text(string value = "")
     {
     {
         Value = value;
         Value = value;
     }
     }
 
 
-    public override Control Build()
+    public override Control BuildNative()
     {
     {
         return new TextBlock { Text = Value };
         return new TextBlock { Text = Value };
     }
     }

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

@@ -1,10 +0,0 @@
-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();
-}

+ 45 - 0
src/PixiEditor.sln

@@ -94,6 +94,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiEditor.Extensions.WasmR
 EndProject
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiEditor.Extensions.CommonApi", "PixiEditor.Extensions.CommonApi\PixiEditor.Extensions.CommonApi.csproj", "{43C03D0E-EF50-4225-A268-CB9B8E0E8622}"
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiEditor.Extensions.CommonApi", "PixiEditor.Extensions.CommonApi\PixiEditor.Extensions.CommonApi.csproj", "{43C03D0E-EF50-4225-A268-CB9B8E0E8622}"
 EndProject
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleExtension.LayoutBuilder", "SampleExtension.LayoutBuilder\SampleExtension.LayoutBuilder.csproj", "{6C74CC1F-B514-4150-A46C-84FEA6F9ED7F}"
+EndProject
 Global
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
 		Debug|Any CPU = Debug|Any CPU
@@ -1607,6 +1609,48 @@ Global
 		{43C03D0E-EF50-4225-A268-CB9B8E0E8622}.Steam|x64.Build.0 = Debug|Any CPU
 		{43C03D0E-EF50-4225-A268-CB9B8E0E8622}.Steam|x64.Build.0 = Debug|Any CPU
 		{43C03D0E-EF50-4225-A268-CB9B8E0E8622}.Steam|x86.ActiveCfg = Debug|Any CPU
 		{43C03D0E-EF50-4225-A268-CB9B8E0E8622}.Steam|x86.ActiveCfg = Debug|Any CPU
 		{43C03D0E-EF50-4225-A268-CB9B8E0E8622}.Steam|x86.Build.0 = Debug|Any CPU
 		{43C03D0E-EF50-4225-A268-CB9B8E0E8622}.Steam|x86.Build.0 = Debug|Any CPU
+		{6C74CC1F-B514-4150-A46C-84FEA6F9ED7F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{6C74CC1F-B514-4150-A46C-84FEA6F9ED7F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{6C74CC1F-B514-4150-A46C-84FEA6F9ED7F}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{6C74CC1F-B514-4150-A46C-84FEA6F9ED7F}.Debug|x64.Build.0 = Debug|Any CPU
+		{6C74CC1F-B514-4150-A46C-84FEA6F9ED7F}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{6C74CC1F-B514-4150-A46C-84FEA6F9ED7F}.Debug|x86.Build.0 = Debug|Any CPU
+		{6C74CC1F-B514-4150-A46C-84FEA6F9ED7F}.DevRelease|Any CPU.ActiveCfg = Debug|Any CPU
+		{6C74CC1F-B514-4150-A46C-84FEA6F9ED7F}.DevRelease|Any CPU.Build.0 = Debug|Any CPU
+		{6C74CC1F-B514-4150-A46C-84FEA6F9ED7F}.DevRelease|x64.ActiveCfg = Debug|Any CPU
+		{6C74CC1F-B514-4150-A46C-84FEA6F9ED7F}.DevRelease|x64.Build.0 = Debug|Any CPU
+		{6C74CC1F-B514-4150-A46C-84FEA6F9ED7F}.DevRelease|x86.ActiveCfg = Debug|Any CPU
+		{6C74CC1F-B514-4150-A46C-84FEA6F9ED7F}.DevRelease|x86.Build.0 = Debug|Any CPU
+		{6C74CC1F-B514-4150-A46C-84FEA6F9ED7F}.DevSteam|Any CPU.ActiveCfg = Debug|Any CPU
+		{6C74CC1F-B514-4150-A46C-84FEA6F9ED7F}.DevSteam|Any CPU.Build.0 = Debug|Any CPU
+		{6C74CC1F-B514-4150-A46C-84FEA6F9ED7F}.DevSteam|x64.ActiveCfg = Debug|Any CPU
+		{6C74CC1F-B514-4150-A46C-84FEA6F9ED7F}.DevSteam|x64.Build.0 = Debug|Any CPU
+		{6C74CC1F-B514-4150-A46C-84FEA6F9ED7F}.DevSteam|x86.ActiveCfg = Debug|Any CPU
+		{6C74CC1F-B514-4150-A46C-84FEA6F9ED7F}.DevSteam|x86.Build.0 = Debug|Any CPU
+		{6C74CC1F-B514-4150-A46C-84FEA6F9ED7F}.MSIX Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{6C74CC1F-B514-4150-A46C-84FEA6F9ED7F}.MSIX Debug|Any CPU.Build.0 = Debug|Any CPU
+		{6C74CC1F-B514-4150-A46C-84FEA6F9ED7F}.MSIX Debug|x64.ActiveCfg = Debug|Any CPU
+		{6C74CC1F-B514-4150-A46C-84FEA6F9ED7F}.MSIX Debug|x64.Build.0 = Debug|Any CPU
+		{6C74CC1F-B514-4150-A46C-84FEA6F9ED7F}.MSIX Debug|x86.ActiveCfg = Debug|Any CPU
+		{6C74CC1F-B514-4150-A46C-84FEA6F9ED7F}.MSIX Debug|x86.Build.0 = Debug|Any CPU
+		{6C74CC1F-B514-4150-A46C-84FEA6F9ED7F}.MSIX|Any CPU.ActiveCfg = Debug|Any CPU
+		{6C74CC1F-B514-4150-A46C-84FEA6F9ED7F}.MSIX|Any CPU.Build.0 = Debug|Any CPU
+		{6C74CC1F-B514-4150-A46C-84FEA6F9ED7F}.MSIX|x64.ActiveCfg = Debug|Any CPU
+		{6C74CC1F-B514-4150-A46C-84FEA6F9ED7F}.MSIX|x64.Build.0 = Debug|Any CPU
+		{6C74CC1F-B514-4150-A46C-84FEA6F9ED7F}.MSIX|x86.ActiveCfg = Debug|Any CPU
+		{6C74CC1F-B514-4150-A46C-84FEA6F9ED7F}.MSIX|x86.Build.0 = Debug|Any CPU
+		{6C74CC1F-B514-4150-A46C-84FEA6F9ED7F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{6C74CC1F-B514-4150-A46C-84FEA6F9ED7F}.Release|Any CPU.Build.0 = Release|Any CPU
+		{6C74CC1F-B514-4150-A46C-84FEA6F9ED7F}.Release|x64.ActiveCfg = Release|Any CPU
+		{6C74CC1F-B514-4150-A46C-84FEA6F9ED7F}.Release|x64.Build.0 = Release|Any CPU
+		{6C74CC1F-B514-4150-A46C-84FEA6F9ED7F}.Release|x86.ActiveCfg = Release|Any CPU
+		{6C74CC1F-B514-4150-A46C-84FEA6F9ED7F}.Release|x86.Build.0 = Release|Any CPU
+		{6C74CC1F-B514-4150-A46C-84FEA6F9ED7F}.Steam|Any CPU.ActiveCfg = Debug|Any CPU
+		{6C74CC1F-B514-4150-A46C-84FEA6F9ED7F}.Steam|Any CPU.Build.0 = Debug|Any CPU
+		{6C74CC1F-B514-4150-A46C-84FEA6F9ED7F}.Steam|x64.ActiveCfg = Debug|Any CPU
+		{6C74CC1F-B514-4150-A46C-84FEA6F9ED7F}.Steam|x64.Build.0 = Debug|Any CPU
+		{6C74CC1F-B514-4150-A46C-84FEA6F9ED7F}.Steam|x86.ActiveCfg = Debug|Any CPU
+		{6C74CC1F-B514-4150-A46C-84FEA6F9ED7F}.Steam|x86.Build.0 = Debug|Any CPU
 	EndGlobalSection
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
 		HideSolutionNode = FALSE
@@ -1648,6 +1692,7 @@ Global
 		{9C1A500D-7A3D-49E3-BD39-05867B1D37F1} = {13DD041C-EE2D-4AF8-B43E-D7BFC7415E4D}
 		{9C1A500D-7A3D-49E3-BD39-05867B1D37F1} = {13DD041C-EE2D-4AF8-B43E-D7BFC7415E4D}
 		{C16EF6F1-4E40-4CC4-9320-99C3C97929D7} = {13DD041C-EE2D-4AF8-B43E-D7BFC7415E4D}
 		{C16EF6F1-4E40-4CC4-9320-99C3C97929D7} = {13DD041C-EE2D-4AF8-B43E-D7BFC7415E4D}
 		{43C03D0E-EF50-4225-A268-CB9B8E0E8622} = {13DD041C-EE2D-4AF8-B43E-D7BFC7415E4D}
 		{43C03D0E-EF50-4225-A268-CB9B8E0E8622} = {13DD041C-EE2D-4AF8-B43E-D7BFC7415E4D}
+		{6C74CC1F-B514-4150-A46C-84FEA6F9ED7F} = {E4FF4CE6-5831-450D-8006-0539353C030B}
 	EndGlobalSection
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {D04B4AB0-CA33-42FD-A909-79966F9255C5}
 		SolutionGuid = {D04B4AB0-CA33-42FD-A909-79966F9255C5}

+ 11 - 0
src/SampleExtension.LayoutBuilder/ButtonTextElement.cs

@@ -0,0 +1,11 @@
+using PixiEditor.Extensions.LayoutBuilding.Elements;
+
+namespace SampleExtension.LayoutBuilder;
+
+public class ButtonTextElement : StatefulElement<ButtonTextElementState>
+{
+    public override ButtonTextElementState CreateState()
+    {
+        return new();
+    }
+}

+ 24 - 0
src/SampleExtension.LayoutBuilder/ButtonTextElementState.cs

@@ -0,0 +1,24 @@
+using Avalonia.Controls;
+using PixiEditor.Extensions.CommonApi.LayoutBuilding;
+using PixiEditor.Extensions.CommonApi.LayoutBuilding.Events;
+using PixiEditor.Extensions.LayoutBuilding.Elements;
+using Button = PixiEditor.Extensions.LayoutBuilding.Elements.Button;
+
+namespace SampleExtension.LayoutBuilder;
+
+public class ButtonTextElementState : State
+{
+    public int ClickedTimes { get; private set; } = 0;
+
+    public override ILayoutElement<Control> Build()
+    {
+        return new Button(
+            onClick: OnClick,
+            child: new Text($"Clicked: {ClickedTimes}"));
+    }
+
+    private void OnClick(ElementEventArgs args)
+    {
+        SetState(() => ClickedTimes++);
+    }
+}

+ 24 - 0
src/SampleExtension.LayoutBuilder/SampleExtension.LayoutBuilder.csproj

@@ -0,0 +1,24 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+    <PropertyGroup>
+        <TargetFramework>net8.0</TargetFramework>
+        <ImplicitUsings>enable</ImplicitUsings>
+        <Nullable>enable</Nullable>
+      <OutputPath>..\PixiEditor.AvaloniaUI.Desktop\bin\Debug\net8.0\Extensions</OutputPath>
+    </PropertyGroup>
+
+    <ItemGroup>
+      <ProjectReference Include="..\PixiEditor.Extensions\PixiEditor.Extensions.csproj" />
+    </ItemGroup>
+
+    <ItemGroup>
+      <Content Update="extension.json">
+        <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+      </Content>
+      <None Remove="extension.json" />
+      <Content Include="extension.json">
+        <CopyToOutputDirectory>Always</CopyToOutputDirectory>
+      </Content>
+    </ItemGroup>
+
+</Project>

+ 23 - 0
src/SampleExtension.LayoutBuilder/SampleExtension.cs

@@ -0,0 +1,23 @@
+using Avalonia.Controls;
+using Avalonia.Layout;
+using Avalonia.Media;
+using PixiEditor.Extensions;
+using PixiEditor.Extensions.LayoutBuilding.Elements;
+using PixiEditor.Extensions.Windowing;
+
+namespace SampleExtension.LayoutBuilder;
+
+public class SampleExtension : Extension
+{
+    protected override void OnLoaded()
+    {
+    }
+
+    protected override void OnInitialized()
+    {
+        Layout layout = new Layout(
+            body: new ButtonTextElement());
+
+        Api.WindowProvider.CreatePopupWindow("Test layout builder", layout.BuildNative()).Show();
+    }
+}

+ 30 - 0
src/SampleExtension.LayoutBuilder/extension.json

@@ -0,0 +1,30 @@
+{
+  "displayName": "Sample Extension 2",
+  "uniqueName": "yourCompany.Samples.SampleExtension2",
+  "description": "Sample extension of LayoutBuilder for PixiEditor",
+  "version": "1.0.0",
+  "author": {
+    "name": "PixiEditor",
+    "email": "[email protected]",
+    "website": "https://pixieditor.net"
+  },
+  "publisher": {
+    "name": "PixiEditor",
+    "email": "[email protected]",
+    "website": "https://pixieditor.net"
+  },
+  "contributors": [
+    {
+      "name": "flabbet",
+      "email": "[email protected]",
+      "website": "https://github.com/flabbet"
+    },
+    {
+      "name": "CPK"
+    }
+  ],
+  "license": "MIT",
+  "categories": [
+    "Extension"
+  ]
+}

+ 4 - 2
src/WasmSampleExtension/SampleExtension.cs

@@ -18,8 +18,10 @@ public class SampleExtension : WasmExtension
             new Center(
             new Center(
                 child: new Button(
                 child: new Button(
                     child: new Text("hello sexy."),
                     child: new Text("hello sexy."),
-                    onClick: _ => Api.Logger.Log("button clicked!")
-                    )
+                    onClick: _ =>
+                    {
+                        Api.Logger.Log("button clicked!");
+                    })
                 )
                 )
             );
             );