瀏覽代碼

Somewhat working hot reload

Krzysztof Krysiński 1 年之前
父節點
當前提交
ce810fbef0
共有 34 個文件被更改,包括 431 次插入555 次删除
  1. 0 1
      src/PixiEditor.AvaloniaUI/Views/Dialogs/Debugging/Localization/LocalizationDebugWindow.axaml
  2. 0 37
      src/PixiEditor.DevTools/CsharpCoding/ExtensionAssemblyLoadContext.cs
  3. 0 306
      src/PixiEditor.DevTools/CsharpCoding/ProjectCompiler.cs
  4. 0 109
      src/PixiEditor.DevTools/CsharpCoding/ProjectLoader.cs
  5. 2 7
      src/PixiEditor.DevTools/HotReloader.cs
  6. 33 0
      src/PixiEditor.DevTools/LayoutDeserializer.cs
  7. 7 18
      src/PixiEditor.DevTools/Layouts/LivePreviewWindowState.cs
  8. 1 0
      src/PixiEditor.DevTools/PixiEditor.DevTools.csproj
  9. 0 2
      src/PixiEditor.Extensions.CommonApi/LayoutBuilding/ByteMap.cs
  10. 2 1
      src/PixiEditor.Extensions.CommonApi/LayoutBuilding/LayoutSerializationSpec.md
  11. 25 0
      src/PixiEditor.Extensions.Gen/Helpers.cs
  12. 57 0
      src/PixiEditor.Extensions.Gen/HotReloadGenerator.cs
  13. 19 0
      src/PixiEditor.Extensions.Gen/PixiEditor.Extensions.Gen.csproj
  14. 25 0
      src/PixiEditor.Extensions.MSBuild/GenerateLayoutFilesTask.cs
  15. 12 0
      src/PixiEditor.Extensions.MSBuild/PixiEditor.Extensions.MSBuild.csproj
  16. 11 0
      src/PixiEditor.Extensions.MSBuildLayoutCompiler/PixiEditor.Extensions.MSBuildLayoutCompiler.csproj
  17. 39 0
      src/PixiEditor.Extensions.MSBuildLayoutCompiler/Program.cs
  18. 20 0
      src/PixiEditor.Extensions.Tests/LayoutBuilderTests.cs
  19. 11 0
      src/PixiEditor.Extensions.Tests/TestNestedState.cs
  20. 11 0
      src/PixiEditor.Extensions.Tests/TestNestedStatefulElement.cs
  21. 18 12
      src/PixiEditor.Extensions.Wasm.Tests/NativeControlSerializationTest.cs
  22. 8 1
      src/PixiEditor.Extensions.Wasm/Api/LayoutBuilding/CompiledControl.cs
  23. 0 17
      src/PixiEditor.Extensions.Wasm/Interop.cs
  24. 4 0
      src/PixiEditor.Extensions.Wasm/PixiEditor.Extensions.Wasm.csproj
  25. 3 0
      src/PixiEditor.Extensions.Wasm/build/PixiEditor.Extensions.Wasm.props
  26. 5 1
      src/PixiEditor.Extensions.Wasm/build/PixiEditor.Extensions.Wasm.targets
  27. 0 17
      src/PixiEditor.Extensions.Wasm/native/layout_builder_api.c
  28. 5 7
      src/PixiEditor.Extensions.WasmRuntime/WasmExtensionInstance.cs
  29. 26 7
      src/PixiEditor.Extensions/LayoutBuilding/ElementMap.cs
  30. 11 9
      src/PixiEditor.Extensions/LayoutBuilding/Elements/LayoutBuilder.cs
  31. 29 1
      src/PixiEditor.Extensions/LayoutBuilding/Elements/StatefulElement.cs
  32. 45 0
      src/PixiEditor.sln
  33. 1 1
      src/SampleExtension.LayoutBuilder/ButtonTextElementState.cs
  34. 1 1
      src/WasmSampleExtension/ButtonTextElementState.cs

+ 0 - 1
src/PixiEditor.AvaloniaUI/Views/Dialogs/Debugging/Localization/LocalizationDebugWindow.axaml

@@ -9,7 +9,6 @@
                          xmlns:xaml="clr-namespace:PixiEditor.AvaloniaUI.Models.Commands.XAML"
                          xmlns:converters="clr-namespace:PixiEditor.AvaloniaUI.Helpers.Converters"
                          xmlns:main="clr-namespace:PixiEditor.AvaloniaUI.ViewModels.SubViewModels"
-                         xmlns:debug="clr-namespace:PixiEditor.AvaloniaUI.Views.Dialogs.Debug"
                          xmlns:ui1="clr-namespace:PixiEditor.AvaloniaUI.Helpers.UI"
                          xmlns:globalization="clr-namespace:System.Globalization;assembly=System.Runtime"
                          xmlns:localization1="clr-namespace:PixiEditor.AvaloniaUI.Views.Dialogs.Debugging.Localization"

+ 0 - 37
src/PixiEditor.DevTools/CsharpCoding/ExtensionAssemblyLoadContext.cs

@@ -1,37 +0,0 @@
-using System.Reflection;
-using System.Runtime.Loader;
-
-namespace PixiEditor.DevTools.CsharpCoding;
-
-internal class ExtensionAssemblyLoadContext : System.Runtime.Loader.AssemblyLoadContext
-{
-    public string AssembliesPath { get; set; }
-    public ExtensionAssemblyLoadContext(string assembliesPath) : base(true)
-    {
-        AssembliesPath = assembliesPath;
-        Resolving += OnResolving;
-    }
-
-    private Assembly? OnResolving(AssemblyLoadContext context, AssemblyName name)
-    {
-        string? assemblyFileName = $"{name.Name}.dll";
-        string? assemblyPath = Path.Combine(AssembliesPath, assemblyFileName);
-        if (File.Exists(assemblyPath))
-        {
-            if (name.Name.StartsWith("PixiEditor"))
-            {
-                // load from base context
-                return null;
-            }
-
-            return context.LoadFromAssemblyPath(assemblyPath);
-        }
-
-        return null;
-    }
-
-    protected override Assembly? Load(AssemblyName assemblyName)
-    {
-        return null;
-    }
-}

+ 0 - 306
src/PixiEditor.DevTools/CsharpCoding/ProjectCompiler.cs

@@ -1,306 +0,0 @@
-using System.Diagnostics;
-using System.Reflection;
-using System.Runtime.CompilerServices;
-using Microsoft.Build.Framework;
-using Microsoft.CodeAnalysis;
-using Microsoft.CodeAnalysis.CSharp;
-using Microsoft.CodeAnalysis.Emit;
-using Microsoft.CodeAnalysis.MSBuild;
-using Microsoft.CodeAnalysis.Text;
-using PixiEditor.Extensions.LayoutBuilding.Elements;
-
-namespace PixiEditor.DevTools.CsharpCoding;
-
-public class ProjectCompiler
-{
-    public MSBuildWorkspace Workspace { get; }
-    public List<Project> CsProjects { get; private set; }
-
-    public List<Type> LayoutElementTypes = new List<Type>();
-    public Assembly? CompiledAssembly { get; private set; }
-
-    private Dictionary<Document, SyntaxTree> _cachedSyntaxTrees = new Dictionary<Document, SyntaxTree>();
-    private Compilation? _cachedCompilation;
-    private WeakReference _weakRef;
-
-    public ProjectCompiler(MSBuildWorkspace workspace, List<Project> projects)
-    {
-        Workspace = workspace;
-        CsProjects = projects;
-    }
-
-    [MethodImpl(MethodImplOptions.NoInlining)]
-    public Assembly Compile(bool restore = false)
-    {
-        CompiledAssembly = Compile(GetDocuments());
-        //CompiledAssembly = await CliCompile(restore);
-        if (CompiledAssembly != null)
-        {
-            LayoutElementTypes = CompiledAssembly.GetTypes().Where(x => typeof(LayoutElement).IsAssignableFrom(x))
-                .ToList();
-        }
-
-        return CompiledAssembly;
-    }
-
-    private async Task<Assembly?> CliCompile(bool restore)
-    {
-        var project = CsProjects[^1];
-        if (restore)
-        {
-            RestorePackages(project);
-        }
-
-        // Run dotnet msbuild
-        ProcessStartInfo startInfo = new ProcessStartInfo("dotnet",
-            $"msbuild {project.FilePath} -p:Configuration=Release -p:Platform=x64 -p:OutputPath={Path.GetDirectoryName(project.OutputRefFilePath)}");
-        startInfo.RedirectStandardOutput = true;
-        startInfo.RedirectStandardError = true;
-        startInfo.UseShellExecute = false;
-        Process process = new Process();
-        process.StartInfo = startInfo;
-        process.OutputDataReceived += (sender, args) => Debug.WriteLine(args.Data);
-        process.ErrorDataReceived += (sender, args) => Debug.WriteLine(args.Data);
-        process.Start();
-
-        process.BeginOutputReadLine();
-        process.BeginErrorReadLine();
-        await process.WaitForExitAsync();
-
-        if (process.ExitCode != 0)
-        {
-            return null;
-        }
-
-        return Assembly.LoadFrom(project.OutputRefFilePath);
-    }
-
-    private void RestorePackages(Project project)
-    {
-        ProcessStartInfo startInfo = new ProcessStartInfo("dotnet", $"restore {project.FilePath}")
-        {
-            RedirectStandardOutput = true,
-            RedirectStandardError = true,
-            UseShellExecute = false
-        };
-        Process process = new Process();
-        process.StartInfo = startInfo;
-        process.OutputDataReceived += (sender, args) => Debug.WriteLine(args.Data);
-        process.ErrorDataReceived += (sender, args) => Debug.WriteLine(args.Data);
-        process.Start();
-
-        process.BeginOutputReadLine();
-        process.BeginErrorReadLine();
-        process.WaitForExit();
-    }
-
-    private HashSet<Document> GetDocuments()
-    {
-        HashSet<Document> documents = new HashSet<Document>();
-        for (var i = 0; i < CsProjects.Count; i++)
-        {
-            var project = CsProjects[i];
-            var generated = project.GetSourceGeneratedDocumentsAsync().Result;
-            List<Document> allDocs = new List<Document>(project.Documents.Concat(generated));
-
-            foreach (var document in allDocs)
-            {
-                if (documents.Contains(document) || (i < CsProjects.Count - 1 && IsAssemblyInfo(document.FilePath)))
-                    continue;
-
-                documents.Add(document);
-            }
-        }
-
-        return documents;
-    }
-
-    private bool IsAssemblyInfo(string documentFilePath)
-    {
-        return documentFilePath.EndsWith("AssemblyInfo.cs") || documentFilePath.EndsWith("AssemblyAttributes.cs");
-    }
-
-    public Assembly? Compile(HashSet<Document> documents)
-    {
-        ParseSyntaxTrees(documents);
-
-        var references = CreateReferences();
-
-        CreateCompilation(documents, references);
-        List<ResourceDescription> manifestResources = GetManifestResources();
-
-        using var ms = new MemoryStream();
-        EmitResult result = _cachedCompilation.Emit(ms, manifestResources: manifestResources);
-        LogDiagnostics(result);
-        if (!result.Success)
-        {
-            IEnumerable<Diagnostic> failures = result.Diagnostics.Where(diagnostic =>
-                diagnostic.IsWarningAsError ||
-                diagnostic.Severity == DiagnosticSeverity.Error);
-
-            foreach (Diagnostic diagnostic in failures)
-            {
-                Debug.WriteLine($"{diagnostic.Id}: {diagnostic.GetMessage()}");
-            }
-        }
-        else
-        {
-            LoadCompiledAssembly(ms);
-        }
-
-        return CompiledAssembly;
-    }
-
-    private void LogDiagnostics(EmitResult result)
-    {
-        foreach (var diagnostic in result.Diagnostics)
-        {
-            if (diagnostic.Severity == DiagnosticSeverity.Error)
-                Debug.WriteLine(diagnostic.GetMessage());
-            else
-                Debug.WriteLine(diagnostic.GetMessage());
-        }
-    }
-
-    private List<ResourceDescription> GetManifestResources()
-    {
-        /*TODO: Doesn't work for precompiled XAML sadly*/
-        List<ResourceDescription> manifestResources = new List<ResourceDescription>();
-        foreach (var project in CsProjects)
-        {
-            string dllDir = Path.GetDirectoryName(project.OutputRefFilePath);
-            string objDir = Path.Combine(dllDir, "..");
-            string avaloniaResources = Path.Combine(objDir, "Avalonia");
-            if (Directory.Exists(avaloniaResources))
-            {
-                foreach (var file in Directory.GetFiles(avaloniaResources))
-                {
-                    string fileName = Path.GetFileName(file);
-                    if (fileName == "resources")
-                    {
-                        manifestResources.Add(new ResourceDescription(
-                            "!AvaloniaResources", () => File.OpenRead(file), true));
-                    }
-                }
-            }
-        }
-
-        return manifestResources;
-    }
-
-    private async Task ParseSyntaxTrees(HashSet<Document> documents)
-    {
-        foreach (var document in documents)
-        {
-            SourceText source = await document.GetTextAsync();
-            _cachedSyntaxTrees[document] = CSharpSyntaxTree.ParseText(source);
-        }
-    }
-
-    private List<MetadataReference> CreateReferences()
-    {
-        List<MetadataReference> references = new List<MetadataReference>();
-        references.AddRange(CsProjects.SelectMany(x => x.MetadataReferences));
-        references.Add(MetadataReference.CreateFromFile(Assembly.GetExecutingAssembly().Location));
-
-        return references.Distinct().ToList();
-    }
-
-    private void LoadCompiledAssembly(MemoryStream ms)
-    {
-        ms.Seek(0, SeekOrigin.Begin);
-        CompiledAssembly = Assembly.Load(ms.ToArray());
-        SaveAssembly(ms);
-        LayoutElementTypes = CompiledAssembly.GetTypes().Where(x => typeof(LayoutElement).IsAssignableFrom(x))
-            .ToList();
-    }
-
-    private void SaveAssembly(MemoryStream ms)
-    {
-        string path = Path.Combine(Path.GetDirectoryName(CsProjects[^1].OutputRefFilePath)!, "T.Animaco.Examples.dll");
-        File.WriteAllBytes(path, ms.ToArray());
-    }
-
-    private Assembly? CurrentDomainOnAssemblyResolve(object? sender, ResolveEventArgs args)
-    {
-        string dllDir = Path.GetDirectoryName(CsProjects[^1].OutputRefFilePath)!;
-        string assemblyPath = Path.Combine(dllDir, $"{args.Name.Split(',')[0]}.dll");
-        if (File.Exists(assemblyPath))
-        {
-            return Assembly.LoadFrom(assemblyPath);
-        }
-
-        return null;
-    }
-
-    private async Task CreateCompilation(HashSet<Document> documents, List<MetadataReference> references)
-    {
-        if (_cachedCompilation != null)
-        {
-            foreach (var document in documents)
-            {
-                SyntaxTree? oldSyntaxTree =
-                    _cachedCompilation.SyntaxTrees.FirstOrDefault(x => x.FilePath == document.FilePath);
-                if (oldSyntaxTree != null)
-                {
-                    _cachedCompilation =
-                        _cachedCompilation.ReplaceSyntaxTree(oldSyntaxTree, _cachedSyntaxTrees[document]);
-                }
-                else
-                {
-                    _cachedCompilation = _cachedCompilation.AddSyntaxTrees(_cachedSyntaxTrees[document]);
-                }
-            }
-        }
-        else
-        {
-            _cachedCompilation = await CsProjects[^1].GetCompilationAsync(); /*CSharpCompilation.Create(
-                CsProjects[^1].Name,
-                _cachedSyntaxTrees.Values,
-                references,
-                new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));*/
-        }
-    }
-
-    public LayoutElement GetLayoutElement(string csFileName)
-    {
-        if (CompiledAssembly == null)
-        {
-            throw new InvalidOperationException("Project must be compiled first.");
-        }
-
-        int projIndex = GetProjectIndex(csFileName);
-
-        if (projIndex == -1)
-        {
-            throw new Exception($"No layout with name {csFileName} found");
-        }
-
-        return (LayoutElement)Activator.CreateInstance(LayoutElementTypes[projIndex])!;
-    }
-
-    public int GetProjectIndex(string csFileName)
-    {
-        if (CompiledAssembly == null)
-        {
-            throw new InvalidOperationException("Project must be compiled first.");
-        }
-
-        if (string.IsNullOrEmpty(csFileName))
-        {
-            return 0;
-        }
-
-        string fileName = Path.GetFileNameWithoutExtension(csFileName);
-
-        for (int i = 0; i < LayoutElementTypes.Count; i++)
-        {
-            if (string.Equals(fileName, LayoutElementTypes[i].Name, StringComparison.OrdinalIgnoreCase))
-            {
-                return i;
-            }
-        }
-
-        return -1;
-    }
-}

+ 0 - 109
src/PixiEditor.DevTools/CsharpCoding/ProjectLoader.cs

@@ -1,109 +0,0 @@
-using System.Xml.Linq;
-using System.Xml.XPath;
-using Microsoft.Build.Locator;
-using Microsoft.CodeAnalysis;
-using Microsoft.CodeAnalysis.MSBuild;
-
-namespace PixiEditor.DevTools.CsharpCoding;
-
-public class ProjectLoader
-{
-    public MSBuildWorkspace Workspace { get; private set; }
-    public string ProjectPath { get; private set; }
-    public List<PackageReference> PackageReferences { get; private set; }
-
-    public Project TargetProject { get; private set; }
-    public List<Project> AllProjects { get; private set; }
-
-    public List<string> ReferencedProjectPaths { get; private set; }
-
-    private static readonly string[] _coreProjects = new[] { "PixiEditor.AvaloniaUI" };
-
-    public ProjectLoader(string projectPath)
-    {
-        MSBuildLocator.RegisterDefaults();
-        Dictionary<string, string> props = new Dictionary<string, string>();
-        Workspace = MSBuildWorkspace.Create(props);
-        Workspace.LoadMetadataForReferencedProjects = true;
-
-        /*PackageReferences = LoadPackageReferences(projectPath, out List<string> projects);
-        ReferencedProjectPaths = projects;*/
-        ProjectPath = projectPath;
-    }
-
-    private List<PackageReference> LoadPackageReferences(string projectPath, out List<string> projects)
-    {
-        projects = DigProjectReferences(projectPath, new List<string>());
-        List<PackageReference> packageReferences = new List<PackageReference>();
-        foreach (var project in projects)
-        {
-            packageReferences.AddRange(LoadForProject(project, packageReferences));
-        }
-
-        return packageReferences;
-    }
-
-    private List<string> DigProjectReferences(string projectPath, List<string> existingProjects)
-    {
-        string xml = File.ReadAllText(projectPath);
-        var doc = XDocument.Parse(xml);
-        var projectReferences = doc.XPathSelectElements("//ProjectReference")
-            .Select(pr => pr.Attribute("Include").Value).Except(existingProjects).ToList();
-
-        foreach (var projectReference in projectReferences)
-        {
-            string projectFullPath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(projectPath)!, projectReference));
-            var references = DigProjectReferences(projectFullPath, existingProjects);
-            foreach (var reference in references)
-            {
-                if (!existingProjects.Contains(reference))
-                {
-                    existingProjects.Add(reference);
-                }
-            }
-        }
-
-        existingProjects.Add(projectPath);
-        return existingProjects;
-    }
-
-    private static List<PackageReference> LoadForProject(string projectPath, List<PackageReference> existingPackages)
-    {
-        string xml = File.ReadAllText(projectPath);
-        var doc = XDocument.Parse(xml);
-        var packageReferences = doc.XPathSelectElements("//PackageReference")
-            .Select(pr => new PackageReference
-            {
-                Include = pr.Attribute("Include").Value,
-                Version = new Version(pr.Attribute("Version").Value)
-            });
-
-        return packageReferences.Where(pr => existingPackages.All(ep => ep.Include != pr.Include))
-            .ToList();
-    }
-
-    public void LoadProjects()
-    {
-        AllProjects = new List<Project>();
-        /*foreach (var projectPath in ReferencedProjectPaths)
-        {
-            if(IsAnimacoCoreProject(projectPath))
-                continue;
-            AllProjects.Add(await Workspace.OpenProjectAsync(projectPath));
-        }*/
-
-        AllProjects.Add(Workspace.OpenProjectAsync(ProjectPath).Result);
-        TargetProject = AllProjects[0];
-    }
-
-    private bool IsAnimacoCoreProject(string projectPath)
-    {
-        return _coreProjects.Any(x => projectPath.EndsWith($"{x}.csproj"));
-    }
-}
-
-public class PackageReference
-{
-    public string Include { get; set; }
-    public Version Version { get; set; }
-}

+ 2 - 7
src/PixiEditor.DevTools/HotReloader.cs

@@ -9,8 +9,9 @@ public class HotReloader
 
     public void WatchFile(string path, string filter)
     {
+        string directory = Path.GetDirectoryName(path);
         WatchedFiles.Add(path);
-        FileSystemWatcher watcher = new(path, filter);
+        FileSystemWatcher watcher = new(directory, filter);
         watcher.Changed += WatcherOnChanged;
         watcher.EnableRaisingEvents = true;
         _watchers.Add(watcher);
@@ -29,10 +30,4 @@ public class HotReloader
             (sender as FileSystemWatcher).EnableRaisingEvents = true;
         }
     }
-
-    public void WatchProject(string selectedProjectFile)
-    {
-        string directory = Path.GetDirectoryName(selectedProjectFile);
-        WatchFile(directory, "*.cs");
-    }
 }

+ 33 - 0
src/PixiEditor.DevTools/LayoutDeserializer.cs

@@ -0,0 +1,33 @@
+using PixiEditor.Extensions.LayoutBuilding;
+using PixiEditor.Extensions.LayoutBuilding.Elements;
+
+namespace PixiEditor.DevTools;
+
+public class LayoutDeserializer
+{
+    private LayoutBuilder _builder;
+    public string LayoutFilePath { get; set; }
+
+    public LayoutDeserializer(string layoutFilePath)
+    {
+        LayoutFilePath = layoutFilePath;
+        _builder = new LayoutBuilder(
+            (ElementMap)DevToolsExtension.PixiEditorApi.Services.GetService(typeof(ElementMap)));
+    }
+
+    public LayoutElement DeserializeLayout()
+    {
+        List<byte> bytes = new();
+        using (FileStream fileStream = new(LayoutFilePath, FileMode.Open))
+        {
+            byte[] buffer = new byte[1024];
+            int bytesRead;
+            while ((bytesRead = fileStream.Read(buffer, 0, buffer.Length)) > 0)
+            {
+                bytes.AddRange(buffer.Take(bytesRead));
+            }
+        }
+
+        return (LayoutElement)_builder.Deserialize(bytes.ToArray().AsSpan(), DuplicateResolutionTactic.ReplaceRemoveChildren);
+    }
+}

+ 7 - 18
src/PixiEditor.DevTools/Layouts/LivePreviewWindowState.cs

@@ -1,6 +1,4 @@
-using Microsoft.CodeAnalysis.MSBuild;
-using PixiEditor.DevTools.CsharpCoding;
-using PixiEditor.Extensions.CommonApi.LayoutBuilding.Events;
+using PixiEditor.Extensions.CommonApi.LayoutBuilding.Events;
 using PixiEditor.Extensions.IO;
 using PixiEditor.Extensions.LayoutBuilding.Elements;
 using PixiEditor.Extensions.Runtime;
@@ -9,17 +7,14 @@ namespace PixiEditor.DevTools.Layouts;
 
 public class LivePreviewWindowState : State
 {
-    private ExtensionLoader Loader { get; }
-    private ProjectLoader projectLoader;
-    private ProjectCompiler compiler;
     private HotReloader reloader;
 
     private LayoutElement? _element;
+    private LayoutDeserializer? _deserializer;
     public string? SelectedProjectFile { get; set; }
 
     public LivePreviewWindowState()
     {
-        Loader = new ExtensionLoader(AppDomain.CurrentDomain.BaseDirectory);
         reloader = new HotReloader();
         reloader.OnFileChanged += OnFileChanged;
     }
@@ -39,23 +34,20 @@ public class LivePreviewWindowState : State
 
     private void OnFileChanged(string obj)
     {
-        compiler.Compile();
         SetState(BuildLayoutElement);
     }
 
     private void BuildLayoutElement()
     {
-        var typeToInit = compiler.LayoutElementTypes.FirstOrDefault();
-        if (typeToInit != null)
+        if (_deserializer != null)
         {
-            var instance = (LayoutElement)Activator.CreateInstance(typeToInit);
-            _element = instance;
+            _element = _deserializer.DeserializeLayout();
         }
     }
 
     private void OnClick(ElementEventArgs args)
     {
-        if (DevToolsExtension.PixiEditorApi.FileSystem.OpenFileDialog(new FileFilter().AddFilter("C# project file", "*.csproj"), out string? path))
+        if (DevToolsExtension.PixiEditorApi.FileSystem.OpenFileDialog(new FileFilter().AddFilter("Layout file", "*.layout"), out string? path))
         {
             SetState(() =>
             {
@@ -67,11 +59,8 @@ public class LivePreviewWindowState : State
 
     private void InitProject()
     {
-        projectLoader = new ProjectLoader(SelectedProjectFile);
-        projectLoader.LoadProjects();
-        compiler = new ProjectCompiler(projectLoader.Workspace, projectLoader.AllProjects);
-        reloader.WatchProject(SelectedProjectFile);
-        compiler.Compile();
+        _deserializer = new LayoutDeserializer(SelectedProjectFile);
+        reloader.WatchFile(SelectedProjectFile, "*.layout");
         BuildLayoutElement();
     }
 }

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

@@ -11,6 +11,7 @@
 
     <ItemGroup>
       <ProjectReference Include="..\PixiEditor.Extensions.Runtime\PixiEditor.Extensions.Runtime.csproj" />
+      <ProjectReference Include="..\PixiEditor.Extensions.Wasm\PixiEditor.Extensions.Wasm.csproj" />
       <ProjectReference Include="..\PixiEditor.Extensions\PixiEditor.Extensions.csproj" />
     </ItemGroup>
 

+ 0 - 2
src/PixiEditor.Extensions.CommonApi/LayoutBuilding/ByteMap.cs

@@ -2,8 +2,6 @@
 
 public static class ByteMap
 {
-    public static Dictionary<string, int> ControlMap { get; set; } = new Dictionary<string, int>();
-
     public static byte GetTypeByteId(Type type)
     {
         if (type == typeof(int))

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

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

+ 25 - 0
src/PixiEditor.Extensions.Gen/Helpers.cs

@@ -0,0 +1,25 @@
+using System.Text;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace PixiEditor.Extensions.Gen;
+
+internal static class Helpers
+{
+    public static bool IsLayoutElementOrStateType(SyntaxNode classSymbol, CancellationToken token)
+    {
+        return classSymbol is ClassDeclarationSyntax classDeclarationSyntax &&
+               (IsTypeName(classDeclarationSyntax, "LayoutElement")
+               || IsTypeName(classDeclarationSyntax, "State"));
+    }
+
+    private static bool IsTypeName(ClassDeclarationSyntax classDeclarationSyntax, string name)
+    {
+        return classDeclarationSyntax.BaseList?.Types.Any(x => x.Type is IdentifierNameSyntax identifierNameSyntax && identifierNameSyntax.Identifier.Text == name) == true;
+    }
+
+    public static bool IsLayoutElementOrStateType(Compilation classSymbol, CancellationToken token)
+    {
+        return classSymbol.SyntaxTrees.Any(x => x.GetRoot().DescendantNodes().Any(y => IsLayoutElementOrStateType(y, token)));
+    }
+}

+ 57 - 0
src/PixiEditor.Extensions.Gen/HotReloadGenerator.cs

@@ -0,0 +1,57 @@
+using System.Collections.Immutable;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace PixiEditor.Extensions.Gen
+{
+    [Generator(LanguageNames.CSharp)]
+    public class HotReloadGenerator : IIncrementalGenerator
+    {
+        public void Initialize(IncrementalGeneratorInitializationContext context)
+        {
+            /*var provider = context.SyntaxProvider
+                .CreateSyntaxProvider(
+                    predicate: Helpers.IsLayoutElementOrStateType,
+                    transform: static (context, cancelToken) =>
+                    {
+                        ClassDeclarationSyntax classDeclaration = (ClassDeclarationSyntax)context.Node;
+                        return classDeclaration;
+                    });
+
+            context.RegisterSourceOutput(provider, Generate);*/
+
+            /*var classProvider = context.SyntaxProvider
+                .CreateSyntaxProvider(predicate: Helpers.IsLayoutElementOrStateType,
+                    (ctx, _) => (ClassDeclarationSyntax)ctx.Node);*/
+
+            var compilationProvider = context.CompilationProvider.Select((compilation, token) => Helpers.IsLayoutElementOrStateType(compilation, token));
+
+            context.RegisterSourceOutput(compilationProvider, Generate);
+        }
+
+        private void Generate(SourceProductionContext context, bool arg2)
+        {
+            
+        }
+
+        private void Generate(SourceProductionContext context, ClassDeclarationSyntax syntax)
+        {
+            GenerateHotReloadCode(context, syntax.Identifier.Text);
+        }
+
+        private void GenerateHotReloadCode(SourceProductionContext context, string name)
+        {
+            var ns = " PixiEditor.Extensions.Gen";
+
+            context.AddSource($"{ns}.{name}.prefs.cs", $@"//
+
+            namespace {ns}
+            {{
+               partial class {name}
+               {{
+               }}
+            }}
+            ");
+        }
+    }
+}

+ 19 - 0
src/PixiEditor.Extensions.Gen/PixiEditor.Extensions.Gen.csproj

@@ -0,0 +1,19 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+    <PropertyGroup>
+        <TargetFramework>netstandard2.0</TargetFramework>
+        <ImplicitUsings>enable</ImplicitUsings>
+        <Nullable>enable</Nullable>
+        <LangVersion>latest</LangVersion>
+       <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
+    </PropertyGroup>
+
+    <ItemGroup>
+      <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.8.0" PrivateAssets="all"/>
+      <None Include="$(OutputPath)\$(AssemblyName).dll"
+            Pack="true"
+            PackagePath="analyzers/dotnet/cs"
+            Visible="false" />
+    </ItemGroup>
+
+</Project>

+ 25 - 0
src/PixiEditor.Extensions.MSBuild/GenerateLayoutFilesTask.cs

@@ -0,0 +1,25 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using Microsoft.Build.Framework;
+using Task = Microsoft.Build.Utilities.Task;
+
+namespace PixiEditor.Extensions.MSBuild
+{
+    public class GenerateLayoutFilesTask : Task
+    {
+        [Required]
+        public string AssemblyPath { get; set; } = null!;
+
+        [Output]
+        public string OutputPath { get; set; }
+
+        public override bool Execute()
+        {
+
+
+            return !Log.HasLoggedErrors;
+        }
+    }
+}

+ 12 - 0
src/PixiEditor.Extensions.MSBuild/PixiEditor.Extensions.MSBuild.csproj

@@ -0,0 +1,12 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+    <PropertyGroup>
+        <TargetFramework>netstandard2.0</TargetFramework>
+        <LangVersion>default</LangVersion>
+    </PropertyGroup>
+
+    <ItemGroup>
+      <PackageReference Include="Microsoft.Build.Utilities.Core" Version="17.8.3" />
+    </ItemGroup>
+
+</Project>

+ 11 - 0
src/PixiEditor.Extensions.MSBuildLayoutCompiler/PixiEditor.Extensions.MSBuildLayoutCompiler.csproj

@@ -0,0 +1,11 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+    <PropertyGroup>
+        <OutputType>Exe</OutputType>
+        <TargetFramework>net8.0</TargetFramework>
+        <ImplicitUsings>enable</ImplicitUsings>
+        <Nullable>enable</Nullable>
+        <RootNamespace>PixiEditor.Extensions.LayoutCompiler</RootNamespace>
+    </PropertyGroup>
+
+</Project>

+ 39 - 0
src/PixiEditor.Extensions.MSBuildLayoutCompiler/Program.cs

@@ -0,0 +1,39 @@
+using System.Reflection;
+
+string assemblyPath = args[0];
+string outputPath = args[1];
+
+Console.WriteLine($"Building layouts from path: {Path.GetFullPath(assemblyPath)} to {Path.GetFullPath(outputPath)}");
+
+Assembly assembly = Assembly.LoadFrom(assemblyPath);
+var exportedTypes = assembly.GetExportedTypes();
+
+exportedTypes.Where(x => IsLayoutElement(x)).ToList().ForEach(x =>
+{
+    string path = Path.Combine(outputPath, x.Name + ".layout");
+    if(Directory.Exists(outputPath) == false)
+    {
+        Directory.CreateDirectory(outputPath);
+    }
+
+    File.WriteAllBytes(path, GenerateLayoutFile(x));
+});
+
+
+byte[] GenerateLayoutFile(Type type)
+{
+    object instance = Activator.CreateInstance(type);
+    MethodInfo buildNativeMethod = type.GetMethod("BuildNative");
+    var compiled = buildNativeMethod.Invoke(instance, null);
+    object bytesArray = compiled.GetType().GetMethod("SerializeBytes").Invoke(compiled, null);
+    byte[] bytes = (byte[])bytesArray;
+    return bytes;
+}
+
+bool IsLayoutElement(Type type)
+{
+    if(type.BaseType == null) return false;
+    if(type.BaseType.Name.Contains("LayoutElement")) return true;
+
+    return IsLayoutElement(type.BaseType);
+}

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

@@ -179,4 +179,24 @@ public class LayoutBuilderTests
 
         Assert.Equal(2, innerPanel.Children.Count);
     }
+
+    [Fact]
+    public void TestThatNestedStatefulElementsAreUpdatedCorrectly()
+    {
+        //TODO: Make this test
+        /*TestNestedStatefulElement testStatefulElement = new TestNestedStatefulElement();
+        testStatefulElement.CreateState();
+
+        var native = testStatefulElement.BuildNative();
+
+        Assert.IsType<ContentPresenter>(native);
+        Assert.IsType<ContentPresenter>((native as ContentPresenter).Content);
+        ContentPresenter innerPresenter = (native as ContentPresenter).Content as ContentPresenter;
+
+        Assert.IsType<Avalonia.Controls.Button>(innerPresenter.Content);
+        Avalonia.Controls.Button button = (innerPresenter.Content as Avalonia.Controls.Button);
+
+        Assert.IsType<TextBlock>(button.Content);
+        TextBlock textBlock = button.Content as TextBlock;*/
+    }
 }

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

@@ -0,0 +1,11 @@
+using PixiEditor.Extensions.LayoutBuilding.Elements;
+
+namespace PixiEditor.Extensions.Test;
+
+public class TestNestedState : State
+{
+    public override LayoutElement BuildElement()
+    {
+        return new TestStatefulElement();
+    }
+}

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

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

+ 18 - 12
src/PixiEditor.Extensions.Wasm.Tests/NativeControlSerializationTest.cs

@@ -15,8 +15,8 @@ public class NativeControlSerializationTest
         int uniqueId = 0;
         byte[] uniqueIdBytes = BitConverter.GetBytes(uniqueId);
 
-        int controlId = ByteMap.ControlMap["Layout"];
-        byte[] controlIdBytes = BitConverter.GetBytes(controlId);
+        string controlId = "Layout";
+        byte[] controlIdBytes = Encoding.UTF8.GetBytes(controlId);
 
         int propertiesCount = 1;
         byte[] propertiesCountBytes = BitConverter.GetBytes(propertiesCount);
@@ -31,6 +31,7 @@ public class NativeControlSerializationTest
 
         List<byte> expectedBytes = new();
         expectedBytes.AddRange(uniqueIdBytes);
+        expectedBytes.AddRange(BitConverter.GetBytes(controlId.Length));
         expectedBytes.AddRange(controlIdBytes);
         expectedBytes.AddRange(propertiesCountBytes);
         expectedBytes.Add(ByteMap.GetTypeByteId(typeof(string)));
@@ -50,8 +51,8 @@ public class NativeControlSerializationTest
         int uniqueId = 0;
         byte[] uniqueIdBytes = BitConverter.GetBytes(uniqueId);
 
-        int controlId = ByteMap.ControlMap["Layout"];
-        byte[] controlIdBytes = BitConverter.GetBytes(controlId);
+        string controlId = "Layout";
+        byte[] controlIdBytes = Encoding.UTF8.GetBytes(controlId);
 
         int propertiesCount = 0;
         byte[] propertiesCountBytes = BitConverter.GetBytes(propertiesCount);
@@ -62,8 +63,8 @@ public class NativeControlSerializationTest
         int childUniqueId = 1;
         byte[] childUniqueIdBytes = BitConverter.GetBytes(childUniqueId);
 
-        int childControlId = ByteMap.ControlMap["Center"];
-        byte[] childControlIdBytes = BitConverter.GetBytes(childControlId);
+        string childControlId = "Center";
+        byte[] childControlIdBytes = Encoding.UTF8.GetBytes(childControlId);
 
         int childPropertiesCount = 0;
         byte[] childPropertiesCountBytes = BitConverter.GetBytes(childPropertiesCount);
@@ -73,10 +74,12 @@ public class NativeControlSerializationTest
 
         List<byte> expectedBytes = new();
         expectedBytes.AddRange(uniqueIdBytes);
+        expectedBytes.AddRange(BitConverter.GetBytes(controlId.Length));
         expectedBytes.AddRange(controlIdBytes);
         expectedBytes.AddRange(propertiesCountBytes);
         expectedBytes.AddRange(childCountBytes);
         expectedBytes.AddRange(childUniqueIdBytes);
+        expectedBytes.AddRange(BitConverter.GetBytes(childControlId.Length));
         expectedBytes.AddRange(childControlIdBytes);
         expectedBytes.AddRange(childPropertiesCountBytes);
         expectedBytes.AddRange(childChildCountBytes);
@@ -97,8 +100,8 @@ public class NativeControlSerializationTest
         int uniqueId = 0;
         byte[] uniqueIdBytes = BitConverter.GetBytes(uniqueId);
 
-        int controlId = ByteMap.ControlMap["Layout"];
-        byte[] controlIdBytes = BitConverter.GetBytes(controlId);
+        string controlId = "Layout";
+        byte[] controlIdBytes = Encoding.UTF8.GetBytes(controlId);
 
         int propertiesCount = 0;
         byte[] propertiesCountBytes = BitConverter.GetBytes(propertiesCount);
@@ -109,8 +112,8 @@ public class NativeControlSerializationTest
         int childUniqueId = 1;
         byte[] childUniqueIdBytes = BitConverter.GetBytes(childUniqueId);
 
-        int childControlId = ByteMap.ControlMap["Center"];
-        byte[] childControlIdBytes = BitConverter.GetBytes(childControlId);
+        string childControlId = "Center";
+        byte[] childControlIdBytes = Encoding.UTF8.GetBytes(childControlId);
 
         int childPropertiesCount = 0;
         byte[] childPropertiesCountBytes = BitConverter.GetBytes(childPropertiesCount);
@@ -121,8 +124,8 @@ public class NativeControlSerializationTest
         int textUniqueId = 2;
         byte[] textUniqueIdBytes = BitConverter.GetBytes(textUniqueId);
 
-        int textControlId = ByteMap.ControlMap["Text"];
-        byte[] textControlIdBytes = BitConverter.GetBytes(textControlId);
+        string textControlId = "Text";
+        byte[] textControlIdBytes = Encoding.UTF8.GetBytes(textControlId);
 
         int textPropertiesCount = 1;
         byte[] textPropertiesCountBytes = BitConverter.GetBytes(textPropertiesCount);
@@ -138,16 +141,19 @@ public class NativeControlSerializationTest
 
         List<byte> expectedBytes = new();
         expectedBytes.AddRange(uniqueIdBytes);
+        expectedBytes.AddRange(BitConverter.GetBytes(controlId.Length));
         expectedBytes.AddRange(controlIdBytes);
         expectedBytes.AddRange(propertiesCountBytes);
         expectedBytes.AddRange(childCountBytes);
 
         expectedBytes.AddRange(childUniqueIdBytes);
+        expectedBytes.AddRange(BitConverter.GetBytes(childControlId.Length));
         expectedBytes.AddRange(childControlIdBytes);
         expectedBytes.AddRange(childPropertiesCountBytes);
         expectedBytes.AddRange(childChildCountBytes);
 
         expectedBytes.AddRange(textUniqueIdBytes);
+        expectedBytes.AddRange(BitConverter.GetBytes(textControlId.Length));
         expectedBytes.AddRange(textControlIdBytes);
         expectedBytes.AddRange(textPropertiesCountBytes);
         expectedBytes.Add(ByteMap.GetTypeByteId(typeof(string)));

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

@@ -38,13 +38,20 @@ public class CompiledControl
         return Serialize(new List<byte>()).ToArray();
     }
 
+    public byte[] SerializeBytes()
+    {
+        return Serialize(new List<byte>()).ToArray();
+    }
+
     private List<byte> Serialize(List<byte> bytes)
     {
         // TODO: Make it more efficient
 
         byte[] uniqueIdBytes = BitConverter.GetBytes(UniqueId);
         bytes.AddRange(uniqueIdBytes);
-        byte[] idBytes = BitConverter.GetBytes(ByteMap.ControlMap[ControlTypeId]);
+        byte[] idLengthBytes = BitConverter.GetBytes(ControlTypeId.Length);
+        bytes.AddRange(idLengthBytes);
+        byte[] idBytes = Encoding.UTF8.GetBytes(ControlTypeId);
         bytes.AddRange(idBytes);
         bytes.AddRange(BitConverter.GetBytes(Properties.Count));
         bytes.AddRange(SerializeProperties());

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

@@ -49,21 +49,4 @@ 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);
-        }
-    }
 }

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

@@ -15,6 +15,10 @@
       <ClCompile Include="native\layout_builder_api.c" />
     </ItemGroup>
 
+    <ItemGroup>
+      <PackageReference Include="Microsoft.Build.Utilities.Core" Version="17.8.3" />
+    </ItemGroup>
+
   <Import Project="build\PixiEditor.Extensions.Wasm.targets"/>
   <Import Project="build\PixiEditor.Extensions.Wasm.props"/>
 

+ 3 - 0
src/PixiEditor.Extensions.Wasm/build/PixiEditor.Extensions.Wasm.props

@@ -1,2 +1,5 @@
 <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+
+
+
 </Project>

+ 5 - 1
src/PixiEditor.Extensions.Wasm/build/PixiEditor.Extensions.Wasm.targets

@@ -1,8 +1,12 @@
 <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
-
   <ItemGroup>
     <WasiNativeFileReference Include="$(MSBuildThisFileDirectory)..\native\*.c" />
     <WasiAfterRuntimeLoaded Include="attach_internal_calls" />
   </ItemGroup>
 
+  <Target Name="GenerateLayouts" AfterTargets="Build" Inputs="bin\Debug\$(TargetFramework)\PixiEditor.Extensions.Wasm.dll" Outputs="$(RootFolder)\Layouts\">
+    <Exec ConsoleToMSBuild="true"
+          Command="..\PixiEditor.Extensions.MSBuildLayoutCompiler\bin\Debug\net8.0\PixiEditor.Extensions.MSBuildLayoutCompiler.exe $(OutputPath)\$(MSBuildProjectName).dll $(OutputPath)\Layouts\"/>
+  </Target>
+
 </Project>

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

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

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

@@ -19,8 +19,6 @@ public class WasmExtensionInstance : Extension
     private Module Module { get; }
 
     private Memory memory = null!;
-
-    private Dictionary<int, ILayoutElement<Control>> managedElements = new();
     private LayoutBuilder LayoutBuilder { get; set; }
     private WasmMemoryUtility WasmMemoryUtility { get; set; }
 
@@ -44,9 +42,9 @@ public class WasmExtensionInstance : Extension
 
     protected override void OnInitialized()
     {
-        LayoutBuilder = new LayoutBuilder(managedElements, (ElementMap)Api.Services.GetService(typeof(ElementMap)));
+        LayoutBuilder = new LayoutBuilder((ElementMap)Api.Services.GetService(typeof(ElementMap)));
 
-        SetElementMap();
+        //SetElementMap();
         Instance.GetAction("initialize").Invoke();
         base.OnInitialized();
     }
@@ -89,7 +87,7 @@ public class WasmExtensionInstance : Extension
         {
             string eventName = WasmMemoryUtility.GetString(eventNameOffset, eventNameLengthOffset);
 
-            managedElements[controlId].AddEvent(eventName, (args) =>
+            LayoutBuilder.ManagedElements[controlId].AddEvent(eventName, (args) =>
             {
                 var action = Instance.GetAction<int, int>("raise_element_event");
                 var ptr = WasmMemoryUtility.WriteString(eventName);
@@ -104,12 +102,12 @@ public class WasmExtensionInstance : Extension
         {
             Span<byte> arr = memory.GetSpan<byte>(bodyOffset, bodyLength);
 
-            var element = managedElements[controlId];
+            var element = LayoutBuilder.ManagedElements[controlId];
             var body = LayoutBuilder.Deserialize(arr, DuplicateResolutionTactic.ReplaceRemoveChildren);
 
             Dispatcher.UIThread.InvokeAsync(() =>
             {
-                managedElements[controlId] = element;
+                LayoutBuilder.ManagedElements[controlId] = element;
                 if (element is StatefulContainer statefulElement && body is StatefulContainer statefulBodyElement)
                 {
                     statefulElement.State.SetState(() => statefulElement.State.Content = statefulBodyElement.State.Content);

+ 26 - 7
src/PixiEditor.Extensions/LayoutBuilding/ElementMap.cs

@@ -7,10 +7,10 @@ namespace PixiEditor.Extensions.LayoutBuilding;
 
 public class ElementMap
 {
-    public IReadOnlyDictionary<int, Type> ControlMap => controlMap;
+    public IReadOnlyDictionary<string, Type> ControlMap => controlMap;
 
     // TODO: Code generation
-    private Dictionary<int, Type> controlMap = new Dictionary<int, Type>();
+    private Dictionary<string, Type> controlMap = new Dictionary<string, Type>();
 
     public ElementMap()
     {
@@ -20,24 +20,24 @@ public class 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++;
+            controlMap.Add(type.Name, type); // TODO: Extension unique name prefix?
         }
     }
 
     public byte[] Serialize()
     {
-        // Dictionary format: [int bytes controlTypeId, string controlTypeName]
+        // Dictionary format: [string 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));
+            bytes.AddRange(BitConverter.GetBytes(key.Length));
+            offset += sizeof(int);
+            byte[] keyBytes = Encoding.UTF8.GetBytes(key);
             offset++;
             byte[] nameBytes = Encoding.UTF8.GetBytes(value.Name);
             bytes.AddRange(BitConverter.GetBytes(nameBytes.Length));
@@ -47,4 +47,23 @@ public class ElementMap
 
         return bytes.ToArray();
     }
+
+    public void Deserialize(byte[] bytes)
+    {
+        int offset = 0;
+        while (offset < bytes.Length)
+        {
+            int controlTypeIdLength = BitConverter.ToInt32(bytes, offset);
+            offset += sizeof(int);
+
+            string controlTypeId = Encoding.UTF8.GetString(bytes, offset, controlTypeIdLength);
+            offset += controlTypeIdLength;
+
+            int nameLength = BitConverter.ToInt32(bytes, offset);
+            offset += sizeof(int);
+            string name = Encoding.UTF8.GetString(bytes, offset, nameLength);
+            offset += nameLength;
+            controlMap.Add(controlTypeId, Type.GetType(name));
+        }
+    }
 }

+ 11 - 9
src/PixiEditor.Extensions/LayoutBuilding/Elements/LayoutBuilder.cs

@@ -11,11 +11,10 @@ public class LayoutBuilder
 {
     private static int int32Size = sizeof(int);
 
-    Dictionary<int, ILayoutElement<Control>> managedElements = new();
+    public Dictionary<int, ILayoutElement<Control>> ManagedElements = new();
     private ElementMap elementMap;
-    public LayoutBuilder(Dictionary<int, ILayoutElement<Control>> managedElements, ElementMap elementMap)
+    public LayoutBuilder(ElementMap elementMap)
     {
-        this.managedElements = managedElements;
         this.elementMap = elementMap;
     }
 
@@ -26,9 +25,12 @@ public class LayoutBuilder
         int uniqueId = BitConverter.ToInt32(layoutSpan[offset..(offset + int32Size)]);
         offset += int32Size;
 
-        int controlTypeId = BitConverter.ToInt32(layoutSpan[offset..(offset + int32Size)]);
+        int controlTypeIdLength = BitConverter.ToInt32(layoutSpan[offset..(offset + int32Size)]);
         offset += int32Size;
 
+        string controlTypeId = Encoding.UTF8.GetString(layoutSpan[offset..(offset + controlTypeIdLength)]);
+        offset += controlTypeIdLength;
+
         int propertiesCount = BitConverter.ToInt32(layoutSpan[offset..(offset + int32Size)]);
         offset += int32Size;
 
@@ -80,7 +82,7 @@ public class LayoutBuilder
         return children;
     }
 
-    private ILayoutElement<Control> BuildLayoutElement(int uniqueId, int controlId, List<object> properties,
+    private ILayoutElement<Control> BuildLayoutElement(int uniqueId, string controlId, List<object> properties,
         List<ILayoutElement<Control>> children, DuplicateResolutionTactic duplicatedIdTactic)
     {
         Type typeToSpawn = elementMap.ControlMap[controlId];
@@ -101,7 +103,7 @@ public class LayoutBuilder
             customChildrenDeserializable.DeserializeChildren(children);
         }
 
-        if (!managedElements.TryAdd(uniqueId, layoutElement))
+        if (!ManagedElements.TryAdd(uniqueId, layoutElement))
         {
             if (duplicatedIdTactic == DuplicateResolutionTactic.ThrowException)
             {
@@ -110,12 +112,12 @@ public class LayoutBuilder
 
             if (duplicatedIdTactic == DuplicateResolutionTactic.ReplaceRemoveChildren)
             {
-                if (managedElements[uniqueId] is IChildHost childrenDeserializable)
+                if (ManagedElements[uniqueId] is IChildHost childrenDeserializable)
                 {
                     RemoveChildren(childrenDeserializable);
                 }
 
-                managedElements[uniqueId] = layoutElement;
+                ManagedElements[uniqueId] = layoutElement;
             }
         }
 
@@ -140,7 +142,7 @@ public class LayoutBuilder
     {
         foreach (var child in childHost)
         {
-            managedElements.Remove(child.UniqueId);
+            ManagedElements.Remove(child.UniqueId);
             if (child is IChildHost childChildrenDeserializable)
             {
                 RemoveChildren(childChildrenDeserializable);

+ 29 - 1
src/PixiEditor.Extensions/LayoutBuilding/Elements/StatefulElement.cs

@@ -1,12 +1,13 @@
 using System.Collections;
 using Avalonia.Controls;
 using Avalonia.Controls.Presenters;
+using Avalonia.Threading;
 using PixiEditor.Extensions.CommonApi.LayoutBuilding;
 using PixiEditor.Extensions.CommonApi.LayoutBuilding.State;
 
 namespace PixiEditor.Extensions.LayoutBuilding.Elements;
 
-public abstract class StatefulElement<TState> : LayoutElement, IStatefulElement<Control, TState> where TState : IState<Control>
+public abstract class StatefulElement<TState> : LayoutElement, /*IPropertyDeserializable,*/ IStatefulElement<Control, TState> where TState : IState<Control>
 {
     private TState? _state;
     private ContentPresenter _presenter = null!;
@@ -109,4 +110,31 @@ public abstract class StatefulElement<TState> : LayoutElement, IStatefulElement<
             propertyDeserializable.DeserializeProperties(fromProps.GetProperties());
         }
     }
+
+    /*IEnumerable<object> IPropertyDeserializable.GetProperties()
+    {
+        if (_content == null)
+        {
+            Dispatcher.UIThread.Invoke(() =>
+            {
+                BuildNative();
+            });
+        }
+
+        yield return _content;
+    }
+
+    void IPropertyDeserializable.DeserializeProperties(IEnumerable<object> values)
+    {
+        if (values == null || !values.Any())
+        {
+            return;
+        }
+
+        object first = values.ElementAt(0);
+        if(first is ILayoutElement<Control> layoutElement)
+        {
+            ContentFromLayout(layoutElement);
+        }
+    }*/
 }

+ 45 - 0
src/PixiEditor.sln

@@ -104,6 +104,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiEditor.Extensions.Runti
 EndProject
 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{F1FDFA82-0C74-446A-AD7D-DE17EC2CF1E8}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PixiEditor.Extensions.MSBuildLayoutCompiler", "PixiEditor.Extensions.MSBuildLayoutCompiler\PixiEditor.Extensions.MSBuildLayoutCompiler.csproj", "{174E2684-9C49-460C-A9F1-A6920F00C036}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -1743,6 +1745,48 @@ Global
 		{5848FCF1-E127-4CE3-8A25-F37032819F8D}.Steam|x64.Build.0 = Debug|Any CPU
 		{5848FCF1-E127-4CE3-8A25-F37032819F8D}.Steam|x86.ActiveCfg = Debug|Any CPU
 		{5848FCF1-E127-4CE3-8A25-F37032819F8D}.Steam|x86.Build.0 = Debug|Any CPU
+		{174E2684-9C49-460C-A9F1-A6920F00C036}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{174E2684-9C49-460C-A9F1-A6920F00C036}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{174E2684-9C49-460C-A9F1-A6920F00C036}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{174E2684-9C49-460C-A9F1-A6920F00C036}.Debug|x64.Build.0 = Debug|Any CPU
+		{174E2684-9C49-460C-A9F1-A6920F00C036}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{174E2684-9C49-460C-A9F1-A6920F00C036}.Debug|x86.Build.0 = Debug|Any CPU
+		{174E2684-9C49-460C-A9F1-A6920F00C036}.DevRelease|Any CPU.ActiveCfg = Debug|Any CPU
+		{174E2684-9C49-460C-A9F1-A6920F00C036}.DevRelease|Any CPU.Build.0 = Debug|Any CPU
+		{174E2684-9C49-460C-A9F1-A6920F00C036}.DevRelease|x64.ActiveCfg = Debug|Any CPU
+		{174E2684-9C49-460C-A9F1-A6920F00C036}.DevRelease|x64.Build.0 = Debug|Any CPU
+		{174E2684-9C49-460C-A9F1-A6920F00C036}.DevRelease|x86.ActiveCfg = Debug|Any CPU
+		{174E2684-9C49-460C-A9F1-A6920F00C036}.DevRelease|x86.Build.0 = Debug|Any CPU
+		{174E2684-9C49-460C-A9F1-A6920F00C036}.DevSteam|Any CPU.ActiveCfg = Debug|Any CPU
+		{174E2684-9C49-460C-A9F1-A6920F00C036}.DevSteam|Any CPU.Build.0 = Debug|Any CPU
+		{174E2684-9C49-460C-A9F1-A6920F00C036}.DevSteam|x64.ActiveCfg = Debug|Any CPU
+		{174E2684-9C49-460C-A9F1-A6920F00C036}.DevSteam|x64.Build.0 = Debug|Any CPU
+		{174E2684-9C49-460C-A9F1-A6920F00C036}.DevSteam|x86.ActiveCfg = Debug|Any CPU
+		{174E2684-9C49-460C-A9F1-A6920F00C036}.DevSteam|x86.Build.0 = Debug|Any CPU
+		{174E2684-9C49-460C-A9F1-A6920F00C036}.MSIX Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{174E2684-9C49-460C-A9F1-A6920F00C036}.MSIX Debug|Any CPU.Build.0 = Debug|Any CPU
+		{174E2684-9C49-460C-A9F1-A6920F00C036}.MSIX Debug|x64.ActiveCfg = Debug|Any CPU
+		{174E2684-9C49-460C-A9F1-A6920F00C036}.MSIX Debug|x64.Build.0 = Debug|Any CPU
+		{174E2684-9C49-460C-A9F1-A6920F00C036}.MSIX Debug|x86.ActiveCfg = Debug|Any CPU
+		{174E2684-9C49-460C-A9F1-A6920F00C036}.MSIX Debug|x86.Build.0 = Debug|Any CPU
+		{174E2684-9C49-460C-A9F1-A6920F00C036}.MSIX|Any CPU.ActiveCfg = Debug|Any CPU
+		{174E2684-9C49-460C-A9F1-A6920F00C036}.MSIX|Any CPU.Build.0 = Debug|Any CPU
+		{174E2684-9C49-460C-A9F1-A6920F00C036}.MSIX|x64.ActiveCfg = Debug|Any CPU
+		{174E2684-9C49-460C-A9F1-A6920F00C036}.MSIX|x64.Build.0 = Debug|Any CPU
+		{174E2684-9C49-460C-A9F1-A6920F00C036}.MSIX|x86.ActiveCfg = Debug|Any CPU
+		{174E2684-9C49-460C-A9F1-A6920F00C036}.MSIX|x86.Build.0 = Debug|Any CPU
+		{174E2684-9C49-460C-A9F1-A6920F00C036}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{174E2684-9C49-460C-A9F1-A6920F00C036}.Release|Any CPU.Build.0 = Release|Any CPU
+		{174E2684-9C49-460C-A9F1-A6920F00C036}.Release|x64.ActiveCfg = Release|Any CPU
+		{174E2684-9C49-460C-A9F1-A6920F00C036}.Release|x64.Build.0 = Release|Any CPU
+		{174E2684-9C49-460C-A9F1-A6920F00C036}.Release|x86.ActiveCfg = Release|Any CPU
+		{174E2684-9C49-460C-A9F1-A6920F00C036}.Release|x86.Build.0 = Release|Any CPU
+		{174E2684-9C49-460C-A9F1-A6920F00C036}.Steam|Any CPU.ActiveCfg = Debug|Any CPU
+		{174E2684-9C49-460C-A9F1-A6920F00C036}.Steam|Any CPU.Build.0 = Debug|Any CPU
+		{174E2684-9C49-460C-A9F1-A6920F00C036}.Steam|x64.ActiveCfg = Debug|Any CPU
+		{174E2684-9C49-460C-A9F1-A6920F00C036}.Steam|x64.Build.0 = Debug|Any CPU
+		{174E2684-9C49-460C-A9F1-A6920F00C036}.Steam|x86.ActiveCfg = Debug|Any CPU
+		{174E2684-9C49-460C-A9F1-A6920F00C036}.Steam|x86.Build.0 = Debug|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -1787,6 +1831,7 @@ Global
 		{9C1A500D-7A3D-49E3-BD39-05867B1D37F1} = {F1FDFA82-0C74-446A-AD7D-DE17EC2CF1E8}
 		{C16EF6F1-4E40-4CC4-9320-99C3C97929D7} = {F1FDFA82-0C74-446A-AD7D-DE17EC2CF1E8}
 		{427CE098-4B13-4E46-8C66-D924140B6CAE} = {F1FDFA82-0C74-446A-AD7D-DE17EC2CF1E8}
+		{174E2684-9C49-460C-A9F1-A6920F00C036} = {13DD041C-EE2D-4AF8-B43E-D7BFC7415E4D}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {D04B4AB0-CA33-42FD-A909-79966F9255C5}

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

@@ -14,7 +14,7 @@ public class ButtonTextElementState : State
     {
         return new Button(
             onClick: OnClick,
-            child: new Text($"Clicked: {ClickedTimes}"));
+            child: new Text($"Cassd: {ClickedTimes}"));
     }
 
     private void OnClick(ElementEventArgs args)

+ 1 - 1
src/WasmSampleExtension/ButtonTextElementState.cs

@@ -11,7 +11,7 @@ public class ButtonTextElementState : State
     {
         return new Button(
             onClick: OnClick,
-            child: new Text($"Clicked: {ClickedTimes}"));
+            child: new Text($"Hot hot hot: {ClickedTimes}"));
     }
 
     private void OnClick(ElementEventArgs args)