Browse Source

DevTools wip and dynamic byte map

Krzysztof Krysiński 1 year ago
parent
commit
f711c915bb
33 changed files with 424 additions and 65 deletions
  1. 17 1
      src/PixiEditor.AvaloniaUI/Helpers/ServiceCollectionHelpers.cs
  2. 4 2
      src/PixiEditor.AvaloniaUI/Models/AppExtensions/ExtensionLoader.cs
  3. 22 0
      src/PixiEditor.AvaloniaUI/Models/AppExtensions/Services/FileFilterExtensions.cs
  4. 24 0
      src/PixiEditor.AvaloniaUI/Models/AppExtensions/Services/FileSystemProvider.cs
  5. 14 0
      src/PixiEditor.DevTools/DevToolsExtension.cs
  6. 23 0
      src/PixiEditor.DevTools/HotReloader.cs
  7. 12 0
      src/PixiEditor.DevTools/Layouts/LiveLayoutPreviewWindow.cs
  8. 28 0
      src/PixiEditor.DevTools/Layouts/LivePreviewWindowState.cs
  9. 22 0
      src/PixiEditor.DevTools/PixiEditor.DevTools.csproj
  10. 21 0
      src/PixiEditor.DevTools/extension.json
  11. 1 11
      src/PixiEditor.Extensions.CommonApi/LayoutBuilding/ByteMap.cs
  12. 2 5
      src/PixiEditor.Extensions.Tests/TestState.cs
  13. 18 0
      src/PixiEditor.Extensions.Wasm/Interop.cs
  14. 17 0
      src/PixiEditor.Extensions.Wasm/native/layout_builder_api.c
  15. 18 4
      src/PixiEditor.Extensions.WasmRuntime/WasmExtensionInstance.cs
  16. 3 1
      src/PixiEditor.Extensions/ExtensionServices.cs
  17. 22 0
      src/PixiEditor.Extensions/IO/FileFilter.cs
  18. 6 0
      src/PixiEditor.Extensions/IO/IFileSystemProvider.cs
  19. 50 0
      src/PixiEditor.Extensions/LayoutBuilding/ElementMap.cs
  20. 5 0
      src/PixiEditor.Extensions/LayoutBuilding/Elements/Button.cs
  21. 5 0
      src/PixiEditor.Extensions/LayoutBuilding/Elements/Center.cs
  22. 3 6
      src/PixiEditor.Extensions/LayoutBuilding/Elements/ContainerState.cs
  23. 6 1
      src/PixiEditor.Extensions/LayoutBuilding/Elements/Layout.cs
  24. 19 3
      src/PixiEditor.Extensions/LayoutBuilding/Elements/LayoutBuilder.cs
  25. 2 1
      src/PixiEditor.Extensions/LayoutBuilding/Elements/State.cs
  26. 2 2
      src/PixiEditor.Extensions/LayoutBuilding/Elements/StatefulContainer.cs
  27. 6 0
      src/PixiEditor.Extensions/LayoutBuilding/Elements/Text.cs
  28. 0 20
      src/PixiEditor.Extensions/LayoutBuilding/GlobalControlFactory.cs
  29. 1 4
      src/PixiEditor.Extensions/LayoutBuilding/IPropertyDeserializable.cs
  30. 47 0
      src/PixiEditor.sln
  31. 1 1
      src/SampleExtension.LayoutBuilder/ButtonTextElementState.cs
  32. 1 1
      src/SampleExtension.LayoutBuilder/SampleExtension.cs
  33. 2 2
      src/SampleExtension/SampleExtension.cs

+ 17 - 1
src/PixiEditor.AvaloniaUI/Helpers/ServiceCollectionHelpers.cs

@@ -1,4 +1,5 @@
 using System.Linq;
+using System.Reflection;
 using Microsoft.Extensions.DependencyInjection;
 using PixiEditor.AvaloniaUI.Models.AppExtensions;
 using PixiEditor.AvaloniaUI.Models.AppExtensions.Services;
@@ -18,6 +19,8 @@ using PixiEditor.AvaloniaUI.ViewModels.Tools;
 using PixiEditor.AvaloniaUI.ViewModels.Tools.Tools;
 using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Extensions.Common.UserPreferences;
+using PixiEditor.Extensions.IO;
+using PixiEditor.Extensions.LayoutBuilding;
 using PixiEditor.Extensions.Palettes;
 using PixiEditor.Extensions.Palettes.Parsers;
 using PixiEditor.Extensions.Windowing;
@@ -109,5 +112,18 @@ internal static class ServiceCollectionHelpers
 
     public static IServiceCollection AddExtensionServices(this IServiceCollection collection, ExtensionLoader loader) =>
         collection.AddSingleton<IWindowProvider, WindowProvider>(x => new WindowProvider(loader, x))
-            .AddSingleton<IPaletteProvider, PaletteProvider>();
+            .AddSingleton<IPaletteProvider, PaletteProvider>()
+            .AddSingleton<ElementMap>(x =>
+            {
+                ElementMap elementMap = new ElementMap();
+                Assembly[] pixiEditorAssemblies = AppDomain.CurrentDomain.GetAssemblies()
+                    .Where(x => x.FullName.StartsWith("PixiEditor")).ToArray();
+                foreach (Assembly assembly in pixiEditorAssemblies)
+                {
+                    elementMap.AddElementsFromAssembly(assembly);
+                }
+
+                return elementMap;
+            })
+            .AddSingleton<IFileSystemProvider, FileSystemProvider>();
 }

+ 4 - 2
src/PixiEditor.AvaloniaUI/Models/AppExtensions/ExtensionLoader.cs

@@ -285,9 +285,11 @@ internal class ExtensionLoader
                 WasmExtensionInstance instance = _wasmRuntime.LoadModule(wasm);
                 return instance;
             }
-            catch
+            catch (Exception ex)
             {
-                // ignored
+#if DEBUG
+                throw;
+#endif
             }
         }
 

+ 22 - 0
src/PixiEditor.AvaloniaUI/Models/AppExtensions/Services/FileFilterExtensions.cs

@@ -0,0 +1,22 @@
+using System.Collections.Generic;
+using Avalonia.Controls;
+using Avalonia.Platform.Storage;
+using PixiEditor.Extensions.IO;
+
+namespace PixiEditor.AvaloniaUI.Models.AppExtensions.Services;
+
+public static class FileFilterExtensions
+{
+    public static FilePickerOpenOptions ToAvaloniaFileFilters(this FileFilter filter)
+    {
+        FilePickerOpenOptions options = new();
+        var avaloniaFilters = new List<FilePickerFileType>();
+        foreach (var filterItem in filter.Filters)
+        {
+            avaloniaFilters.Add(new FilePickerFileType(filterItem.Name) { Patterns = filterItem.Extensions });
+        }
+
+        options.FileTypeFilter = avaloniaFilters;
+        return options;
+    }
+}

+ 24 - 0
src/PixiEditor.AvaloniaUI/Models/AppExtensions/Services/FileSystemProvider.cs

@@ -0,0 +1,24 @@
+using Avalonia;
+using PixiEditor.AvaloniaUI.Helpers.Extensions;
+using PixiEditor.AvaloniaUI.Views;
+using PixiEditor.Extensions.IO;
+
+namespace PixiEditor.AvaloniaUI.Models.AppExtensions.Services;
+
+public class FileSystemProvider : IFileSystemProvider
+{
+    public bool OpenFileDialog(FileFilter filter, out string path)
+    {
+        var task = MainWindow.Current.StorageProvider.OpenFilePickerAsync(filter.ToAvaloniaFileFilters());
+        task.Wait();
+
+        if (task.Result == null || task.Result.Count == 0)
+        {
+            path = null;
+            return false;
+        }
+
+        path = task.Result[0].Path.AbsolutePath;
+        return true;
+    }
+}

+ 14 - 0
src/PixiEditor.DevTools/DevToolsExtension.cs

@@ -0,0 +1,14 @@
+using PixiEditor.DevTools.Layouts;
+using PixiEditor.Extensions;
+
+namespace PixiEditor.DevTools;
+
+public class DevToolsExtension : Extension
+{
+    public static ExtensionServices PixiEditorApi { get; private set; } = null!;
+    protected override void OnInitialized()
+    {
+        PixiEditorApi = Api;
+        Api.Windowing.CreatePopupWindow("Elements UI Builder", new LiveLayoutPreviewWindow().BuildNative()).Show();
+    }
+}

+ 23 - 0
src/PixiEditor.DevTools/HotReloader.cs

@@ -0,0 +1,23 @@
+namespace PixiEditor.DevTools;
+
+public class HotReloader
+{
+    public List<string> WatchedFiles { get; } = new();
+    public Action<string>? OnFileChanged { get; set; }
+
+    private List<FileSystemWatcher> _watchers = new();
+
+    public void WatchFile(string path, string filter)
+    {
+        WatchedFiles.Add(path);
+        FileSystemWatcher watcher = new(path, filter);
+        watcher.Changed += WatcherOnChanged;
+        watcher.EnableRaisingEvents = true;
+        _watchers.Add(watcher);
+    }
+
+    private void WatcherOnChanged(object sender, FileSystemEventArgs e)
+    {
+        OnFileChanged?.Invoke(e.FullPath);
+    }
+}

+ 12 - 0
src/PixiEditor.DevTools/Layouts/LiveLayoutPreviewWindow.cs

@@ -0,0 +1,12 @@
+using PixiEditor.Extensions.CommonApi.LayoutBuilding.Events;
+using PixiEditor.Extensions.LayoutBuilding.Elements;
+
+namespace PixiEditor.DevTools.Layouts;
+
+public class LiveLayoutPreviewWindow : StatefulElement<LivePreviewWindowState>
+{
+    public override LivePreviewWindowState CreateState()
+    {
+        return new();
+    }
+}

+ 28 - 0
src/PixiEditor.DevTools/Layouts/LivePreviewWindowState.cs

@@ -0,0 +1,28 @@
+using PixiEditor.Extensions.CommonApi.LayoutBuilding;
+using PixiEditor.Extensions.CommonApi.LayoutBuilding.Events;
+using PixiEditor.Extensions.IO;
+using PixiEditor.Extensions.LayoutBuilding.Elements;
+
+namespace PixiEditor.DevTools.Layouts;
+
+public class LivePreviewWindowState : State
+{
+    public string? SelectedExtension { get; set; }
+    public override LayoutElement BuildElement()
+    {
+        return new Button(
+            child: SelectedExtension != null ? new Text($"Selected extension: {SelectedExtension}") : new Text("Select extension"),
+            onClick: OnClick);
+    }
+
+    private void OnClick(ElementEventArgs args)
+    {
+        if (DevToolsExtension.PixiEditorApi.FileSystem.OpenFileDialog(new FileFilter().AddFilter("Extension Metadata file", "*.json"), out string? path))
+        {
+            SetState(() =>
+            {
+                SelectedExtension = path;
+            });
+        }
+    }
+}

+ 22 - 0
src/PixiEditor.DevTools/PixiEditor.DevTools.csproj

@@ -0,0 +1,22 @@
+<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\DevTools</OutputPath>
+       <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
+    </PropertyGroup>
+
+    <ItemGroup>
+      <ProjectReference Include="..\PixiEditor.Extensions\PixiEditor.Extensions.csproj" />
+    </ItemGroup>
+
+    <ItemGroup>
+      <None Remove="extension.json" />
+      <Content Include="extension.json">
+        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+      </Content>
+    </ItemGroup>
+
+</Project>

+ 21 - 0
src/PixiEditor.DevTools/extension.json

@@ -0,0 +1,21 @@
+{
+  "displayName": "PixiEditor Dev Tools",
+  "uniqueName": "Pexeedytoo.DevTools",
+  "description": "PixiEditor Dev Tools contains tools for developers to use when developing PixiEditor extensions.",
+  "version": "0.0.1",
+  "author": {
+    "name": "PixiEditor",
+    "email": "[email protected]",
+    "website": "https://pixieditor.net"
+  },
+  "publisher": {
+    "name": "PixiEditor",
+    "email": "[email protected]",
+    "website": "https://pixieditor.net"
+  },
+  "license": "Copyright PixiEditor Organization",
+  "categories": [
+    "Development",
+    "Utilities"
+  ]
+}

+ 1 - 11
src/PixiEditor.Extensions.CommonApi/LayoutBuilding/ByteMap.cs

@@ -2,17 +2,7 @@
 
 public static class ByteMap
 {
-    public static IReadOnlyDictionary<string, int> ControlMap => controlMap;
-
-    private static Dictionary<string, int> controlMap = new Dictionary<string, int>()
-    {
-        // If you do add a new control, make sure to add it to GlobalControlFactory inside PixiEditor project
-        { "Layout", 0 },
-        { "Center", 1 },
-        { "Text", 2 },
-        { "Button", 3 },
-        { "StatefulContainer", 4 }
-    };
+    public static Dictionary<string, int> ControlMap { get; set; } = new Dictionary<string, int>();
 
     public static byte GetTypeByteId(Type type)
     {

+ 2 - 5
src/PixiEditor.Extensions.Tests/TestState.cs

@@ -1,8 +1,5 @@
-using Avalonia.Controls;
-using PixiEditor.Extensions.CommonApi.LayoutBuilding;
-using PixiEditor.Extensions.CommonApi.LayoutBuilding.Events;
+using PixiEditor.Extensions.CommonApi.LayoutBuilding.Events;
 using PixiEditor.Extensions.LayoutBuilding.Elements;
-using Button = PixiEditor.Extensions.LayoutBuilding.Elements.Button;
 
 namespace PixiEditor.Extensions.Test;
 
@@ -12,7 +9,7 @@ public class TestState : State
     public int ClickedTimes { get; private set; } = 0;
     public bool RemoveText { get; set; } = false;
 
-    public override ILayoutElement<Control> Build()
+    public override LayoutElement BuildElement()
     {
         return new Button(
             onClick: OnClick,

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

@@ -2,6 +2,7 @@
 using System.Reflection;
 using System.Runtime.CompilerServices;
 using System.Runtime.InteropServices;
+using System.Text;
 using PixiEditor.Extensions.CommonApi.LayoutBuilding;
 using PixiEditor.Extensions.CommonApi.LayoutBuilding.Events;
 using PixiEditor.Extensions.Wasm.Api.LayoutBuilding;
@@ -48,4 +49,21 @@ internal class Interop
             element.RaiseEvent(eventName ?? "", new ElementEventArgs());
         }
     }
+
+    internal static void SetElementMap(byte[] bytes)
+    {
+        // Dictionary format: [int bytes controlTypeId, string controlTypeName]
+
+        int offset = 0;
+        while (offset < bytes.Length)
+        {
+            int id = BitConverter.ToInt32(bytes, offset);
+            offset += sizeof(int);
+            int nameLength = BitConverter.ToInt32(bytes, offset);
+            offset += sizeof(int);
+            string name = Encoding.UTF8.GetString(bytes, offset, nameLength);
+            offset += nameLength;
+            ByteMap.ControlMap.Add(name, id);
+        }
+    }
 }

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

@@ -26,6 +26,23 @@ void raise_element_event(int elementId, const char* eventName)
     free(method);
 }
 
+__attribute((export_name("set_element_map")))
+void set_element_map(uint8_t* data, int length)
+{
+    MonoArray* array = mono_wasm_obj_array_new(length);
+
+    for (int i = 0; i < length; i++)
+    {
+        mono_array_set(array, uint8_t, i, data[i]);
+    }
+
+    MonoMethod* method = lookup_interop_method("SetElementMap");
+    void* args[] = { array };
+    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);

+ 18 - 4
src/PixiEditor.Extensions.WasmRuntime/WasmExtensionInstance.cs

@@ -4,6 +4,7 @@ using Avalonia.Controls;
 using Avalonia.Threading;
 using PixiEditor.Extensions.CommonApi.LayoutBuilding;
 using PixiEditor.Extensions.CommonApi.LayoutBuilding.State;
+using PixiEditor.Extensions.LayoutBuilding;
 using PixiEditor.Extensions.LayoutBuilding.Elements;
 using Wasmtime;
 
@@ -20,7 +21,7 @@ public class WasmExtensionInstance : Extension
     private Memory memory = null!;
 
     private Dictionary<int, ILayoutElement<Control>> managedElements = new();
-    private LayoutBuilder LayoutBuilder { get; }
+    private LayoutBuilder LayoutBuilder { get; set; }
     private WasmMemoryUtility WasmMemoryUtility { get; set; }
 
     public WasmExtensionInstance(Linker linker, Store store, Module module)
@@ -28,7 +29,6 @@ public class WasmExtensionInstance : Extension
         Linker = linker;
         Store = store;
         Module = module;
-        LayoutBuilder = new LayoutBuilder(managedElements);
     }
 
     public void Instantiate()
@@ -44,6 +44,9 @@ public class WasmExtensionInstance : Extension
 
     protected override void OnInitialized()
     {
+        LayoutBuilder = new LayoutBuilder(managedElements, (ElementMap)Api.Services.GetService(typeof(ElementMap)));
+
+        SetElementMap();
         Instance.GetAction("initialize").Invoke();
         base.OnInitialized();
     }
@@ -54,6 +57,16 @@ public class WasmExtensionInstance : Extension
         base.OnLoaded();
     }
 
+    private void SetElementMap()
+    {
+        var elementMap = (ElementMap)Api.Services.GetService(typeof(ElementMap));
+        byte[] map = elementMap.Serialize();
+        var ptr = WasmMemoryUtility.WriteSpan(map);
+        Instance.GetAction<int, int>("set_element_map").Invoke(ptr, map.Length);
+
+        WasmMemoryUtility.Free(ptr);
+    }
+
     private void DefinePixiEditorApi()
     {
         Linker.DefineFunction("env", "log_message",(int messageOffset, int messageLength) =>
@@ -69,7 +82,7 @@ public class WasmExtensionInstance : Extension
 
             var body = LayoutBuilder.Deserialize(arr, DuplicateResolutionTactic.ThrowException);
 
-            Api.WindowProvider.CreatePopupWindow(title, body.BuildNative()).Show();
+            Api.Windowing.CreatePopupWindow(title, body.BuildNative()).Show();
         });
 
         Linker.DefineFunction("env", "subscribe_to_event", (int controlId, int eventNameOffset, int eventNameLengthOffset) =>
@@ -82,7 +95,8 @@ public class WasmExtensionInstance : Extension
                 var ptr = WasmMemoryUtility.WriteString(eventName);
 
                 action.Invoke(controlId, ptr);
-                //WasmMemoryUtility.Free(nameOffset);
+
+                WasmMemoryUtility.Free(ptr);
             });
         });
 

+ 3 - 1
src/PixiEditor.Extensions/ExtensionServices.cs

@@ -1,4 +1,5 @@
 using Microsoft.Extensions.DependencyInjection;
+using PixiEditor.Extensions.IO;
 using PixiEditor.Extensions.Palettes;
 using PixiEditor.Extensions.Windowing;
 
@@ -7,8 +8,9 @@ namespace PixiEditor.Extensions;
 public class ExtensionServices
 {
     public IServiceProvider Services { get; private set; }
-    public IWindowProvider WindowProvider => Services.GetRequiredService<IWindowProvider>();
+    public IWindowProvider Windowing => Services.GetRequiredService<IWindowProvider>();
     public IPaletteProvider PaletteProvider => Services.GetRequiredService<IPaletteProvider>();
+    public IFileSystemProvider FileSystem => Services.GetRequiredService<IFileSystemProvider>();
 
     public ExtensionServices(IServiceProvider services)
     {

+ 22 - 0
src/PixiEditor.Extensions/IO/FileFilter.cs

@@ -0,0 +1,22 @@
+namespace PixiEditor.Extensions.IO;
+
+public class FileFilter
+{
+    public List<FileFilterItem> Filters { get; set; } = new();
+
+    public FileFilter()
+    {
+    }
+
+    public FileFilter AddFilter(string name, params string[] patterns)
+    {
+        Filters.Add(new FileFilterItem { Name = name, Extensions = patterns.ToList() });
+        return this;
+    }
+}
+
+public class FileFilterItem
+{
+    public string Name { get; set; } = "";
+    public List<string> Extensions { get; set; } = new();
+}

+ 6 - 0
src/PixiEditor.Extensions/IO/IFileSystemProvider.cs

@@ -0,0 +1,6 @@
+namespace PixiEditor.Extensions.IO;
+
+public interface IFileSystemProvider
+{
+    public bool OpenFileDialog(FileFilter filter, out string path);
+}

+ 50 - 0
src/PixiEditor.Extensions/LayoutBuilding/ElementMap.cs

@@ -0,0 +1,50 @@
+using System.Reflection;
+using System.Text;
+using Avalonia.Controls;
+using PixiEditor.Extensions.CommonApi.LayoutBuilding;
+
+namespace PixiEditor.Extensions.LayoutBuilding;
+
+public class ElementMap
+{
+    public IReadOnlyDictionary<int, Type> ControlMap => controlMap;
+
+    // TODO: Code generation
+    private Dictionary<int, Type> controlMap = new Dictionary<int, Type>();
+
+    public ElementMap()
+    {
+
+    }
+
+    public void AddElementsFromAssembly(Assembly assembly)
+    {
+        var types = assembly.GetTypes().Where(x => x.GetInterfaces().Contains(typeof(ILayoutElement<Control>)));
+        int id = controlMap.Count;
+        foreach (var type in types)
+        {
+            controlMap.Add(id, type);
+            id++;
+        }
+    }
+
+    public byte[] Serialize()
+    {
+        // Dictionary format: [int bytes controlTypeId, string controlTypeName]
+        int size = controlMap.Count * (sizeof(int) + 1);
+        List<byte> bytes = new List<byte>(size);
+
+        int offset = 0;
+        foreach (var (key, value) in controlMap)
+        {
+            bytes.AddRange(BitConverter.GetBytes(key));
+            offset++;
+            byte[] nameBytes = Encoding.UTF8.GetBytes(value.Name);
+            bytes.AddRange(BitConverter.GetBytes(nameBytes.Length));
+            bytes.AddRange(nameBytes);
+            offset += nameBytes.Length;
+        }
+
+        return bytes.ToArray();
+    }
+}

+ 5 - 0
src/PixiEditor.Extensions/LayoutBuilding/Elements/Button.cs

@@ -15,6 +15,11 @@ public class Button : SingleChildLayoutElement
         remove => RemoveEvent(nameof(Click), value);
     }
 
+    public Button()
+    {
+
+    }
+
     public Button(ILayoutElement<Control>? child = null, ElementEventHandler? onClick = null)
     {
         Child = child;

+ 5 - 0
src/PixiEditor.Extensions/LayoutBuilding/Elements/Center.cs

@@ -9,6 +9,11 @@ namespace PixiEditor.Extensions.LayoutBuilding.Elements;
 public class Center : SingleChildLayoutElement
 {
     private Panel panel;
+    public Center()
+    {
+
+    }
+
     public Center(ILayoutElement<Control> child = null)
     {
         Child = child;

+ 3 - 6
src/PixiEditor.Extensions/LayoutBuilding/Elements/ContainerState.cs

@@ -1,13 +1,10 @@
-using Avalonia.Controls;
-using PixiEditor.Extensions.CommonApi.LayoutBuilding;
-
-namespace PixiEditor.Extensions.LayoutBuilding.Elements;
+namespace PixiEditor.Extensions.LayoutBuilding.Elements;
 
 public class ContainerState : State
 {
-    public ILayoutElement<Control> Content { get; set; }
+    public LayoutElement Content { get; set; }
 
-    public override ILayoutElement<Control> Build()
+    public override LayoutElement BuildElement()
     {
         return Content;
     }

+ 6 - 1
src/PixiEditor.Extensions/LayoutBuilding/Elements/Layout.cs

@@ -3,8 +3,13 @@ using PixiEditor.Extensions.CommonApi.LayoutBuilding;
 
 namespace PixiEditor.Extensions.LayoutBuilding.Elements;
 
-public sealed class Layout : SingleChildLayoutElement
+public class Layout : SingleChildLayoutElement
 {
+    public Layout()
+    {
+
+    }
+
     public Layout(ILayoutElement<Control> body = null)
     {
         Child = body;

+ 19 - 3
src/PixiEditor.Extensions/LayoutBuilding/Elements/LayoutBuilder.cs

@@ -12,9 +12,11 @@ public class LayoutBuilder
     private static int int32Size = sizeof(int);
 
     Dictionary<int, ILayoutElement<Control>> managedElements = new();
-    public LayoutBuilder(Dictionary<int, ILayoutElement<Control>> managedElements)
+    private ElementMap elementMap;
+    public LayoutBuilder(Dictionary<int, ILayoutElement<Control>> managedElements, ElementMap elementMap)
     {
         this.managedElements = managedElements;
+        this.elementMap = elementMap;
     }
 
     public ILayoutElement<Control> Deserialize(Span<byte> layoutSpan, DuplicateResolutionTactic duplicatedIdTactic)
@@ -81,8 +83,8 @@ public class LayoutBuilder
     private ILayoutElement<Control> BuildLayoutElement(int uniqueId, int controlId, List<object> properties,
         List<ILayoutElement<Control>> children, DuplicateResolutionTactic duplicatedIdTactic)
     {
-        Func<ILayoutElement<Control>> factory = GlobalControlFactory.Map[controlId];
-        var element = factory();
+        Type typeToSpawn = elementMap.ControlMap[controlId];
+        var element = CreateInstance(typeToSpawn);
         
         if(element is not { } layoutElement)
             throw new Exception("Element is not ILayoutElement<Control>");
@@ -120,6 +122,20 @@ public class LayoutBuilder
         return layoutElement;
     }
 
+    private ILayoutElement<Control> CreateInstance(Type typeToSpawn)
+    {
+        var constructor = typeToSpawn.GetConstructor(Type.EmptyTypes);
+        if (constructor != null)
+        {
+            return (ILayoutElement<Control>)Activator.CreateInstance(typeToSpawn);
+        }
+
+        var constructorWithParams = typeToSpawn.GetConstructors()[0];
+        var parameters = constructorWithParams.GetParameters();
+        var parameterValues = parameters.Select(x => x.DefaultValue).ToArray();
+        return (ILayoutElement<Control>)Activator.CreateInstance(typeToSpawn, parameterValues);
+    }
+
     private void RemoveChildren(IChildHost childHost)
     {
         foreach (var child in childHost)

+ 2 - 1
src/PixiEditor.Extensions/LayoutBuilding/Elements/State.cs

@@ -6,7 +6,8 @@ namespace PixiEditor.Extensions.LayoutBuilding.Elements;
 
 public abstract class State : IState<Control>
 {
-    public abstract ILayoutElement<Control> Build();
+    public ILayoutElement<Control> Build() => BuildElement();
+    public abstract LayoutElement BuildElement();
 
     public void SetState(Action setAction)
     {

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

@@ -13,12 +13,12 @@ public class StatefulContainer : StatefulElement<ContainerState>, IChildHost
 
     void IChildHost.DeserializeChildren(List<ILayoutElement<Control>> children)
     {
-        State.Content = children.FirstOrDefault();
+        State.Content = (LayoutElement)children.FirstOrDefault();
     }
 
     public void AddChild(ILayoutElement<Control> child)
     {
-        State.SetState(() => State.Content = child);
+        State.SetState(() => State.Content = (LayoutElement)child);
     }
 
     public void RemoveChild(ILayoutElement<Control> child)

+ 6 - 0
src/PixiEditor.Extensions/LayoutBuilding/Elements/Text.cs

@@ -10,6 +10,12 @@ public class Text : StatelessElement, IPropertyDeserializable
 {
     private string _value = null!;
     public string Value { get => _value; set => SetField(ref _value, value); }
+
+    public Text()
+    {
+
+    }
+
     public Text(string value = "")
     {
         Value = value;

+ 0 - 20
src/PixiEditor.Extensions/LayoutBuilding/GlobalControlFactory.cs

@@ -1,20 +0,0 @@
-using Avalonia.Controls;
-using PixiEditor.Extensions.CommonApi.LayoutBuilding;
-using PixiEditor.Extensions.LayoutBuilding.Elements;
-using Button = PixiEditor.Extensions.LayoutBuilding.Elements.Button;
-
-namespace PixiEditor.Extensions.LayoutBuilding;
-
-public static class GlobalControlFactory
-{
-    public static IReadOnlyDictionary<int, Func<ILayoutElement<Control>>> Map => map;
-
-    private static Dictionary<int, Func<ILayoutElement<Control>>> map = new()
-    {
-        { 0, () => new Layout() },
-        { 1,  () => new Center() },
-        { 2, () => new Text() },
-        { 3, () => new Button() },
-        { 4, () => new StatefulContainer() }
-    };
-}

+ 1 - 4
src/PixiEditor.Extensions/LayoutBuilding/Elements/IPropertyDeserializable.cs → src/PixiEditor.Extensions/LayoutBuilding/IPropertyDeserializable.cs

@@ -1,7 +1,4 @@
-using Avalonia.Controls;
-using PixiEditor.Extensions.CommonApi.LayoutBuilding;
-
-namespace PixiEditor.Extensions.LayoutBuilding.Elements;
+namespace PixiEditor.Extensions.LayoutBuilding;
 
 public interface IPropertyDeserializable
 {

+ 47 - 0
src/PixiEditor.sln

@@ -96,6 +96,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiEditor.Extensions.Commo
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleExtension.LayoutBuilder", "SampleExtension.LayoutBuilder\SampleExtension.LayoutBuilder.csproj", "{6C74CC1F-B514-4150-A46C-84FEA6F9ED7F}"
 EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "OfficialExtensions", "OfficialExtensions", "{DF94111B-9D40-42D7-9416-4875E846FE8A}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiEditor.DevTools", "PixiEditor.DevTools\PixiEditor.DevTools.csproj", "{A0C4E418-467E-40E2-BAD6-35F953BA69E5}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -1651,6 +1655,48 @@ Global
 		{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
+		{A0C4E418-467E-40E2-BAD6-35F953BA69E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{A0C4E418-467E-40E2-BAD6-35F953BA69E5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{A0C4E418-467E-40E2-BAD6-35F953BA69E5}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{A0C4E418-467E-40E2-BAD6-35F953BA69E5}.Debug|x64.Build.0 = Debug|Any CPU
+		{A0C4E418-467E-40E2-BAD6-35F953BA69E5}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{A0C4E418-467E-40E2-BAD6-35F953BA69E5}.Debug|x86.Build.0 = Debug|Any CPU
+		{A0C4E418-467E-40E2-BAD6-35F953BA69E5}.DevRelease|Any CPU.ActiveCfg = Debug|Any CPU
+		{A0C4E418-467E-40E2-BAD6-35F953BA69E5}.DevRelease|Any CPU.Build.0 = Debug|Any CPU
+		{A0C4E418-467E-40E2-BAD6-35F953BA69E5}.DevRelease|x64.ActiveCfg = Debug|Any CPU
+		{A0C4E418-467E-40E2-BAD6-35F953BA69E5}.DevRelease|x64.Build.0 = Debug|Any CPU
+		{A0C4E418-467E-40E2-BAD6-35F953BA69E5}.DevRelease|x86.ActiveCfg = Debug|Any CPU
+		{A0C4E418-467E-40E2-BAD6-35F953BA69E5}.DevRelease|x86.Build.0 = Debug|Any CPU
+		{A0C4E418-467E-40E2-BAD6-35F953BA69E5}.DevSteam|Any CPU.ActiveCfg = Debug|Any CPU
+		{A0C4E418-467E-40E2-BAD6-35F953BA69E5}.DevSteam|Any CPU.Build.0 = Debug|Any CPU
+		{A0C4E418-467E-40E2-BAD6-35F953BA69E5}.DevSteam|x64.ActiveCfg = Debug|Any CPU
+		{A0C4E418-467E-40E2-BAD6-35F953BA69E5}.DevSteam|x64.Build.0 = Debug|Any CPU
+		{A0C4E418-467E-40E2-BAD6-35F953BA69E5}.DevSteam|x86.ActiveCfg = Debug|Any CPU
+		{A0C4E418-467E-40E2-BAD6-35F953BA69E5}.DevSteam|x86.Build.0 = Debug|Any CPU
+		{A0C4E418-467E-40E2-BAD6-35F953BA69E5}.MSIX Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{A0C4E418-467E-40E2-BAD6-35F953BA69E5}.MSIX Debug|Any CPU.Build.0 = Debug|Any CPU
+		{A0C4E418-467E-40E2-BAD6-35F953BA69E5}.MSIX Debug|x64.ActiveCfg = Debug|Any CPU
+		{A0C4E418-467E-40E2-BAD6-35F953BA69E5}.MSIX Debug|x64.Build.0 = Debug|Any CPU
+		{A0C4E418-467E-40E2-BAD6-35F953BA69E5}.MSIX Debug|x86.ActiveCfg = Debug|Any CPU
+		{A0C4E418-467E-40E2-BAD6-35F953BA69E5}.MSIX Debug|x86.Build.0 = Debug|Any CPU
+		{A0C4E418-467E-40E2-BAD6-35F953BA69E5}.MSIX|Any CPU.ActiveCfg = Debug|Any CPU
+		{A0C4E418-467E-40E2-BAD6-35F953BA69E5}.MSIX|Any CPU.Build.0 = Debug|Any CPU
+		{A0C4E418-467E-40E2-BAD6-35F953BA69E5}.MSIX|x64.ActiveCfg = Debug|Any CPU
+		{A0C4E418-467E-40E2-BAD6-35F953BA69E5}.MSIX|x64.Build.0 = Debug|Any CPU
+		{A0C4E418-467E-40E2-BAD6-35F953BA69E5}.MSIX|x86.ActiveCfg = Debug|Any CPU
+		{A0C4E418-467E-40E2-BAD6-35F953BA69E5}.MSIX|x86.Build.0 = Debug|Any CPU
+		{A0C4E418-467E-40E2-BAD6-35F953BA69E5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{A0C4E418-467E-40E2-BAD6-35F953BA69E5}.Release|Any CPU.Build.0 = Release|Any CPU
+		{A0C4E418-467E-40E2-BAD6-35F953BA69E5}.Release|x64.ActiveCfg = Release|Any CPU
+		{A0C4E418-467E-40E2-BAD6-35F953BA69E5}.Release|x64.Build.0 = Release|Any CPU
+		{A0C4E418-467E-40E2-BAD6-35F953BA69E5}.Release|x86.ActiveCfg = Release|Any CPU
+		{A0C4E418-467E-40E2-BAD6-35F953BA69E5}.Release|x86.Build.0 = Release|Any CPU
+		{A0C4E418-467E-40E2-BAD6-35F953BA69E5}.Steam|Any CPU.ActiveCfg = Debug|Any CPU
+		{A0C4E418-467E-40E2-BAD6-35F953BA69E5}.Steam|Any CPU.Build.0 = Debug|Any CPU
+		{A0C4E418-467E-40E2-BAD6-35F953BA69E5}.Steam|x64.ActiveCfg = Debug|Any CPU
+		{A0C4E418-467E-40E2-BAD6-35F953BA69E5}.Steam|x64.Build.0 = Debug|Any CPU
+		{A0C4E418-467E-40E2-BAD6-35F953BA69E5}.Steam|x86.ActiveCfg = Debug|Any CPU
+		{A0C4E418-467E-40E2-BAD6-35F953BA69E5}.Steam|x86.Build.0 = Debug|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -1693,6 +1739,7 @@ Global
 		{C16EF6F1-4E40-4CC4-9320-99C3C97929D7} = {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}
+		{A0C4E418-467E-40E2-BAD6-35F953BA69E5} = {DF94111B-9D40-42D7-9416-4875E846FE8A}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {D04B4AB0-CA33-42FD-A909-79966F9255C5}

+ 1 - 1
src/SampleExtension.LayoutBuilder/ButtonTextElementState.cs

@@ -10,7 +10,7 @@ public class ButtonTextElementState : State
 {
     public int ClickedTimes { get; private set; } = 0;
 
-    public override ILayoutElement<Control> Build()
+    public override LayoutElement BuildElement()
     {
         return new Button(
             onClick: OnClick,

+ 1 - 1
src/SampleExtension.LayoutBuilder/SampleExtension.cs

@@ -18,6 +18,6 @@ public class SampleExtension : Extension
         Layout layout = new Layout(
             body: new ButtonTextElement());
 
-        Api.WindowProvider.CreatePopupWindow("Test layout builder", layout.BuildNative()).Show();
+        Api.Windowing.CreatePopupWindow("Test layout builder", layout.BuildNative()).Show();
     }
 }

+ 2 - 2
src/SampleExtension/SampleExtension.cs

@@ -14,7 +14,7 @@ public class SampleExtension : Extension
 
     protected override void OnInitialized()
     {
-        var popup = Api.WindowProvider.CreatePopupWindow("Hello World!", new TextBlock
+        var popup = Api.Windowing.CreatePopupWindow("Hello World!", new TextBlock
         {
             Text = "Hello World!", Foreground = Brushes.White,
             HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center
@@ -22,6 +22,6 @@ public class SampleExtension : Extension
         Api.PaletteProvider.RegisterDataSource(new TestPaletteDataSource());
         popup.ShowDialog();
 
-        Api.WindowProvider.GetWindow(WindowType.PalettesBrowser).Show();
+        Api.Windowing.GetWindow(WindowType.PalettesBrowser).Show();
     }
 }