Selaa lähdekoodia

Visual Tree extensions api wip

Krzysztof Krysiński 2 kuukautta sitten
vanhempi
commit
0ec595f0df
75 muutettua tiedostoa jossa 1526 lisäystä ja 99 poistoa
  1. 6 0
      samples/PixiEditorExtensionSamples.sln
  2. 8 0
      samples/Sample10_VisualTree/Program.cs
  3. 45 0
      samples/Sample10_VisualTree/Sample10_VisualTree.csproj
  4. 43 0
      samples/Sample10_VisualTree/VisualTreeSampleExtension.cs
  5. 36 0
      samples/Sample10_VisualTree/extension.json
  6. 10 0
      src/PixiEditor.Extensions.CommonApi/FlyUI/LayoutElementIdGenerator.cs
  7. 10 0
      src/PixiEditor.Extensions.CommonApi/Ui/IVisualTreeProvider.cs
  8. 4 1
      src/PixiEditor.Extensions.CommonApi/Windowing/IWindowProvider.cs
  9. 17 0
      src/PixiEditor.Extensions.Runtime/ExtensionLoader.cs
  10. 3 1
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/Align.cs
  11. 3 1
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/Border.cs
  12. 3 1
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/Button.cs
  13. 3 1
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/Center.cs
  14. 3 1
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/CheckBox.cs
  15. 3 1
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/Column.cs
  16. 3 1
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/Container.cs
  17. 4 2
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/ControlDefinition.cs
  18. 3 1
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/Hyperlink.cs
  19. 3 1
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/Icon.cs
  20. 3 1
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/Image.cs
  21. 3 1
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/Layout.cs
  22. 18 0
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/NativeElement.cs
  23. 27 0
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/NativeMultiChildElement.cs
  24. 3 1
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/Padding.cs
  25. 3 1
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/Row.cs
  26. 3 1
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/SizeInputField.cs
  27. 3 1
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/StatefulElement.cs
  28. 3 1
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/Text.cs
  29. 3 1
      src/PixiEditor.Extensions.Sdk/Api/FlyUI/TextField.cs
  30. 36 0
      src/PixiEditor.Extensions.Sdk/Api/Ui/VisualTreeProvider.cs
  31. 1 0
      src/PixiEditor.Extensions.Sdk/Api/Window/PopupWindow.cs
  32. 12 15
      src/PixiEditor.Extensions.Sdk/Api/Window/WindowProvider.cs
  33. 12 0
      src/PixiEditor.Extensions.Sdk/Attributes/ControlTypeIdAttribute.cs
  34. 89 0
      src/PixiEditor.Extensions.Sdk/Bridge/Interop.Ui.cs
  35. 33 0
      src/PixiEditor.Extensions.Sdk/Bridge/Interop.Windowing.cs
  36. 15 1
      src/PixiEditor.Extensions.Sdk/Bridge/Interop.cs
  37. 15 0
      src/PixiEditor.Extensions.Sdk/Bridge/Native.Ui.cs
  38. 20 9
      src/PixiEditor.Extensions.Sdk/Bridge/Native.Windowing.cs
  39. 6 0
      src/PixiEditor.Extensions.Sdk/Bridge/Native.cs
  40. 3 0
      src/PixiEditor.Extensions.Sdk/PixiEditorApi.cs
  41. 10 0
      src/PixiEditor.Extensions.Sdk/PixiEditorExtension.cs
  42. 15 0
      src/PixiEditor.Extensions.Sdk/Utilities/InteropUtility.cs
  43. 256 0
      src/PixiEditor.Extensions.Sdk/build/PixiEditor.Api.CGlueMSBuild.deps.json
  44. BIN
      src/PixiEditor.Extensions.Sdk/build/PixiEditor.Api.CGlueMSBuild.dll
  45. 244 0
      src/PixiEditor.Extensions.Sdk/build/PixiEditor.Extensions.MSPackageBuilder.deps.json
  46. BIN
      src/PixiEditor.Extensions.Sdk/build/PixiEditor.Extensions.MSPackageBuilder.dll
  47. 43 0
      src/PixiEditor.Extensions.WasmRuntime/Api/Modules/UiModule.cs
  48. 16 0
      src/PixiEditor.Extensions.WasmRuntime/Api/Modules/WindowingModule.cs
  49. 45 0
      src/PixiEditor.Extensions.WasmRuntime/Api/VisualTreeApi.cs
  50. 7 0
      src/PixiEditor.Extensions.WasmRuntime/Api/WindowingApi.cs
  51. 9 1
      src/PixiEditor.Extensions.WasmRuntime/WasmExtensionInstance.cs
  52. 19 0
      src/PixiEditor.Extensions.WasmRuntime/WasmMemoryUtility.cs
  53. 14 0
      src/PixiEditor.Extensions/Extension.cs
  54. 2 0
      src/PixiEditor.Extensions/ExtensionServices.cs
  55. 6 0
      src/PixiEditor.Extensions/FlyUI/ElementMap.cs
  56. 1 0
      src/PixiEditor.Extensions/FlyUI/Elements/IChildHost.cs
  57. 4 4
      src/PixiEditor.Extensions/FlyUI/Elements/LayoutBuilder.cs
  58. 1 0
      src/PixiEditor.Extensions/FlyUI/Elements/LayoutElement.cs
  59. 15 2
      src/PixiEditor.Extensions/FlyUI/Elements/MultiChildLayoutElement.cs
  60. 17 0
      src/PixiEditor.Extensions/FlyUI/Elements/Native/NativeElement.cs
  61. 73 0
      src/PixiEditor.Extensions/FlyUI/Elements/Native/NativeMultiChildElement.cs
  62. 11 0
      src/PixiEditor.Extensions/FlyUI/Elements/SingleChildLayoutElement.cs
  63. 10 0
      src/PixiEditor.Extensions/FlyUI/Elements/StatefulContainer.cs
  64. 1 0
      src/PixiEditor.Extensions/Windowing/PopupWindow.cs
  65. 7 1
      src/PixiEditor.Platform.Standalone/StandaloneAdditionalContentProvider.cs
  66. 1 1
      src/PixiEditor.WasmApi.Gen/ApiGenerator.cs
  67. 2 0
      src/PixiEditor/Helpers/ServiceCollectionHelpers.cs
  68. 75 0
      src/PixiEditor/Models/ExtensionServices/VisualTreeProvider.cs
  69. 41 0
      src/PixiEditor/Models/ExtensionServices/WindowProvider.cs
  70. 0 20
      src/PixiEditor/ViewModels/SubViewModels/AdditionalContent/AdditionalContentViewModel.cs
  71. 33 2
      src/PixiEditor/ViewModels/SubViewModels/ExtensionsViewModel.cs
  72. 3 0
      src/PixiEditor/ViewModels/ViewModelMain.cs
  73. 15 2
      src/PixiEditor/Views/Dialogs/PixiEditorPopup.cs
  74. 1 6
      src/PixiEditor/Views/Main/MainTitleBar.axaml
  75. 8 15
      src/PixiEditor/Views/Windows/HelloTherePopup.axaml

+ 6 - 0
samples/PixiEditorExtensionSamples.sln

@@ -27,6 +27,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample8_CommandLibrary", "S
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample9_Document", "Sample9_Document\Sample9_Document.csproj", "{E018D2C3-2DD7-4BC7-AAAF-91DA949789E4}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample10_VisualTree", "Sample10_VisualTree\Sample10_VisualTree.csproj", "{574D7C79-8899-4A69-911A-05AEA650A275}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -80,6 +82,10 @@ Global
 		{E018D2C3-2DD7-4BC7-AAAF-91DA949789E4}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{E018D2C3-2DD7-4BC7-AAAF-91DA949789E4}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{E018D2C3-2DD7-4BC7-AAAF-91DA949789E4}.Release|Any CPU.Build.0 = Release|Any CPU
+		{574D7C79-8899-4A69-911A-05AEA650A275}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{574D7C79-8899-4A69-911A-05AEA650A275}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{574D7C79-8899-4A69-911A-05AEA650A275}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{574D7C79-8899-4A69-911A-05AEA650A275}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(NestedProjects) = preSolution
 		{FD9B4C32-4D2E-410E-BC6B-787779BEB6E2} = {7CC35BC4-829F-4EF4-8EB6-E1D46206E7DC}

+ 8 - 0
samples/Sample10_VisualTree/Program.cs

@@ -0,0 +1,8 @@
+namespace Sample9_Commands;
+
+public static class Program
+{
+    public static void Main()
+    {
+    }
+}

+ 45 - 0
samples/Sample10_VisualTree/Sample10_VisualTree.csproj

@@ -0,0 +1,45 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+    <PropertyGroup>
+        <TargetFramework>net8.0</TargetFramework>
+        <RuntimeIdentifier>wasi-wasm</RuntimeIdentifier>
+        <OutputType>Exe</OutputType>
+        <PublishTrimmed>true</PublishTrimmed>
+        <WasmSingleFileBundle>true</WasmSingleFileBundle>
+        <GenerateExtensionPackage>true</GenerateExtensionPackage>
+        <PixiExtOutputPath>..\..\src\PixiEditor.Desktop\bin\Debug\net8.0\Extensions</PixiExtOutputPath>
+        <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
+        <ValidateExecutableReferencesMatchSelfContained>false</ValidateExecutableReferencesMatchSelfContained>
+        <RootNamespace>Sample10_VisualTree</RootNamespace>
+    </PropertyGroup>
+
+    <!--Below is not required if you use Nuget package, this sample references project directly, so it must be here-->
+    <ItemGroup>
+        <ProjectReference Include="..\..\src\PixiEditor.Extensions.Sdk\PixiEditor.Extensions.Sdk.csproj"/>
+    </ItemGroup>
+
+    <ItemGroup>
+        <None Remove="extension.json"/>
+        <Content Include="extension.json">
+            <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+        </Content>
+    </ItemGroup>
+
+    <ItemGroup>
+        <Content Include="Localization\*">
+            <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+        </Content>
+    </ItemGroup>
+
+    <ItemGroup>
+        <Content Include="Resources\*">
+            <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+        </Content>
+    </ItemGroup>
+
+    <!--Below is not required if you use Nuget package, this sample references project directly, so it must be here-->
+    <Import Project="..\..\src\PixiEditor.Extensions.Sdk\build\PixiEditor.Extensions.Sdk.props"/>
+    <Import Project="..\..\src\PixiEditor.Extensions.Sdk\build\PixiEditor.Extensions.Sdk.targets"/>
+
+
+</Project>

+ 43 - 0
samples/Sample10_VisualTree/VisualTreeSampleExtension.cs

@@ -0,0 +1,43 @@
+using PixiEditor.Extensions.CommonApi.FlyUI.Properties;
+using PixiEditor.Extensions.CommonApi.Windowing;
+using PixiEditor.Extensions.Sdk;
+using PixiEditor.Extensions.Sdk.Api.FlyUI;
+using PixiEditor.Extensions.Sdk.Api.Window;
+
+namespace Sample10_VisualTree;
+
+public class VisualTreeSampleExtension : PixiEditorExtension
+{
+    /// <summary>
+    ///     This method is called when extension is loaded.
+    ///  All extensions are first loaded and then initialized. This method is called before <see cref="OnInitialized"/>.
+    /// </summary>
+    public override void OnLoaded()
+    {
+    }
+
+    /// <summary>
+    ///     This method is called when extension is initialized. After this method is called, you can use Api property to access PixiEditor API.
+    /// </summary>
+    public override void OnInitialized()
+    {
+        Api.WindowProvider.SubscribeWindowOpened(BuiltInWindowType.StartupWindow, InjectButton);
+    }
+
+    private void InjectButton(PopupWindow window)
+    {
+        var button = new Button(new Text("Click me!"));
+
+        var element = Api.VisualTreeProvider.FindElement("ExampleFilesGrid", window);
+
+        if (element is NativeMultiChildElement panel)
+        {
+            panel.AppendChild(0, button);
+        }
+    }
+
+    public override void OnMainWindowLoaded()
+    {
+       // wip
+    }
+}

+ 36 - 0
samples/Sample10_VisualTree/extension.json

@@ -0,0 +1,36 @@
+{
+  "displayName": "Sample Extension - Visual Tree",
+  "uniqueName": "yourCompany.Samples.VisualTree",
+  "description": "Sample Visual Tree extension for PixiEditor",
+  "version": "1.0.0",
+  "localization": {
+    "languages": [
+      {
+        "name": "English",
+        "code": "en",
+        "localeFileName": "Localization/en.json"
+      }
+    ]
+  },
+  "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"
+    }
+  ],
+  "license": "MIT",
+  "categories": [
+    "Extension"
+  ]
+}

+ 10 - 0
src/PixiEditor.Extensions.CommonApi/FlyUI/LayoutElementIdGenerator.cs

@@ -3,10 +3,20 @@
 public static class LayoutElementIdGenerator
 {
     private static int _lastId = -1;
+    
+    public static int CurrentId => _lastId;
 
     public static int GetNextId()
     {
         _lastId++;
         return _lastId;
     }
+
+    public static void SetId(int id)
+    {
+        if (id > _lastId)
+        {
+            _lastId = id;
+        }
+    }
 }

+ 10 - 0
src/PixiEditor.Extensions.CommonApi/Ui/IVisualTreeProvider.cs

@@ -0,0 +1,10 @@
+using PixiEditor.Extensions.CommonApi.FlyUI;
+using PixiEditor.Extensions.CommonApi.Windowing;
+
+namespace PixiEditor.Extensions.CommonApi.Ui;
+
+public interface IVisualTreeProvider
+{
+    public ILayoutElement<T>? FindElement<T>(string name);
+    public ILayoutElement<T>? FindElement<T>(string name, IPopupWindow root);
+}

+ 4 - 1
src/PixiEditor.Extensions.CommonApi/Windowing/IWindowProvider.cs

@@ -7,10 +7,13 @@ public interface IWindowProvider
     public IPopupWindow CreatePopupWindow(string title, object body);
     public IPopupWindow GetWindow(BuiltInWindowType type);
     public IPopupWindow GetWindow(string windowId);
+    public void SubscribeWindowOpened(BuiltInWindowType type, Action<IPopupWindow> action);
 }
 
 public enum BuiltInWindowType
 {
     [Description("PalettesBrowser")]
-    PalettesBrowser
+    PalettesBrowser,
+    [Description("HelloTherePopup")]
+    StartupWindow
 }

+ 17 - 0
src/PixiEditor.Extensions.Runtime/ExtensionLoader.cs

@@ -111,6 +111,23 @@ public class ExtensionLoader
         }
     }
 
+    public void InvokeMainWindowLoaded()
+    {
+        foreach (var extension in LoadedExtensions)
+        {
+            try
+            {
+                extension.MainWindowLoaded();
+            }
+            catch (Exception ex)
+            {
+#if DEBUG
+                throw;
+#endif
+            }
+        }
+    }
+
     public Extension? LoadExtension(string extension)
     {
         var extZip = ZipFile.OpenRead(extension);

+ 3 - 1
src/PixiEditor.Extensions.Sdk/Api/FlyUI/Align.cs

@@ -1,8 +1,10 @@
 using PixiEditor.Extensions.CommonApi.FlyUI;
 using PixiEditor.Extensions.CommonApi.FlyUI.Properties;
+using PixiEditor.Extensions.Sdk.Attributes;
 
 namespace PixiEditor.Extensions.Sdk.Api.FlyUI;
 
+[ControlTypeId("Align")]
 public class Align : SingleChildLayoutElement
 {
     public Alignment Alignment { get; set; }
@@ -15,7 +17,7 @@ public class Align : SingleChildLayoutElement
 
     protected override ControlDefinition CreateControl()
     {
-        ControlDefinition controlDefinition = new ControlDefinition(UniqueId, "Align");
+        ControlDefinition controlDefinition = new ControlDefinition(UniqueId, GetType());
         controlDefinition.AddProperty((int)Alignment);
         
         if (Child != null)

+ 3 - 1
src/PixiEditor.Extensions.Sdk/Api/FlyUI/Border.cs

@@ -1,8 +1,10 @@
 using PixiEditor.Extensions.CommonApi.FlyUI;
 using PixiEditor.Extensions.CommonApi.FlyUI.Properties;
+using PixiEditor.Extensions.Sdk.Attributes;
 
 namespace PixiEditor.Extensions.Sdk.Api.FlyUI;
 
+[ControlTypeId("Border")]
 public class Border : SingleChildLayoutElement
 {
     public Color Color { get; set; }
@@ -38,7 +40,7 @@ public class Border : SingleChildLayoutElement
 
     protected override ControlDefinition CreateControl()
     {
-        ControlDefinition controlDefinition = new(UniqueId, "Border");
+        ControlDefinition controlDefinition = new(UniqueId, GetType());
         if (Child != null)
         {
             controlDefinition.Children.Add(Child.BuildNative());

+ 3 - 1
src/PixiEditor.Extensions.Sdk/Api/FlyUI/Button.cs

@@ -1,8 +1,10 @@
 using PixiEditor.Extensions.CommonApi.FlyUI;
 using PixiEditor.Extensions.CommonApi.FlyUI.Events;
+using PixiEditor.Extensions.Sdk.Attributes;
 
 namespace PixiEditor.Extensions.Sdk.Api.FlyUI;
 
+[ControlTypeId("Button")]
 public class Button : SingleChildLayoutElement
 {
     public event ElementEventHandler Click
@@ -20,7 +22,7 @@ public class Button : SingleChildLayoutElement
 
     protected override ControlDefinition CreateControl()
     {
-        ControlDefinition button = new ControlDefinition(UniqueId, "Button");
+        ControlDefinition button = new ControlDefinition(UniqueId, GetType());
         if (Child != null)
             button.AddChild(Child.BuildNative());
 

+ 3 - 1
src/PixiEditor.Extensions.Sdk/Api/FlyUI/Center.cs

@@ -1,7 +1,9 @@
 using PixiEditor.Extensions.CommonApi.FlyUI;
+using PixiEditor.Extensions.Sdk.Attributes;
 
 namespace PixiEditor.Extensions.Sdk.Api.FlyUI;
 
+[ControlTypeId("Center")]
 public class Center : SingleChildLayoutElement
 {
     public Center(ILayoutElement<ControlDefinition> child, Cursor? cursor = null) : base(cursor)
@@ -11,7 +13,7 @@ public class Center : SingleChildLayoutElement
 
     protected override ControlDefinition CreateControl()
     {
-        ControlDefinition center = new ControlDefinition(UniqueId, "Center");
+        ControlDefinition center = new ControlDefinition(UniqueId, GetType());
 
         if (Child != null)
             center.AddChild(Child.BuildNative());

+ 3 - 1
src/PixiEditor.Extensions.Sdk/Api/FlyUI/CheckBox.cs

@@ -1,8 +1,10 @@
 using PixiEditor.Extensions.CommonApi.FlyUI;
 using PixiEditor.Extensions.CommonApi.FlyUI.Events;
+using PixiEditor.Extensions.Sdk.Attributes;
 
 namespace PixiEditor.Extensions.Sdk.Api.FlyUI;
 
+[ControlTypeId("CheckBox")]
 public class CheckBox : SingleChildLayoutElement
 {
     public event ElementEventHandler CheckedChanged
@@ -35,7 +37,7 @@ public class CheckBox : SingleChildLayoutElement
 
     protected override ControlDefinition CreateControl()
     {
-        ControlDefinition checkbox = new ControlDefinition(UniqueId, "CheckBox");
+        ControlDefinition checkbox = new ControlDefinition(UniqueId, GetType());
         if (Child != null)
             checkbox.AddChild(Child.BuildNative());
 

+ 3 - 1
src/PixiEditor.Extensions.Sdk/Api/FlyUI/Column.cs

@@ -1,7 +1,9 @@
 using PixiEditor.Extensions.CommonApi.FlyUI;
+using PixiEditor.Extensions.Sdk.Attributes;
 
 namespace PixiEditor.Extensions.Sdk.Api.FlyUI;
 
+[ControlTypeId("Column")]
 public class Column : MultiChildLayoutElement
 {
     public MainAxisAlignment MainAxisAlignment { get; set; }
@@ -26,7 +28,7 @@ public class Column : MultiChildLayoutElement
 
     protected override ControlDefinition CreateControl()
     {
-        ControlDefinition controlDefinition = new ControlDefinition(UniqueId, "Column");
+        ControlDefinition controlDefinition = new ControlDefinition(UniqueId, GetType());
         controlDefinition.AddProperty(MainAxisAlignment);
         controlDefinition.AddProperty(CrossAxisAlignment);
         controlDefinition.Children.AddRange(Children.Where(x => x != null).Select(x => x.BuildNative()));

+ 3 - 1
src/PixiEditor.Extensions.Sdk/Api/FlyUI/Container.cs

@@ -1,8 +1,10 @@
 using PixiEditor.Extensions.CommonApi.FlyUI;
 using PixiEditor.Extensions.CommonApi.FlyUI.Properties;
+using PixiEditor.Extensions.Sdk.Attributes;
 
 namespace PixiEditor.Extensions.Sdk.Api.FlyUI;
 
+[ControlTypeId("Container")]
 public class Container : SingleChildLayoutElement
 {
     public Edges Margin { get; set; }
@@ -22,7 +24,7 @@ public class Container : SingleChildLayoutElement
     
     protected override ControlDefinition CreateControl()
     {
-        ControlDefinition container = new ControlDefinition(UniqueId, "Container");
+        ControlDefinition container = new ControlDefinition(UniqueId, GetType());
         container.AddProperty(Margin);
 
         container.AddProperty(BackgroundColor);

+ 4 - 2
src/PixiEditor.Extensions.Sdk/Api/FlyUI/ControlDefinition.cs

@@ -3,6 +3,7 @@ using System.Runtime.InteropServices;
 using System.Text;
 using PixiEditor.Extensions.CommonApi.FlyUI;
 using PixiEditor.Extensions.CommonApi.FlyUI.Properties;
+using PixiEditor.Extensions.Sdk.Attributes;
 
 namespace PixiEditor.Extensions.Sdk.Api.FlyUI;
 
@@ -16,9 +17,10 @@ public class ControlDefinition
 
     private List<string> _buildQueuedEvents = new List<string>();
 
-    public ControlDefinition(int uniqueId, string controlTypeId)
+    public ControlDefinition(int uniqueId, Type type)
     {
-        ControlTypeId = controlTypeId;
+        ControlTypeId = type.GetCustomAttribute<ControlTypeIdAttribute>()?.TypeId
+            ?? throw new ArgumentException($"Type {type.FullName} does not have ControlTypeIdAttribute");
         UniqueId = uniqueId;
     }
 

+ 3 - 1
src/PixiEditor.Extensions.Sdk/Api/FlyUI/Hyperlink.cs

@@ -1,8 +1,10 @@
 using PixiEditor.Extensions.CommonApi.FlyUI;
 using PixiEditor.Extensions.CommonApi.FlyUI.Properties;
+using PixiEditor.Extensions.Sdk.Attributes;
 
 namespace PixiEditor.Extensions.Sdk.Api.FlyUI;
 
+[ControlTypeId("Hyperlink")]
 public class Hyperlink : Text
 {
     public string Url { get; set; }
@@ -14,7 +16,7 @@ public class Hyperlink : Text
 
     protected override ControlDefinition CreateControl()
     {
-        ControlDefinition hyperlink = new ControlDefinition(UniqueId, "Hyperlink");
+        ControlDefinition hyperlink = new ControlDefinition(UniqueId, GetType());
         hyperlink.AddProperty(Value);
         hyperlink.AddProperty(TextWrap);
         hyperlink.AddProperty(TextStyle);

+ 3 - 1
src/PixiEditor.Extensions.Sdk/Api/FlyUI/Icon.cs

@@ -1,8 +1,10 @@
 using PixiEditor.Extensions.CommonApi.FlyUI;
 using PixiEditor.Extensions.CommonApi.FlyUI.Properties;
+using PixiEditor.Extensions.Sdk.Attributes;
 
 namespace PixiEditor.Extensions.Sdk.Api.FlyUI;
 
+[ControlTypeId("Icon")]
 public class Icon : LayoutElement
 {
     public string IconName { get; set; }
@@ -19,7 +21,7 @@ public class Icon : LayoutElement
 
     protected override ControlDefinition CreateControl()
     {
-        ControlDefinition icon = new ControlDefinition(UniqueId, "Icon");
+        ControlDefinition icon = new ControlDefinition(UniqueId, GetType());
         icon.AddProperty(IconName);
         icon.AddProperty(Size);
         icon.AddProperty(Color);

+ 3 - 1
src/PixiEditor.Extensions.Sdk/Api/FlyUI/Image.cs

@@ -1,9 +1,11 @@
 using PixiEditor.Extensions.CommonApi.FlyUI;
 using PixiEditor.Extensions.CommonApi.FlyUI.Properties;
+using PixiEditor.Extensions.Sdk.Attributes;
 using PixiEditor.Extensions.Sdk.Bridge;
 
 namespace PixiEditor.Extensions.Sdk.Api.FlyUI;
 
+[ControlTypeId("Image")]
 public class Image : LayoutElement
 {
     private string source = null!;
@@ -40,7 +42,7 @@ public class Image : LayoutElement
 
     protected override ControlDefinition CreateControl()
     {
-        ControlDefinition image = new ControlDefinition(UniqueId, "Image");
+        ControlDefinition image = new ControlDefinition(UniqueId, GetType());
         
         image.AddProperty(Source);
         image.AddProperty(Width);

+ 3 - 1
src/PixiEditor.Extensions.Sdk/Api/FlyUI/Layout.cs

@@ -1,7 +1,9 @@
 using PixiEditor.Extensions.CommonApi.FlyUI;
+using PixiEditor.Extensions.Sdk.Attributes;
 
 namespace PixiEditor.Extensions.Sdk.Api.FlyUI;
 
+[ControlTypeId("Layout")]
 public sealed class Layout : SingleChildLayoutElement
 {
     public Layout(ILayoutElement<ControlDefinition> body = null)
@@ -11,7 +13,7 @@ public sealed class Layout : SingleChildLayoutElement
 
     protected override ControlDefinition CreateControl()
     {
-        ControlDefinition layout = new ControlDefinition(UniqueId, "Layout");
+        ControlDefinition layout = new ControlDefinition(UniqueId, GetType());
 
         if (Child != null)
             layout.AddChild(Child.BuildNative());

+ 18 - 0
src/PixiEditor.Extensions.Sdk/Api/FlyUI/NativeElement.cs

@@ -0,0 +1,18 @@
+using PixiEditor.Extensions.CommonApi.FlyUI;
+using PixiEditor.Extensions.Sdk.Attributes;
+
+namespace PixiEditor.Extensions.Sdk.Api.FlyUI;
+
+[ControlTypeId("NativeElement")]
+public class NativeElement : LayoutElement
+{
+    public NativeElement(Cursor? cursor) : base(cursor)
+    {
+    }
+
+    protected override ControlDefinition CreateControl()
+    {
+        ControlDefinition control = new ControlDefinition(UniqueId, GetType());
+        return control;
+    }
+}

+ 27 - 0
src/PixiEditor.Extensions.Sdk/Api/FlyUI/NativeMultiChildElement.cs

@@ -0,0 +1,27 @@
+using PixiEditor.Extensions.CommonApi.FlyUI;
+using PixiEditor.Extensions.Sdk.Attributes;
+using PixiEditor.Extensions.Sdk.Bridge;
+
+namespace PixiEditor.Extensions.Sdk.Api.FlyUI;
+
+[ControlTypeId("MultiChildNativeElement")]
+public class NativeMultiChildElement : LayoutElement
+{
+    public NativeMultiChildElement(Cursor? cursor) : base(cursor)
+    {
+    }
+
+    protected override ControlDefinition CreateControl()
+    {
+        ControlDefinition control = new ControlDefinition(UniqueId, GetType());
+        return control;
+    }
+
+    public void AppendChild(int atIndex, LayoutElement layoutElement)
+    {
+        if (layoutElement == null)
+            return;
+
+        Interop.AppendElementToNativeMultiChild(UniqueId, layoutElement, atIndex);
+    }
+}

+ 3 - 1
src/PixiEditor.Extensions.Sdk/Api/FlyUI/Padding.cs

@@ -1,8 +1,10 @@
 using PixiEditor.Extensions.CommonApi.FlyUI;
 using PixiEditor.Extensions.CommonApi.FlyUI.Properties;
+using PixiEditor.Extensions.Sdk.Attributes;
 
 namespace PixiEditor.Extensions.Sdk.Api.FlyUI;
 
+[ControlTypeId("Padding")]
 public class Padding : SingleChildLayoutElement
 {
     public Edges Edges { get; set; } = Edges.All(0);
@@ -15,7 +17,7 @@ public class Padding : SingleChildLayoutElement
 
     protected override ControlDefinition CreateControl()
     {
-        ControlDefinition controlDefinition = new ControlDefinition(UniqueId, "Padding");
+        ControlDefinition controlDefinition = new ControlDefinition(UniqueId, GetType());
         controlDefinition.Children.Add(Child.BuildNative());
         
         controlDefinition.AddProperty(Edges);

+ 3 - 1
src/PixiEditor.Extensions.Sdk/Api/FlyUI/Row.cs

@@ -1,7 +1,9 @@
 using PixiEditor.Extensions.CommonApi.FlyUI;
+using PixiEditor.Extensions.Sdk.Attributes;
 
 namespace PixiEditor.Extensions.Sdk.Api.FlyUI;
 
+[ControlTypeId("Row")]
 public class Row : MultiChildLayoutElement
 {
     public MainAxisAlignment MainAxisAlignment { get; set; }
@@ -26,7 +28,7 @@ public class Row : MultiChildLayoutElement
 
     protected override ControlDefinition CreateControl()
     {
-        ControlDefinition controlDefinition = new ControlDefinition(UniqueId, "Row");
+        ControlDefinition controlDefinition = new ControlDefinition(UniqueId, GetType());
         controlDefinition.AddProperty(MainAxisAlignment);
         controlDefinition.AddProperty(CrossAxisAlignment);
         controlDefinition.Children.AddRange(Children.Select(x => x.BuildNative()));

+ 3 - 1
src/PixiEditor.Extensions.Sdk/Api/FlyUI/SizeInputField.cs

@@ -1,8 +1,10 @@
 using PixiEditor.Extensions.CommonApi.FlyUI;
 using PixiEditor.Extensions.CommonApi.FlyUI.Events;
+using PixiEditor.Extensions.Sdk.Attributes;
 
 namespace PixiEditor.Extensions.Sdk.Api.FlyUI;
 
+[ControlTypeId("SizeInputField")]
 public class SizeInputField : LayoutElement
 {
     public event ElementEventHandler<NumberEventArgs> SizeChanged
@@ -28,7 +30,7 @@ public class SizeInputField : LayoutElement
 
     protected override ControlDefinition CreateControl()
     {
-        ControlDefinition field = new ControlDefinition(UniqueId, "SizeInputField");
+        ControlDefinition field = new ControlDefinition(UniqueId, GetType());
         field.AddProperty(Value);
         field.AddProperty(Min);
         field.AddProperty(Max);

+ 3 - 1
src/PixiEditor.Extensions.Sdk/Api/FlyUI/StatefulElement.cs

@@ -1,8 +1,10 @@
 using PixiEditor.Extensions.CommonApi.FlyUI;
 using PixiEditor.Extensions.CommonApi.FlyUI.State;
+using PixiEditor.Extensions.Sdk.Attributes;
 
 namespace PixiEditor.Extensions.Sdk.Api.FlyUI;
 
+[ControlTypeId("StatefulElement")]
 public abstract class StatefulElement<TState> : LayoutElement, IStatefulElement<ControlDefinition, TState> where TState : IState<ControlDefinition>
 {
     private TState state;
@@ -34,7 +36,7 @@ public abstract class StatefulElement<TState> : LayoutElement, IStatefulElement<
     protected override ControlDefinition CreateControl()
     {
         ControlDefinition controlDefinition = State.Build().BuildNative();
-        ControlDefinition statefulContainer = new ControlDefinition(UniqueId, "StatefulContainer");
+        ControlDefinition statefulContainer = new ControlDefinition(UniqueId, GetType());
         statefulContainer.Children.Add(controlDefinition);
 
         return statefulContainer;

+ 3 - 1
src/PixiEditor.Extensions.Sdk/Api/FlyUI/Text.cs

@@ -1,8 +1,10 @@
 using PixiEditor.Extensions.CommonApi.FlyUI;
 using PixiEditor.Extensions.CommonApi.FlyUI.Properties;
+using PixiEditor.Extensions.Sdk.Attributes;
 
 namespace PixiEditor.Extensions.Sdk.Api.FlyUI;
 
+[ControlTypeId("Text")]
 public class Text : LayoutElement
 {
     public string Value { get; set; }
@@ -20,7 +22,7 @@ public class Text : LayoutElement
 
     protected override ControlDefinition CreateControl()
     {
-        ControlDefinition text = new ControlDefinition(UniqueId, "Text");
+        ControlDefinition text = new ControlDefinition(UniqueId, GetType());
         text.AddProperty(Value);
         text.AddProperty(TextWrap);
         text.AddProperty(TextStyle);

+ 3 - 1
src/PixiEditor.Extensions.Sdk/Api/FlyUI/TextField.cs

@@ -1,8 +1,10 @@
 using PixiEditor.Extensions.CommonApi.FlyUI;
 using PixiEditor.Extensions.CommonApi.FlyUI.Events;
+using PixiEditor.Extensions.Sdk.Attributes;
 
 namespace PixiEditor.Extensions.Sdk.Api.FlyUI;
 
+[ControlTypeId("TextField")]
 public class TextField : LayoutElement
 {
     public event ElementEventHandler<TextEventArgs> TextChanged
@@ -20,7 +22,7 @@ public class TextField : LayoutElement
 
     protected override ControlDefinition CreateControl()
     {
-        ControlDefinition textField = new ControlDefinition(UniqueId, "TextField");
+        ControlDefinition textField = new ControlDefinition(UniqueId, GetType());
         textField.AddProperty(Text);
         return textField;
     }

+ 36 - 0
src/PixiEditor.Extensions.Sdk/Api/Ui/VisualTreeProvider.cs

@@ -0,0 +1,36 @@
+using PixiEditor.Extensions.CommonApi.FlyUI;
+using PixiEditor.Extensions.CommonApi.Ui;
+using PixiEditor.Extensions.CommonApi.Windowing;
+using PixiEditor.Extensions.Sdk.Api.FlyUI;
+using PixiEditor.Extensions.Sdk.Api.Window;
+using PixiEditor.Extensions.Sdk.Bridge;
+
+namespace PixiEditor.Extensions.Sdk.Api.Ui;
+
+public class VisualTreeProvider : IVisualTreeProvider
+{
+    ILayoutElement<T>? IVisualTreeProvider.FindElement<T>(string name)
+    {
+        return FindElement(name) as ILayoutElement<T>;
+    }
+
+    ILayoutElement<T> IVisualTreeProvider.FindElement<T>(string name, IPopupWindow root)
+    {
+        return FindElement(name, root as PopupWindow) as ILayoutElement<T>;
+    }
+
+    public LayoutElement? FindElement(string name)
+    {
+        return Interop.FindUiElement(name);
+    }
+
+    public LayoutElement? FindElement(string name, PopupWindow root)
+    {
+        if (root == null)
+        {
+            throw new ArgumentNullException(nameof(root), "Root window cannot be null.");
+        }
+
+        return Interop.FindUiElement(name, root);
+    }
+}

+ 1 - 0
src/PixiEditor.Extensions.Sdk/Api/Window/PopupWindow.cs

@@ -6,6 +6,7 @@ namespace PixiEditor.Extensions.Sdk.Api.Window;
 
 public class PopupWindow : IPopupWindow
 {
+    internal int Handle => windowHandle;
     private int windowHandle;
 
     internal PopupWindow(int handle)

+ 12 - 15
src/PixiEditor.Extensions.Sdk/Api/Window/WindowProvider.cs

@@ -16,7 +16,7 @@ public class WindowProvider : IWindowProvider
         int handle = Native.create_popup_window(title, ptr, bytes.Length);
         Marshal.FreeHGlobal(ptr);
         
-        SubscribeToEvents(controlDefinition);
+        Interop.SubscribeToEvents(controlDefinition);
         return new PopupWindow(handle);
     }
 
@@ -28,20 +28,7 @@ public class WindowProvider : IWindowProvider
         Native.state_changed(uniqueId, ptr, bytes.Length);
         Marshal.FreeHGlobal(ptr);
 
-        SubscribeToEvents(newLayout);
-    }
-
-    private void SubscribeToEvents(ControlDefinition body)
-    {
-        foreach (ControlDefinition child in body.Children)
-        {
-            SubscribeToEvents(child);
-        }
-
-        foreach (var queuedEvent in body.QueuedEvents)
-        {
-            Native.subscribe_to_event(body.UniqueId, queuedEvent);
-        }
+        Interop.SubscribeToEvents(newLayout);
     }
 
     public IPopupWindow CreatePopupWindow(string title, object body)
@@ -63,4 +50,14 @@ public class WindowProvider : IWindowProvider
         int handle = Native.get_window(windowId);
         return new PopupWindow(handle);
     }
+
+    void IWindowProvider.SubscribeWindowOpened(BuiltInWindowType type, Action<IPopupWindow> action)
+    {
+        SubscribeWindowOpened(type, action);
+    }
+
+    public void SubscribeWindowOpened(BuiltInWindowType type, Action<PopupWindow> action)
+    {
+        Interop.RegisterWindowOpenedCallback((int)type, action);
+    }
 }

+ 12 - 0
src/PixiEditor.Extensions.Sdk/Attributes/ControlTypeIdAttribute.cs

@@ -0,0 +1,12 @@
+namespace PixiEditor.Extensions.Sdk.Attributes;
+
+[System.AttributeUsage(System.AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
+public class ControlTypeIdAttribute : System.Attribute
+{
+    public string TypeId { get; }
+
+    public ControlTypeIdAttribute(string typeId)
+    {
+        TypeId = typeId;
+    }
+}

+ 89 - 0
src/PixiEditor.Extensions.Sdk/Bridge/Interop.Ui.cs

@@ -0,0 +1,89 @@
+using PixiEditor.Extensions.CommonApi.FlyUI;
+using PixiEditor.Extensions.CommonApi.Windowing;
+using PixiEditor.Extensions.Sdk.Api.FlyUI;
+using PixiEditor.Extensions.Sdk.Api.Window;
+using PixiEditor.Extensions.Sdk.Utilities;
+
+namespace PixiEditor.Extensions.Sdk.Bridge;
+
+internal partial class Interop
+{
+    private static Dictionary<string, Type> typeMap;
+
+    public static void SubscribeToEvents(ControlDefinition body)
+    {
+        foreach (ControlDefinition child in body.Children)
+        {
+            SubscribeToEvents(child);
+        }
+
+        foreach (var queuedEvent in body.QueuedEvents)
+        {
+            Native.subscribe_to_event(body.UniqueId, queuedEvent);
+        }
+    }
+
+    /// <summary>
+    /// Finds an element in the visual tree by its name.
+    /// </summary>
+    /// <param name="name">The name of the element to find.</param>
+    /// <returns>A byte array representing the found element, or null if not found.</returns>
+    public static LayoutElement? FindUiElement(string name)
+    {
+        int id = LayoutElementIdGenerator.CurrentId + 1;
+        var element = InteropUtility.PrefixedIntPtrToByteArray(Native.find_ui_element(name, id));
+        if (element.Length == 0)
+        {
+            return null;
+        }
+
+        int typeLength = BitConverter.ToInt32(element, 0);
+        string typeId = System.Text.Encoding.UTF8.GetString(element, 4, typeLength);
+
+        if (!typeMap.TryGetValue(typeId, out Type? type))
+        {
+            type = typeof(NativeElement);
+        }
+
+        LayoutElement lElem = (LayoutElement)Activator.CreateInstance(type, (Cursor?)null)!;
+
+        return lElem;
+    }
+
+    public static LayoutElement FindUiElement(string name, PopupWindow root)
+    {
+        int id = LayoutElementIdGenerator.CurrentId + 1;
+        var element = InteropUtility.PrefixedIntPtrToByteArray(Native.find_ui_element_in_popup(name, root.Handle, id));
+        if (element.Length == 0)
+        {
+            return null;
+        }
+
+        int typeLength = BitConverter.ToInt32(element, 0);
+        string typeId = System.Text.Encoding.UTF8.GetString(element, 4, typeLength);
+
+        if (!typeMap.TryGetValue(typeId, out Type? type))
+        {
+            type = typeof(NativeElement);
+        }
+
+        LayoutElement lElem = (LayoutElement)Activator.CreateInstance(type, (Cursor?)null)!;
+
+        return lElem;
+    }
+
+    public static void AppendElementToNativeMultiChild(int uniqueId, LayoutElement element, int atIndex)
+    {
+        if (element == null)
+            return;
+
+        var built = element.BuildNative();
+        var bytes = built.SerializeBytes();
+        int bodyLen = bytes.Length;
+
+        Native.append_element_to_native_multi_child(atIndex, uniqueId, InteropUtility.ByteArrayToIntPtr(bytes),
+            bodyLen);
+
+        SubscribeToEvents(built);
+    }
+}

+ 33 - 0
src/PixiEditor.Extensions.Sdk/Bridge/Interop.Windowing.cs

@@ -0,0 +1,33 @@
+using PixiEditor.Extensions.CommonApi.Windowing;
+using PixiEditor.Extensions.Sdk.Api.Window;
+
+namespace PixiEditor.Extensions.Sdk.Bridge;
+
+internal static partial class Interop
+{
+    private static Dictionary<int, List<Action<PopupWindow>>> callbacks =
+        new Dictionary<int, List<Action<PopupWindow>>>();
+
+    public static void RegisterWindowOpenedCallback(int type, Action<PopupWindow> callback)
+    {
+        if (!callbacks.ContainsKey(type))
+        {
+            callbacks[type] = new List<Action<PopupWindow>>();
+        }
+
+        callbacks[type].Add(callback);
+        Native.subscribe_built_in_window_opened(type);
+    }
+
+    public static void OnBuiltInWindowOpened(int type, int handle)
+    {
+        if (callbacks.TryGetValue(type, out var callback))
+        {
+            PopupWindow window = new PopupWindow(handle);
+            foreach (var action in callback)
+            {
+                action?.Invoke(window);
+            }
+        }
+    }
+}

+ 15 - 1
src/PixiEditor.Extensions.Sdk/Bridge/Interop.cs

@@ -1,4 +1,6 @@
-using PixiEditor.Extensions.CommonApi.Palettes;
+using System.Reflection;
+using PixiEditor.Extensions.CommonApi.Palettes;
+using PixiEditor.Extensions.Sdk.Attributes;
 
 namespace PixiEditor.Extensions.Sdk.Bridge;
 
@@ -6,9 +8,21 @@ internal static partial class Interop
 {
     static Interop()
     {
+        typeMap = new Dictionary<string, Type>();
+
+        foreach (Type type in Assembly.GetExecutingAssembly().GetTypes())
+        {
+            ControlTypeIdAttribute? attr = type.GetCustomAttribute<ControlTypeIdAttribute>();
+            if (attr != null)
+            {
+                typeMap[attr.TypeId] = type;
+            }
+        }
+
         uniqueName = Native.get_extension_unique_name();
         Native.PreferenceUpdated += NativeOnPreferenceUpdated;
         Native.CommandInvoked += OnCommandInvoked;
+        Native.WindowOpened += OnBuiltInWindowOpened;
     }
 
     public static void UpdateUserPreference<T>(string name, T value)

+ 15 - 0
src/PixiEditor.Extensions.Sdk/Bridge/Native.Ui.cs

@@ -0,0 +1,15 @@
+using System.Runtime.CompilerServices;
+
+namespace PixiEditor.Extensions.Sdk.Bridge;
+
+internal partial class Native
+{
+    [MethodImpl(MethodImplOptions.InternalCall)]
+    public static extern IntPtr find_ui_element(string name, int elementHandle);
+
+    [MethodImpl(MethodImplOptions.InternalCall)]
+    public static extern IntPtr find_ui_element_in_popup(string name, int popupHandle, int elementHandle);
+
+    [MethodImpl(MethodImplOptions.InternalCall)]
+    public static extern void append_element_to_native_multi_child(int atIndex, int uniqueId, IntPtr body, int bodyLen);
+}

+ 20 - 9
src/PixiEditor.Extensions.Sdk/Bridge/Native.Windowing.cs

@@ -4,6 +4,7 @@ namespace PixiEditor.Extensions.Sdk.Bridge;
 
 internal static partial class Native
 {
+    public static event Action<int, int> WindowOpened;
     [MethodImpl(MethodImplOptions.InternalCall)]
     internal static extern int create_popup_window(string title, IntPtr data, int length);
 
@@ -12,31 +13,31 @@ internal static partial class Native
 
     [MethodImpl(MethodImplOptions.InternalCall)]
     internal static extern string get_window_title(int windowHandle);
-    
+
     [MethodImpl(MethodImplOptions.InternalCall)]
     internal static extern double get_window_width(int windowHandle);
-    
+
     [MethodImpl(MethodImplOptions.InternalCall)]
     internal static extern void set_window_width(int windowHandle, double width);
-    
+
     [MethodImpl(MethodImplOptions.InternalCall)]
     internal static extern double get_window_height(int windowHandle);
-    
+
     [MethodImpl(MethodImplOptions.InternalCall)]
     internal static extern void set_window_height(int windowHandle, double height);
-    
+
     [MethodImpl(MethodImplOptions.InternalCall)]
     internal static extern bool get_window_resizable(int windowHandle);
-    
+
     [MethodImpl(MethodImplOptions.InternalCall)]
     internal static extern void set_window_resizable(int windowHandle, bool resizable);
-    
+
     [MethodImpl(MethodImplOptions.InternalCall)]
     internal static extern bool get_window_minimizable(int windowHandle);
-    
+
     [MethodImpl(MethodImplOptions.InternalCall)]
     internal static extern void set_window_minimizable(int windowHandle, bool minimizable);
-    
+
 
     [MethodImpl(MethodImplOptions.InternalCall)]
     internal static extern void show_window(int windowHandle);
@@ -50,6 +51,16 @@ internal static partial class Native
     [MethodImpl(MethodImplOptions.InternalCall)]
     public static extern int get_built_in_window(int type);
 
+    [MethodImpl(MethodImplOptions.InternalCall)]
+    public static extern void subscribe_built_in_window_opened(int type);
+
     [MethodImpl(MethodImplOptions.InternalCall)]
     public static extern int get_window(string windowId);
+
+    [ApiExport("on_built_in_window_opened")]
+    public static void OnBuiltInWindowOpened(int type, int handle)
+    {
+        WindowOpened?.Invoke(type, handle);
+    }
+
 }

+ 6 - 0
src/PixiEditor.Extensions.Sdk/Bridge/Native.cs

@@ -57,6 +57,12 @@ internal static partial class Native
         ExtensionContext.Active.OnUserReady();
     }
 
+    [ApiExport("main_window_loaded")]
+    internal static void OnMainWindowLoaded()
+    {
+        ExtensionContext.Active.OnMainWindowLoaded();
+    }
+
     [ApiExport("raise_element_event")]
     internal static void EventRaised(int internalControlId, string eventName, IntPtr eventData, int dataLength)
     {

+ 3 - 0
src/PixiEditor.Extensions.Sdk/PixiEditorApi.cs

@@ -4,6 +4,7 @@ using PixiEditor.Extensions.Sdk.Api.Commands;
 using PixiEditor.Extensions.Sdk.Api.IO;
 using PixiEditor.Extensions.Sdk.Api.Logging;
 using PixiEditor.Extensions.Sdk.Api.Palettes;
+using PixiEditor.Extensions.Sdk.Api.Ui;
 using PixiEditor.Extensions.Sdk.Api.UserPreferences;
 using PixiEditor.Extensions.Sdk.Api.Window;
 
@@ -17,6 +18,7 @@ public class PixiEditorApi
     public PalettesProvider Palettes { get; }
     public CommandProvider Commands { get; }
     public DocumentProvider Documents { get; }
+    public VisualTreeProvider VisualTreeProvider { get; }
 
     public PixiEditorApi()
     {
@@ -26,5 +28,6 @@ public class PixiEditorApi
         Palettes = new PalettesProvider();
         Commands = new CommandProvider();
         Documents = new DocumentProvider();
+        VisualTreeProvider = new VisualTreeProvider();
     }
 }

+ 10 - 0
src/PixiEditor.Extensions.Sdk/PixiEditorExtension.cs

@@ -19,4 +19,14 @@ public abstract class PixiEditorExtension
     /// If user didn't complete onboarding, it will be called after the user completes it.
     /// </summary>
     public virtual void OnUserReady() { }
+
+
+    /// <summary>
+    /// This method is called when the main window is loaded.
+    /// You can use this method to perform actions that require the main window to be fully loaded.
+    /// </summary>
+    public virtual void OnMainWindowLoaded()
+    {
+
+    }
 }

+ 15 - 0
src/PixiEditor.Extensions.Sdk/Utilities/InteropUtility.cs

@@ -17,4 +17,19 @@ public static class InteropUtility
         Marshal.Copy(ptr, array, 0, length);
         return array;
     }
+
+    public static byte[] PrefixedIntPtrToByteArray(IntPtr ptr)
+    {
+        // Read the first 4 bytes as an integer (length of the data)
+        int length = Marshal.ReadInt32(ptr);
+        if (length <= 0)
+        {
+            return [];
+        }
+
+        // Read the next bytes based on the length
+        byte[] array = new byte[length];
+        Marshal.Copy(ptr + sizeof(int), array, 0, length);
+        return array;
+    }
 }

+ 256 - 0
src/PixiEditor.Extensions.Sdk/build/PixiEditor.Api.CGlueMSBuild.deps.json

@@ -0,0 +1,256 @@
+{
+  "runtimeTarget": {
+    "name": ".NETStandard,Version=v2.0/",
+    "signature": ""
+  },
+  "compilationOptions": {},
+  "targets": {
+    ".NETStandard,Version=v2.0": {},
+    ".NETStandard,Version=v2.0/": {
+      "PixiEditor.Api.CGlueMSBuild/1.0.0": {
+        "dependencies": {
+          "Microsoft.Build.Utilities.Core": "17.12.6",
+          "Mono.Cecil": "0.11.6",
+          "NETStandard.Library": "2.0.3",
+          "StyleCop.Analyzers": "1.1.118"
+        },
+        "runtime": {
+          "PixiEditor.Api.CGlueMSBuild.dll": {}
+        }
+      },
+      "Microsoft.Build.Framework/17.12.6": {
+        "dependencies": {
+          "Microsoft.Win32.Registry": "5.0.0",
+          "System.Memory": "4.5.5",
+          "System.Runtime.CompilerServices.Unsafe": "6.0.0",
+          "System.Security.Principal.Windows": "5.0.0"
+        }
+      },
+      "Microsoft.Build.Utilities.Core/17.12.6": {
+        "dependencies": {
+          "Microsoft.Build.Framework": "17.12.6",
+          "Microsoft.NET.StringTools": "17.12.6",
+          "Microsoft.Win32.Registry": "5.0.0",
+          "System.Collections.Immutable": "8.0.0",
+          "System.Configuration.ConfigurationManager": "8.0.0",
+          "System.Memory": "4.5.5",
+          "System.Runtime.CompilerServices.Unsafe": "6.0.0",
+          "System.Security.Principal.Windows": "5.0.0",
+          "System.Text.Encoding.CodePages": "7.0.0"
+        }
+      },
+      "Microsoft.NET.StringTools/17.12.6": {
+        "dependencies": {
+          "System.Memory": "4.5.5",
+          "System.Runtime.CompilerServices.Unsafe": "6.0.0"
+        }
+      },
+      "Microsoft.NETCore.Platforms/1.1.0": {},
+      "Microsoft.Win32.Registry/5.0.0": {
+        "dependencies": {
+          "System.Buffers": "4.5.1",
+          "System.Memory": "4.5.5",
+          "System.Security.AccessControl": "5.0.0",
+          "System.Security.Principal.Windows": "5.0.0"
+        }
+      },
+      "Mono.Cecil/0.11.6": {
+        "runtime": {
+          "lib/netstandard2.0/Mono.Cecil.Mdb.dll": {
+            "assemblyVersion": "0.11.6.0",
+            "fileVersion": "0.11.6.0"
+          },
+          "lib/netstandard2.0/Mono.Cecil.Pdb.dll": {
+            "assemblyVersion": "0.11.6.0",
+            "fileVersion": "0.11.6.0"
+          },
+          "lib/netstandard2.0/Mono.Cecil.Rocks.dll": {
+            "assemblyVersion": "0.11.6.0",
+            "fileVersion": "0.11.6.0"
+          },
+          "lib/netstandard2.0/Mono.Cecil.dll": {
+            "assemblyVersion": "0.11.6.0",
+            "fileVersion": "0.11.6.0"
+          }
+        }
+      },
+      "NETStandard.Library/2.0.3": {
+        "dependencies": {
+          "Microsoft.NETCore.Platforms": "1.1.0"
+        }
+      },
+      "StyleCop.Analyzers/1.1.118": {},
+      "System.Buffers/4.5.1": {},
+      "System.Collections.Immutable/8.0.0": {
+        "dependencies": {
+          "System.Memory": "4.5.5",
+          "System.Runtime.CompilerServices.Unsafe": "6.0.0"
+        }
+      },
+      "System.Configuration.ConfigurationManager/8.0.0": {
+        "dependencies": {
+          "System.Security.Cryptography.ProtectedData": "8.0.0"
+        }
+      },
+      "System.Memory/4.5.5": {
+        "dependencies": {
+          "System.Buffers": "4.5.1",
+          "System.Numerics.Vectors": "4.4.0",
+          "System.Runtime.CompilerServices.Unsafe": "6.0.0"
+        }
+      },
+      "System.Numerics.Vectors/4.4.0": {},
+      "System.Runtime.CompilerServices.Unsafe/6.0.0": {},
+      "System.Security.AccessControl/5.0.0": {
+        "dependencies": {
+          "System.Security.Principal.Windows": "5.0.0"
+        }
+      },
+      "System.Security.Cryptography.ProtectedData/8.0.0": {
+        "dependencies": {
+          "System.Memory": "4.5.5"
+        }
+      },
+      "System.Security.Principal.Windows/5.0.0": {},
+      "System.Text.Encoding.CodePages/7.0.0": {
+        "dependencies": {
+          "System.Memory": "4.5.5",
+          "System.Runtime.CompilerServices.Unsafe": "6.0.0"
+        }
+      }
+    }
+  },
+  "libraries": {
+    "PixiEditor.Api.CGlueMSBuild/1.0.0": {
+      "type": "project",
+      "serviceable": false,
+      "sha512": ""
+    },
+    "Microsoft.Build.Framework/17.12.6": {
+      "type": "package",
+      "serviceable": true,
+      "sha512": "sha512-jleteC0seumLGTmTVwob97lcwPj/dfgzL/V3g/VVcMZgo2Ic7jzdy8AYpByPDh8e3uRq0SjCl6HOFCjhy5GzRQ==",
+      "path": "microsoft.build.framework/17.12.6",
+      "hashPath": "microsoft.build.framework.17.12.6.nupkg.sha512"
+    },
+    "Microsoft.Build.Utilities.Core/17.12.6": {
+      "type": "package",
+      "serviceable": true,
+      "sha512": "sha512-pU3GnHcXp8VRMGKxdJCq+tixfhFn+QwEbpqmZmc/nqFHFyuhlGwjonWZMIWcwuCv/8EHgxoOttFvna1vrN+RrA==",
+      "path": "microsoft.build.utilities.core/17.12.6",
+      "hashPath": "microsoft.build.utilities.core.17.12.6.nupkg.sha512"
+    },
+    "Microsoft.NET.StringTools/17.12.6": {
+      "type": "package",
+      "serviceable": true,
+      "sha512": "sha512-w8Ehofqte5bJoR+Fa3f6JwkwFEkGtXxqvQHGOVOSHDzgNVySvL5FSNhavbQSZ864el9c3rjdLPLAtBW8dq6fmg==",
+      "path": "microsoft.net.stringtools/17.12.6",
+      "hashPath": "microsoft.net.stringtools.17.12.6.nupkg.sha512"
+    },
+    "Microsoft.NETCore.Platforms/1.1.0": {
+      "type": "package",
+      "serviceable": true,
+      "sha512": "sha512-kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==",
+      "path": "microsoft.netcore.platforms/1.1.0",
+      "hashPath": "microsoft.netcore.platforms.1.1.0.nupkg.sha512"
+    },
+    "Microsoft.Win32.Registry/5.0.0": {
+      "type": "package",
+      "serviceable": true,
+      "sha512": "sha512-dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==",
+      "path": "microsoft.win32.registry/5.0.0",
+      "hashPath": "microsoft.win32.registry.5.0.0.nupkg.sha512"
+    },
+    "Mono.Cecil/0.11.6": {
+      "type": "package",
+      "serviceable": true,
+      "sha512": "sha512-f33RkDtZO8VlGXCtmQIviOtxgnUdym9xx/b1p9h91CRGOsJFxCFOFK1FDbVt1OCf1aWwYejUFa2MOQyFWTFjbA==",
+      "path": "mono.cecil/0.11.6",
+      "hashPath": "mono.cecil.0.11.6.nupkg.sha512"
+    },
+    "NETStandard.Library/2.0.3": {
+      "type": "package",
+      "serviceable": true,
+      "sha512": "sha512-st47PosZSHrjECdjeIzZQbzivYBJFv6P2nv4cj2ypdI204DO+vZ7l5raGMiX4eXMJ53RfOIg+/s4DHVZ54Nu2A==",
+      "path": "netstandard.library/2.0.3",
+      "hashPath": "netstandard.library.2.0.3.nupkg.sha512"
+    },
+    "StyleCop.Analyzers/1.1.118": {
+      "type": "package",
+      "serviceable": true,
+      "sha512": "sha512-Onx6ovGSqXSK07n/0eM3ZusiNdB6cIlJdabQhWGgJp3Vooy9AaLS/tigeybOJAobqbtggTamoWndz72JscZBvw==",
+      "path": "stylecop.analyzers/1.1.118",
+      "hashPath": "stylecop.analyzers.1.1.118.nupkg.sha512"
+    },
+    "System.Buffers/4.5.1": {
+      "type": "package",
+      "serviceable": true,
+      "sha512": "sha512-Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==",
+      "path": "system.buffers/4.5.1",
+      "hashPath": "system.buffers.4.5.1.nupkg.sha512"
+    },
+    "System.Collections.Immutable/8.0.0": {
+      "type": "package",
+      "serviceable": true,
+      "sha512": "sha512-AurL6Y5BA1WotzlEvVaIDpqzpIPvYnnldxru8oXJU2yFxFUy3+pNXjXd1ymO+RA0rq0+590Q8gaz2l3Sr7fmqg==",
+      "path": "system.collections.immutable/8.0.0",
+      "hashPath": "system.collections.immutable.8.0.0.nupkg.sha512"
+    },
+    "System.Configuration.ConfigurationManager/8.0.0": {
+      "type": "package",
+      "serviceable": true,
+      "sha512": "sha512-JlYi9XVvIREURRUlGMr1F6vOFLk7YSY4p1vHo4kX3tQ0AGrjqlRWHDi66ImHhy6qwXBG3BJ6Y1QlYQ+Qz6Xgww==",
+      "path": "system.configuration.configurationmanager/8.0.0",
+      "hashPath": "system.configuration.configurationmanager.8.0.0.nupkg.sha512"
+    },
+    "System.Memory/4.5.5": {
+      "type": "package",
+      "serviceable": true,
+      "sha512": "sha512-XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==",
+      "path": "system.memory/4.5.5",
+      "hashPath": "system.memory.4.5.5.nupkg.sha512"
+    },
+    "System.Numerics.Vectors/4.4.0": {
+      "type": "package",
+      "serviceable": true,
+      "sha512": "sha512-UiLzLW+Lw6HLed1Hcg+8jSRttrbuXv7DANVj0DkL9g6EnnzbL75EB7EWsw5uRbhxd/4YdG8li5XizGWepmG3PQ==",
+      "path": "system.numerics.vectors/4.4.0",
+      "hashPath": "system.numerics.vectors.4.4.0.nupkg.sha512"
+    },
+    "System.Runtime.CompilerServices.Unsafe/6.0.0": {
+      "type": "package",
+      "serviceable": true,
+      "sha512": "sha512-/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==",
+      "path": "system.runtime.compilerservices.unsafe/6.0.0",
+      "hashPath": "system.runtime.compilerservices.unsafe.6.0.0.nupkg.sha512"
+    },
+    "System.Security.AccessControl/5.0.0": {
+      "type": "package",
+      "serviceable": true,
+      "sha512": "sha512-dagJ1mHZO3Ani8GH0PHpPEe/oYO+rVdbQjvjJkBRNQkX4t0r1iaeGn8+/ybkSLEan3/slM0t59SVdHzuHf2jmw==",
+      "path": "system.security.accesscontrol/5.0.0",
+      "hashPath": "system.security.accesscontrol.5.0.0.nupkg.sha512"
+    },
+    "System.Security.Cryptography.ProtectedData/8.0.0": {
+      "type": "package",
+      "serviceable": true,
+      "sha512": "sha512-+TUFINV2q2ifyXauQXRwy4CiBhqvDEDZeVJU7qfxya4aRYOKzVBpN+4acx25VcPB9ywUN6C0n8drWl110PhZEg==",
+      "path": "system.security.cryptography.protecteddata/8.0.0",
+      "hashPath": "system.security.cryptography.protecteddata.8.0.0.nupkg.sha512"
+    },
+    "System.Security.Principal.Windows/5.0.0": {
+      "type": "package",
+      "serviceable": true,
+      "sha512": "sha512-t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==",
+      "path": "system.security.principal.windows/5.0.0",
+      "hashPath": "system.security.principal.windows.5.0.0.nupkg.sha512"
+    },
+    "System.Text.Encoding.CodePages/7.0.0": {
+      "type": "package",
+      "serviceable": true,
+      "sha512": "sha512-LSyCblMpvOe0N3E+8e0skHcrIhgV2huaNcjUUEa8hRtgEAm36aGkRoC8Jxlb6Ra6GSfF29ftduPNywin8XolzQ==",
+      "path": "system.text.encoding.codepages/7.0.0",
+      "hashPath": "system.text.encoding.codepages.7.0.0.nupkg.sha512"
+    }
+  }
+}

BIN
src/PixiEditor.Extensions.Sdk/build/PixiEditor.Api.CGlueMSBuild.dll


+ 244 - 0
src/PixiEditor.Extensions.Sdk/build/PixiEditor.Extensions.MSPackageBuilder.deps.json

@@ -0,0 +1,244 @@
+{
+  "runtimeTarget": {
+    "name": ".NETStandard,Version=v2.0/",
+    "signature": ""
+  },
+  "compilationOptions": {},
+  "targets": {
+    ".NETStandard,Version=v2.0": {},
+    ".NETStandard,Version=v2.0/": {
+      "PixiEditor.Extensions.MSPackageBuilder/1.0.0": {
+        "dependencies": {
+          "Microsoft.Build.Utilities.Core": "17.12.6",
+          "NETStandard.Library": "2.0.3",
+          "Newtonsoft.Json": "13.0.3",
+          "StyleCop.Analyzers": "1.1.118"
+        },
+        "runtime": {
+          "PixiEditor.Extensions.MSPackageBuilder.dll": {}
+        }
+      },
+      "Microsoft.Build.Framework/17.12.6": {
+        "dependencies": {
+          "Microsoft.Win32.Registry": "5.0.0",
+          "System.Memory": "4.5.5",
+          "System.Runtime.CompilerServices.Unsafe": "6.0.0",
+          "System.Security.Principal.Windows": "5.0.0"
+        }
+      },
+      "Microsoft.Build.Utilities.Core/17.12.6": {
+        "dependencies": {
+          "Microsoft.Build.Framework": "17.12.6",
+          "Microsoft.NET.StringTools": "17.12.6",
+          "Microsoft.Win32.Registry": "5.0.0",
+          "System.Collections.Immutable": "8.0.0",
+          "System.Configuration.ConfigurationManager": "8.0.0",
+          "System.Memory": "4.5.5",
+          "System.Runtime.CompilerServices.Unsafe": "6.0.0",
+          "System.Security.Principal.Windows": "5.0.0",
+          "System.Text.Encoding.CodePages": "7.0.0"
+        }
+      },
+      "Microsoft.NET.StringTools/17.12.6": {
+        "dependencies": {
+          "System.Memory": "4.5.5",
+          "System.Runtime.CompilerServices.Unsafe": "6.0.0"
+        }
+      },
+      "Microsoft.NETCore.Platforms/1.1.0": {},
+      "Microsoft.Win32.Registry/5.0.0": {
+        "dependencies": {
+          "System.Buffers": "4.5.1",
+          "System.Memory": "4.5.5",
+          "System.Security.AccessControl": "5.0.0",
+          "System.Security.Principal.Windows": "5.0.0"
+        }
+      },
+      "NETStandard.Library/2.0.3": {
+        "dependencies": {
+          "Microsoft.NETCore.Platforms": "1.1.0"
+        }
+      },
+      "Newtonsoft.Json/13.0.3": {
+        "runtime": {
+          "lib/netstandard2.0/Newtonsoft.Json.dll": {
+            "assemblyVersion": "13.0.0.0",
+            "fileVersion": "13.0.3.27908"
+          }
+        }
+      },
+      "StyleCop.Analyzers/1.1.118": {},
+      "System.Buffers/4.5.1": {},
+      "System.Collections.Immutable/8.0.0": {
+        "dependencies": {
+          "System.Memory": "4.5.5",
+          "System.Runtime.CompilerServices.Unsafe": "6.0.0"
+        }
+      },
+      "System.Configuration.ConfigurationManager/8.0.0": {
+        "dependencies": {
+          "System.Security.Cryptography.ProtectedData": "8.0.0"
+        }
+      },
+      "System.Memory/4.5.5": {
+        "dependencies": {
+          "System.Buffers": "4.5.1",
+          "System.Numerics.Vectors": "4.4.0",
+          "System.Runtime.CompilerServices.Unsafe": "6.0.0"
+        }
+      },
+      "System.Numerics.Vectors/4.4.0": {},
+      "System.Runtime.CompilerServices.Unsafe/6.0.0": {},
+      "System.Security.AccessControl/5.0.0": {
+        "dependencies": {
+          "System.Security.Principal.Windows": "5.0.0"
+        }
+      },
+      "System.Security.Cryptography.ProtectedData/8.0.0": {
+        "dependencies": {
+          "System.Memory": "4.5.5"
+        }
+      },
+      "System.Security.Principal.Windows/5.0.0": {},
+      "System.Text.Encoding.CodePages/7.0.0": {
+        "dependencies": {
+          "System.Memory": "4.5.5",
+          "System.Runtime.CompilerServices.Unsafe": "6.0.0"
+        }
+      }
+    }
+  },
+  "libraries": {
+    "PixiEditor.Extensions.MSPackageBuilder/1.0.0": {
+      "type": "project",
+      "serviceable": false,
+      "sha512": ""
+    },
+    "Microsoft.Build.Framework/17.12.6": {
+      "type": "package",
+      "serviceable": true,
+      "sha512": "sha512-jleteC0seumLGTmTVwob97lcwPj/dfgzL/V3g/VVcMZgo2Ic7jzdy8AYpByPDh8e3uRq0SjCl6HOFCjhy5GzRQ==",
+      "path": "microsoft.build.framework/17.12.6",
+      "hashPath": "microsoft.build.framework.17.12.6.nupkg.sha512"
+    },
+    "Microsoft.Build.Utilities.Core/17.12.6": {
+      "type": "package",
+      "serviceable": true,
+      "sha512": "sha512-pU3GnHcXp8VRMGKxdJCq+tixfhFn+QwEbpqmZmc/nqFHFyuhlGwjonWZMIWcwuCv/8EHgxoOttFvna1vrN+RrA==",
+      "path": "microsoft.build.utilities.core/17.12.6",
+      "hashPath": "microsoft.build.utilities.core.17.12.6.nupkg.sha512"
+    },
+    "Microsoft.NET.StringTools/17.12.6": {
+      "type": "package",
+      "serviceable": true,
+      "sha512": "sha512-w8Ehofqte5bJoR+Fa3f6JwkwFEkGtXxqvQHGOVOSHDzgNVySvL5FSNhavbQSZ864el9c3rjdLPLAtBW8dq6fmg==",
+      "path": "microsoft.net.stringtools/17.12.6",
+      "hashPath": "microsoft.net.stringtools.17.12.6.nupkg.sha512"
+    },
+    "Microsoft.NETCore.Platforms/1.1.0": {
+      "type": "package",
+      "serviceable": true,
+      "sha512": "sha512-kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==",
+      "path": "microsoft.netcore.platforms/1.1.0",
+      "hashPath": "microsoft.netcore.platforms.1.1.0.nupkg.sha512"
+    },
+    "Microsoft.Win32.Registry/5.0.0": {
+      "type": "package",
+      "serviceable": true,
+      "sha512": "sha512-dDoKi0PnDz31yAyETfRntsLArTlVAVzUzCIvvEDsDsucrl33Dl8pIJG06ePTJTI3tGpeyHS9Cq7Foc/s4EeKcg==",
+      "path": "microsoft.win32.registry/5.0.0",
+      "hashPath": "microsoft.win32.registry.5.0.0.nupkg.sha512"
+    },
+    "NETStandard.Library/2.0.3": {
+      "type": "package",
+      "serviceable": true,
+      "sha512": "sha512-st47PosZSHrjECdjeIzZQbzivYBJFv6P2nv4cj2ypdI204DO+vZ7l5raGMiX4eXMJ53RfOIg+/s4DHVZ54Nu2A==",
+      "path": "netstandard.library/2.0.3",
+      "hashPath": "netstandard.library.2.0.3.nupkg.sha512"
+    },
+    "Newtonsoft.Json/13.0.3": {
+      "type": "package",
+      "serviceable": true,
+      "sha512": "sha512-HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==",
+      "path": "newtonsoft.json/13.0.3",
+      "hashPath": "newtonsoft.json.13.0.3.nupkg.sha512"
+    },
+    "StyleCop.Analyzers/1.1.118": {
+      "type": "package",
+      "serviceable": true,
+      "sha512": "sha512-Onx6ovGSqXSK07n/0eM3ZusiNdB6cIlJdabQhWGgJp3Vooy9AaLS/tigeybOJAobqbtggTamoWndz72JscZBvw==",
+      "path": "stylecop.analyzers/1.1.118",
+      "hashPath": "stylecop.analyzers.1.1.118.nupkg.sha512"
+    },
+    "System.Buffers/4.5.1": {
+      "type": "package",
+      "serviceable": true,
+      "sha512": "sha512-Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==",
+      "path": "system.buffers/4.5.1",
+      "hashPath": "system.buffers.4.5.1.nupkg.sha512"
+    },
+    "System.Collections.Immutable/8.0.0": {
+      "type": "package",
+      "serviceable": true,
+      "sha512": "sha512-AurL6Y5BA1WotzlEvVaIDpqzpIPvYnnldxru8oXJU2yFxFUy3+pNXjXd1ymO+RA0rq0+590Q8gaz2l3Sr7fmqg==",
+      "path": "system.collections.immutable/8.0.0",
+      "hashPath": "system.collections.immutable.8.0.0.nupkg.sha512"
+    },
+    "System.Configuration.ConfigurationManager/8.0.0": {
+      "type": "package",
+      "serviceable": true,
+      "sha512": "sha512-JlYi9XVvIREURRUlGMr1F6vOFLk7YSY4p1vHo4kX3tQ0AGrjqlRWHDi66ImHhy6qwXBG3BJ6Y1QlYQ+Qz6Xgww==",
+      "path": "system.configuration.configurationmanager/8.0.0",
+      "hashPath": "system.configuration.configurationmanager.8.0.0.nupkg.sha512"
+    },
+    "System.Memory/4.5.5": {
+      "type": "package",
+      "serviceable": true,
+      "sha512": "sha512-XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==",
+      "path": "system.memory/4.5.5",
+      "hashPath": "system.memory.4.5.5.nupkg.sha512"
+    },
+    "System.Numerics.Vectors/4.4.0": {
+      "type": "package",
+      "serviceable": true,
+      "sha512": "sha512-UiLzLW+Lw6HLed1Hcg+8jSRttrbuXv7DANVj0DkL9g6EnnzbL75EB7EWsw5uRbhxd/4YdG8li5XizGWepmG3PQ==",
+      "path": "system.numerics.vectors/4.4.0",
+      "hashPath": "system.numerics.vectors.4.4.0.nupkg.sha512"
+    },
+    "System.Runtime.CompilerServices.Unsafe/6.0.0": {
+      "type": "package",
+      "serviceable": true,
+      "sha512": "sha512-/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==",
+      "path": "system.runtime.compilerservices.unsafe/6.0.0",
+      "hashPath": "system.runtime.compilerservices.unsafe.6.0.0.nupkg.sha512"
+    },
+    "System.Security.AccessControl/5.0.0": {
+      "type": "package",
+      "serviceable": true,
+      "sha512": "sha512-dagJ1mHZO3Ani8GH0PHpPEe/oYO+rVdbQjvjJkBRNQkX4t0r1iaeGn8+/ybkSLEan3/slM0t59SVdHzuHf2jmw==",
+      "path": "system.security.accesscontrol/5.0.0",
+      "hashPath": "system.security.accesscontrol.5.0.0.nupkg.sha512"
+    },
+    "System.Security.Cryptography.ProtectedData/8.0.0": {
+      "type": "package",
+      "serviceable": true,
+      "sha512": "sha512-+TUFINV2q2ifyXauQXRwy4CiBhqvDEDZeVJU7qfxya4aRYOKzVBpN+4acx25VcPB9ywUN6C0n8drWl110PhZEg==",
+      "path": "system.security.cryptography.protecteddata/8.0.0",
+      "hashPath": "system.security.cryptography.protecteddata.8.0.0.nupkg.sha512"
+    },
+    "System.Security.Principal.Windows/5.0.0": {
+      "type": "package",
+      "serviceable": true,
+      "sha512": "sha512-t0MGLukB5WAVU9bO3MGzvlGnyJPgUlcwerXn1kzBRjwLKixT96XV0Uza41W49gVd8zEMFu9vQEFlv0IOrytICA==",
+      "path": "system.security.principal.windows/5.0.0",
+      "hashPath": "system.security.principal.windows.5.0.0.nupkg.sha512"
+    },
+    "System.Text.Encoding.CodePages/7.0.0": {
+      "type": "package",
+      "serviceable": true,
+      "sha512": "sha512-LSyCblMpvOe0N3E+8e0skHcrIhgV2huaNcjUUEa8hRtgEAm36aGkRoC8Jxlb6Ra6GSfF29ftduPNywin8XolzQ==",
+      "path": "system.text.encoding.codepages/7.0.0",
+      "hashPath": "system.text.encoding.codepages.7.0.0.nupkg.sha512"
+    }
+  }
+}

BIN
src/PixiEditor.Extensions.Sdk/build/PixiEditor.Extensions.MSPackageBuilder.dll


+ 43 - 0
src/PixiEditor.Extensions.WasmRuntime/Api/Modules/UiModule.cs

@@ -0,0 +1,43 @@
+using System.Text;
+using Avalonia.Controls;
+using PixiEditor.Extensions.CommonApi.FlyUI;
+using PixiEditor.Extensions.FlyUI.Elements;
+
+namespace PixiEditor.Extensions.WasmRuntime.Api.Modules;
+
+internal class UiModule(WasmExtensionInstance extension) : ApiModule(extension)
+{
+    public LayoutBuilder LayoutBuilder => Extension.LayoutBuilder;
+
+    public byte[] ToSerializedNativeElement(ILayoutElement<Control>? element, int elementHandle)
+    {
+        List<byte> result = new List<byte>();
+
+        if (element is not null)
+        {
+            LayoutBuilder.ManagedElements.Add(elementHandle, element);
+
+            var nativeElement = element.BuildNative();
+
+            LayoutBuilder.ElementMap.ControlMapReversed.TryGetValue(element.BuildNative()?.GetType() ?? typeof(Control),
+                out string? controlTypeId);
+
+            if (controlTypeId is null)
+            {
+                if (nativeElement is Panel)
+                {
+                    controlTypeId = "MultiChildNativeElement";
+                }
+                else
+                {
+                    controlTypeId = "NativeElement";
+                }
+            }
+
+            result.AddRange(BitConverter.GetBytes(controlTypeId.Length));
+            result.AddRange(Encoding.UTF8.GetBytes(controlTypeId));
+        }
+
+        return result.ToArray();
+    }
+}

+ 16 - 0
src/PixiEditor.Extensions.WasmRuntime/Api/Modules/WindowingModule.cs

@@ -0,0 +1,16 @@
+using PixiEditor.Extensions.CommonApi.Windowing;
+
+namespace PixiEditor.Extensions.WasmRuntime.Api.Modules;
+
+public class WindowingModule(WasmExtensionInstance extension) : ApiModule(extension)
+{
+    public void SubscribeBuiltInWindowOpened(int type)
+    {
+        BuiltInWindowType windowType = (BuiltInWindowType)type;
+        Extension.Api.Windowing.SubscribeWindowOpened(windowType, (window) =>
+        {
+            int handle = Extension.NativeObjectManager.AddObject(window);
+            Extension.Instance.GetAction<int, int>("on_built_in_window_opened").Invoke(type, handle);
+        });
+    }
+}

+ 45 - 0
src/PixiEditor.Extensions.WasmRuntime/Api/VisualTreeApi.cs

@@ -0,0 +1,45 @@
+using System.Text;
+using Avalonia.Controls;
+using PixiEditor.Extensions.CommonApi.FlyUI;
+using PixiEditor.Extensions.FlyUI.Elements;
+using PixiEditor.Extensions.WasmRuntime.Api.Modules;
+using PixiEditor.Extensions.Windowing;
+
+namespace PixiEditor.Extensions.WasmRuntime.Api;
+
+internal class VisualTreeApi : ApiGroupHandler
+{
+    [ApiFunction("find_ui_element")]
+    public byte[] FindUiElement(string name, int elementHandle)
+    {
+        var element = Api.VisualTree.FindElement<Control>(name);
+
+        var module = Extension.GetModule<UiModule>();
+        byte[] serialized = module.ToSerializedNativeElement(element, elementHandle);
+        return serialized;
+    }
+
+    [ApiFunction("find_ui_element_in_popup")]
+    public byte[] FindUiElement(string name, int popupHandle, int elementHandle)
+    {
+        var element =
+            Api.VisualTree.FindElement<Control>(name, NativeObjectManager.GetObject<PopupWindow>(popupHandle));
+
+        var module = Extension.GetModule<UiModule>();
+        byte[] serialized = module.ToSerializedNativeElement(element, elementHandle);
+        return serialized;
+    }
+
+    [ApiFunction("append_element_to_native_multi_child")]
+    public void AppendElementToNativeMultiChild(int atIndex, int uniqueId, Span<byte> body)
+    {
+        if (LayoutBuilder.ManagedElements.TryGetValue(uniqueId, out var element))
+        {
+            if (element is IChildHost childHost)
+            {
+                var deserializedChild = LayoutBuilder.Deserialize(body, DuplicateResolutionTactic.ThrowException);
+                childHost.AppendChild(atIndex, deserializedChild);
+            }
+        }
+    }
+}

+ 7 - 0
src/PixiEditor.Extensions.WasmRuntime/Api/WindowingApi.cs

@@ -1,5 +1,6 @@
 using PixiEditor.Extensions.CommonApi.Windowing;
 using PixiEditor.Extensions.FlyUI.Elements;
+using PixiEditor.Extensions.WasmRuntime.Api.Modules;
 using PixiEditor.Extensions.WasmRuntime.Utilities;
 using PixiEditor.Extensions.Windowing;
 using PixiEditor.UI.Common.Localization;
@@ -34,6 +35,12 @@ internal class WindowingApi : ApiGroupHandler
         return NativeObjectManager.AddObject(window);
     }
 
+    [ApiFunction("subscribe_built_in_window_opened")]
+    public void SubscribeWindowOpened(int type)
+    {
+        Extension.GetModule<WindowingModule>().SubscribeBuiltInWindowOpened(type);
+    }
+
     [ApiFunction("set_window_title")]
     public void SetWindowTitle(int handle, string title)
     {

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

@@ -22,7 +22,7 @@ public partial class WasmExtensionInstance : Extension
     private Store Store { get; }
     private Module Module { get; }
 
-    private LayoutBuilder LayoutBuilder { get; set; }
+    internal LayoutBuilder LayoutBuilder { get; set; }
     internal ObjectManager NativeObjectManager { get; set; }
     internal AsyncCallsManager AsyncHandleManager { get; set; }
 
@@ -65,6 +65,8 @@ public partial class WasmExtensionInstance : Extension
 
     protected override void OnInitialized()
     {
+        modules.Add(new WindowingModule(this));
+        modules.Add(new UiModule(this));
         modules.Add(new PreferencesModule(this, Api.Preferences));
         modules.Add(new CommandModule(this, Api.Commands,
             (ICommandSupervisor)Api.Services.GetService(typeof(ICommandSupervisor))));
@@ -81,6 +83,12 @@ public partial class WasmExtensionInstance : Extension
         base.OnUserReady();
     }
 
+    protected override void OnMainWindowLoaded()
+    {
+        Instance.GetAction("main_window_loaded").Invoke();
+        base.OnMainWindowLoaded();
+    }
+
     private void OnAsyncCallCompleted(int handle, int result)
     {
         Dispatcher.UIThread.Invoke(() =>

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

@@ -64,6 +64,25 @@ public class WasmMemoryUtility
         return ptr;
     }
 
+    /// <summary>
+    ///     Writes a byte array to memory with 4 bytes prefixed length.
+    /// </summary>
+    /// <param name="bytes">The byte array to write.</param>
+    /// <returns>Integer pointer to the allocated memory containing the length and the byte array.</returns>
+    public int WriteBytesWithEncodedLength(byte[] bytes)
+    {
+        int lenBytesLength = BitConverter.GetBytes(bytes.Length).Length;
+        var length = bytes.Length + lenBytesLength;
+        var ptr = malloc.Invoke(length);
+
+        var span = memory.GetSpan<byte>(ptr, length);
+
+        BitConverter.GetBytes(bytes.Length).CopyTo(span);
+        bytes.CopyTo(span.Slice(lenBytesLength));
+
+        return ptr;
+    }
+
     public int WriteInt32(int value)
     {
         const int length = 4;

+ 14 - 0
src/PixiEditor.Extensions/Extension.cs

@@ -40,6 +40,11 @@ public abstract class Extension
         OnUserReady();
     }
 
+    public void MainWindowLoaded()
+    {
+        OnMainWindowLoaded();
+    }
+
     /// <summary>
     ///     Called right after the extension is loaded. Not all extensions are initialized at this point. PixiEditor API at this point is not available.
     ///     Use this method to load resources, patch language files, etc.
@@ -63,4 +68,13 @@ public abstract class Extension
     {
 
     }
+
+    /// <summary>
+    /// This method is called when the main window is loaded.
+    /// You can use this method to perform actions that require the main window to be fully loaded.
+    /// </summary>
+    protected virtual void OnMainWindowLoaded()
+    {
+
+    }
 }

+ 2 - 0
src/PixiEditor.Extensions/ExtensionServices.cs

@@ -4,6 +4,7 @@ using PixiEditor.Extensions.CommonApi.Commands;
 using PixiEditor.Extensions.CommonApi.IO;
 using PixiEditor.Extensions.CommonApi.Logging;
 using PixiEditor.Extensions.CommonApi.Palettes;
+using PixiEditor.Extensions.CommonApi.Ui;
 using PixiEditor.Extensions.CommonApi.UserPreferences;
 using PixiEditor.Extensions.CommonApi.Windowing;
 using PixiEditor.Extensions.IO;
@@ -20,6 +21,7 @@ public class ExtensionServices
     public IPalettesProvider? Palettes => Services.GetService<IPalettesProvider>();
     public IDocumentProvider Documents => Services.GetService<IDocumentProvider>();
     public ICommandSupervisor CommandSupervisor => Services.GetService<ICommandSupervisor>();
+    public IVisualTreeProvider VisualTree => Services.GetService<IVisualTreeProvider>();
     public ILogger Logger => Services.GetService<ILogger>();
 
     public ExtensionServices(IServiceProvider services)

+ 6 - 0
src/PixiEditor.Extensions/FlyUI/ElementMap.cs

@@ -9,10 +9,12 @@ namespace PixiEditor.Extensions.FlyUI;
 public class ElementMap
 {
     public IReadOnlyDictionary<string, Type> ControlMap => controlMap;
+    public IReadOnlyDictionary<Type, string> ControlMapReversed => controlMapReversed;
     public IReadOnlyDictionary<string, Type> WellKnownStructs => wellKnownStructs;
 
     // TODO: Code generation
     private Dictionary<string, Type> controlMap = new Dictionary<string, Type>();
+    private Dictionary<Type, string> controlMapReversed = new Dictionary<Type, string>();
     private Dictionary<string, Type> wellKnownStructs = new Dictionary<string, Type>();
 
     public ElementMap()
@@ -26,6 +28,10 @@ public class ElementMap
         foreach (var type in layoutElementTypes)
         {
             controlMap.Add(type.Name, type); // TODO: Extension unique name prefix?
+            if (!controlMapReversed.ContainsKey(type))
+            {
+                controlMapReversed.Add(type, type.Name);
+            }
         }
         
         var structTypes = assembly.GetTypes().Where(x => x.GetInterfaces().Contains(typeof(IStructProperty)));

+ 1 - 0
src/PixiEditor.Extensions/FlyUI/Elements/IChildHost.cs

@@ -8,4 +8,5 @@ public interface IChildHost : IEnumerable<ILayoutElement<Control>>
     public void DeserializeChildren(List<ILayoutElement<Control>> children);
     public void AddChild(ILayoutElement<Control> child);
     public void RemoveChild(ILayoutElement<Control> child);
+    public void AppendChild(int atIndex, ILayoutElement<Control> deserializedChild);
 }

+ 4 - 4
src/PixiEditor.Extensions/FlyUI/Elements/LayoutBuilder.cs

@@ -14,10 +14,10 @@ public class LayoutBuilder
     private static int int32Size = sizeof(int);
 
     public Dictionary<int, ILayoutElement<Control>> ManagedElements = new();
-    private ElementMap elementMap;
+    public ElementMap ElementMap { get; }
     public LayoutBuilder(ElementMap elementMap)
     {
-        this.elementMap = elementMap;
+        this.ElementMap = elementMap;
     }
 
     public ILayoutElement<Control> Deserialize(Span<byte> layoutSpan, DuplicateResolutionTactic duplicatedIdTactic)
@@ -40,7 +40,7 @@ public class LayoutBuilder
         int propertiesCount = BitConverter.ToInt32(layoutSpan[offset..(offset + int32Size)]);
         offset += int32Size;
 
-        List<object> properties = DeserializeProperties(layoutSpan, propertiesCount, ref offset, elementMap);
+        List<object> properties = DeserializeProperties(layoutSpan, propertiesCount, ref offset, ElementMap);
 
         int childrenCount = BitConverter.ToInt32(layoutSpan[offset..(offset + int32Size)]);
         offset += int32Size;
@@ -121,7 +121,7 @@ public class LayoutBuilder
     private ILayoutElement<Control> BuildLayoutElement(int uniqueId, string controlId, List<object> properties,
         List<ILayoutElement<Control>> children, DuplicateResolutionTactic duplicatedIdTactic)
     {
-        Type typeToSpawn = elementMap.ControlMap[controlId];
+        Type typeToSpawn = ElementMap.ControlMap[controlId];
         var element = CreateInstance(typeToSpawn);
         
         if(element is not { } layoutElement)

+ 1 - 0
src/PixiEditor.Extensions/FlyUI/Elements/LayoutElement.cs

@@ -15,6 +15,7 @@ namespace PixiEditor.Extensions.FlyUI.Elements;
 public abstract class LayoutElement : ILayoutElement<Control>, INotifyPropertyChanged, IPropertyDeserializable
 {
     public int UniqueId { get; set; }
+    public string ControlTypeId { get; set; } = string.Empty;
 
     public event ElementEventHandler PointerEnter
     {

+ 15 - 2
src/PixiEditor.Extensions/FlyUI/Elements/MultiChildLayoutElement.cs

@@ -45,13 +45,26 @@ public abstract class MultiChildLayoutElement : LayoutElement, IMultiChildLayout
     public void AddChild(ILayoutElement<Control> child)
     {
         Children.Add((LayoutElement)child);
-        //AddChild(child.BuildNative());
     }
 
     public void RemoveChild(ILayoutElement<Control> child)
     {
         int index = Children.IndexOf((LayoutElement)child);
         Children.Remove((LayoutElement)child);
-        //RemoveChild(index);   
+    }
+
+    public void AppendChild(int atIndex, ILayoutElement<Control> deserializedChild)
+    {
+        if (atIndex < 0 || atIndex > Children.Count)
+        {
+            throw new ArgumentOutOfRangeException(nameof(atIndex), "Index is out of range.");
+        }
+
+        if (deserializedChild is not LayoutElement layoutChild)
+        {
+            throw new InvalidOperationException("Deserialized child must be of type LayoutElement.");
+        }
+
+        Children.Insert(atIndex, layoutChild);
     }
 }

+ 17 - 0
src/PixiEditor.Extensions/FlyUI/Elements/Native/NativeElement.cs

@@ -0,0 +1,17 @@
+using Avalonia.Controls;
+
+namespace PixiEditor.Extensions.FlyUI.Elements.Native;
+
+public class NativeElement : LayoutElement
+{
+    public Control Native { get; }
+
+    public NativeElement(Control native)
+    {
+        Native = native;
+    }
+    protected override Control CreateNativeControl()
+    {
+        return Native;
+    }
+}

+ 73 - 0
src/PixiEditor.Extensions/FlyUI/Elements/Native/NativeMultiChildElement.cs

@@ -0,0 +1,73 @@
+using System.Collections;
+using System.Collections.ObjectModel;
+using Avalonia.Controls;
+using PixiEditor.Extensions.CommonApi.FlyUI;
+
+namespace PixiEditor.Extensions.FlyUI.Elements.Native;
+
+public class NativeMultiChildElement : NativeElement, IChildHost, IMultiChildLayoutElement<Control>
+{
+    private Panel nativePanel => (Panel)Native;
+
+    public NativeMultiChildElement(Panel native) : base(native)
+    {
+    }
+
+    List<ILayoutElement<Control>> IMultiChildLayoutElement<Control>.Children
+    {
+        get => Children.Cast<ILayoutElement<Control>>().ToList();
+        set => throw new NotSupportedException("Setting children directly is not supported for NativeMultiChildElement.");
+    }
+
+    public ObservableCollection<LayoutElement> Children => WrapPanelChildren();
+
+    public IEnumerator<ILayoutElement<Control>> GetEnumerator()
+    {
+        return Children.ToList().GetEnumerator();
+    }
+
+    IEnumerator IEnumerable.GetEnumerator()
+    {
+        return GetEnumerator();
+    }
+
+    public void DeserializeChildren(List<ILayoutElement<Control>> children)
+    {
+        throw new NotSupportedException("Deserializing children directly is not supported for NativeMultiChildElement.");
+    }
+
+    public void AddChild(ILayoutElement<Control> child)
+    {
+        Children.Add((LayoutElement)child);
+    }
+
+    public void RemoveChild(ILayoutElement<Control> child)
+    {
+        Children.Remove((LayoutElement)child);
+    }
+
+    public void AppendChild(int atIndex, ILayoutElement<Control> deserializedChild)
+    {
+        if (atIndex < 0 || atIndex > Children.Count)
+        {
+            throw new ArgumentOutOfRangeException(nameof(atIndex), "Index is out of range.");
+        }
+
+        LayoutElement layoutChild = (LayoutElement)deserializedChild;
+
+        nativePanel.Children.Insert(atIndex, layoutChild.BuildNative());
+        Children.Insert(atIndex, layoutChild);
+    }
+
+    private ObservableCollection<LayoutElement> WrapPanelChildren()
+    {
+        ObservableCollection<LayoutElement> wrappedChildren = new();
+        foreach (var child in nativePanel.Children)
+        {
+            NativeElement nativeChild = new NativeElement(child);
+            wrappedChildren.Add(nativeChild);
+        }
+
+        return wrappedChildren;
+    }
+}

+ 11 - 0
src/PixiEditor.Extensions/FlyUI/Elements/SingleChildLayoutElement.cs

@@ -43,6 +43,17 @@ public abstract class SingleChildLayoutElement : LayoutElement, ISingleChildLayo
         RemoveChild();
     }
 
+    public void AppendChild(int atIndex, ILayoutElement<Control> deserializedChild)
+    {
+        if (atIndex != 0)
+        {
+            throw new ArgumentOutOfRangeException(nameof(atIndex), "SingleChildLayoutElement can only have one child at index 0.");
+        }
+
+        Child = (LayoutElement)deserializedChild;
+        AddChild(deserializedChild.BuildNative());
+    }
+
     public IEnumerator<ILayoutElement<Control>> GetEnumerator()
     {
         if (Child != null)

+ 10 - 0
src/PixiEditor.Extensions/FlyUI/Elements/StatefulContainer.cs

@@ -26,6 +26,16 @@ public class StatefulContainer : StatefulElement<ContainerState>, IChildHost
         State.SetState(() => State.Content = null);
     }
 
+    public void AppendChild(int atIndex, ILayoutElement<Control> deserializedChild)
+    {
+        if (atIndex != 0)
+        {
+            throw new NotSupportedException("Appending children at an index other than 0 is not supported for StatefulContainer.");
+        }
+
+        State.SetState(() => State.Content = (LayoutElement)deserializedChild);
+    }
+
     public IEnumerator<ILayoutElement<Control>> GetEnumerator()
     {
         yield return State.Content;

+ 1 - 0
src/PixiEditor.Extensions/Windowing/PopupWindow.cs

@@ -5,6 +5,7 @@ namespace PixiEditor.Extensions.Windowing;
 
 public class PopupWindow : IPopupWindow
 {
+    public IPopupWindow UnderlyingWindow => _underlyingWindow;
     private IPopupWindow _underlyingWindow;
 
     public PopupWindow(IPopupWindow basicPopup)

+ 7 - 1
src/PixiEditor.Platform.Standalone/StandaloneAdditionalContentProvider.cs

@@ -80,7 +80,13 @@ public sealed class StandaloneAdditionalContentProvider : IAdditionalContentProv
         if (string.IsNullOrEmpty(productId)) return false;
 
         string filePath = Path.Combine(ExtensionsPath, $"{productId}.pixiext");
-        return File.Exists(filePath);
+        bool exists = File.Exists(filePath);
+        if (exists) return true;
+
+        filePath = Path.Combine(Path.GetDirectoryName(Environment.ProcessPath), "Extensions", $"{productId}.pixiext");
+        exists = File.Exists(filePath);
+
+        return exists;
     }
 
     public bool IsContentOwned(string product)

+ 1 - 1
src/PixiEditor.WasmApi.Gen/ApiGenerator.cs

@@ -142,7 +142,7 @@ public class ApiGenerator : IIncrementalGenerator
                 }
                 else
                 {
-                    var returnType = method.methodSymbol.ReturnType.Name;
+                    var returnType = method.methodSymbol.ReturnType.TypeKind == TypeKind.Array ? "BytesWithEncodedLength" : method.methodSymbol.ReturnType.Name;
                     string statementString =
                         $"return WasmMemoryUtility.Write{returnType}({returnStatementSyntax.Expression.ToFullString()});";
 

+ 2 - 0
src/PixiEditor/Helpers/ServiceCollectionHelpers.cs

@@ -10,6 +10,7 @@ using PixiEditor.Extensions.CommonApi.IO;
 using PixiEditor.Extensions.CommonApi.Logging;
 using PixiEditor.Extensions.CommonApi.Palettes;
 using PixiEditor.Extensions.CommonApi.Palettes.Parsers;
+using PixiEditor.Extensions.CommonApi.Ui;
 using PixiEditor.Extensions.CommonApi.UserPreferences;
 using PixiEditor.Extensions.CommonApi.Windowing;
 using PixiEditor.Extensions.FlyUI;
@@ -214,5 +215,6 @@ internal static class ServiceCollectionHelpers
             })
             .AddSingleton<ICommandSupervisor, CommandSupervisor>()
             .AddSingleton<ILogger, ConsoleLogger>()
+            .AddSingleton<IVisualTreeProvider, VisualTreeProvider>()
             .AddSingleton<IFileSystemProvider, FileSystemProvider>();
 }

+ 75 - 0
src/PixiEditor/Models/ExtensionServices/VisualTreeProvider.cs

@@ -0,0 +1,75 @@
+using Avalonia.Controls;
+using Avalonia.VisualTree;
+using PixiEditor.Extensions.CommonApi.FlyUI;
+using PixiEditor.Extensions.CommonApi.Ui;
+using PixiEditor.Extensions.CommonApi.Windowing;
+using PixiEditor.Extensions.FlyUI.Elements;
+using PixiEditor.Extensions.FlyUI.Elements.Native;
+using PixiEditor.Extensions.Windowing;
+using PixiEditor.Views;
+
+namespace PixiEditor.Models.ExtensionServices;
+
+public class VisualTreeProvider : IVisualTreeProvider
+{
+    ILayoutElement<T>? IVisualTreeProvider.FindElement<T>(string name)
+    {
+        return FindElement(name) as ILayoutElement<T>;
+    }
+
+    ILayoutElement<T>? IVisualTreeProvider.FindElement<T>(string name, IPopupWindow root)
+    {
+        return FindElement(name, root) as ILayoutElement<T>;
+    }
+
+    public ILayoutElement<Control>? FindElement(string name, IPopupWindow root)
+    {
+        var control = RecursiveLookup((root as PopupWindow).UnderlyingWindow as Window, name);
+        return ToNativeType(control);
+    }
+    public ILayoutElement<Control>? FindElement(string name)
+    {
+        var control = RecursiveLookup(MainWindow.Current, name);
+        return ToNativeType(control);
+    }
+
+    private static ILayoutElement<Control>? ToNativeType(Control? control)
+    {
+        if (control is null)
+        {
+            return null;
+        }
+
+        if (control is Panel panel)
+        {
+            NativeMultiChildElement nativeElement = new NativeMultiChildElement(panel);
+            return nativeElement;
+        }
+
+        return new NativeElement(control);
+    }
+
+    private Control? RecursiveLookup(Control? control, string name)
+    {
+        if (control is null)
+        {
+            return null;
+        }
+
+        if (control.Name == name)
+        {
+            return control;
+        }
+
+        foreach (var child in control.GetVisualChildren())
+        {
+            var found = RecursiveLookup(child as Control, name);
+            if (found != null)
+            {
+                return found;
+            }
+        }
+
+        return null;
+    }
+}

+ 41 - 0
src/PixiEditor/Models/ExtensionServices/WindowProvider.cs

@@ -13,6 +13,7 @@ namespace PixiEditor.Models.ExtensionServices;
 public class WindowProvider : IWindowProvider
 {
     private readonly Dictionary<string, Type> registeredWindows = new();
+    private readonly Dictionary<string, List<Action<IPopupWindow>>> windowOpenedCallbacks = new();
     private ExtensionLoader extensionLoader;
     private IServiceProvider services;
 
@@ -20,6 +21,26 @@ public class WindowProvider : IWindowProvider
     {
         this.extensionLoader = loader;
         this.services = services;
+        PixiEditorPopup.PopupLoaded += PixiEditorPopupOnPopupLoaded;
+        PixiEditorPopup.PopupClosed += PixiEditorPopupOnPopupClosed;
+    }
+
+    private void PixiEditorPopupOnPopupLoaded(PixiEditorPopup obj)
+    {
+        string id = extensionLoader.GetTypeId(obj.GetType());
+        PopupWindow popupWindow = new PopupWindow(obj);
+        if(windowOpenedCallbacks.TryGetValue(id, out List<Action<IPopupWindow>> actions))
+        {
+            foreach (var action in actions)
+            {
+                action?.Invoke(popupWindow);
+            }
+        }
+    }
+
+    private void PixiEditorPopupOnPopupClosed(PixiEditorPopup obj)
+    {
+
     }
 
     public WindowProvider RegisterWindow<T>() where T : IPopupWindow
@@ -61,6 +82,26 @@ public class WindowProvider : IWindowProvider
         throw new ArgumentException($"Window with id {windowId} does not exist");
     }
 
+    public void SubscribeWindowOpened(BuiltInWindowType type, Action<IPopupWindow> action)
+    {
+        string id = type.GetDescription();
+        string fullId = $"PixiEditor.{id}";
+        if (registeredWindows.ContainsKey(fullId))
+        {
+            if (!windowOpenedCallbacks.TryGetValue(fullId, out List<Action<IPopupWindow>> actions))
+            {
+                actions = new List<Action<IPopupWindow>>();
+                windowOpenedCallbacks[fullId] = actions;
+            }
+
+            actions.Add(action);
+        }
+        else
+        {
+            throw new ArgumentException($"Window with id {id} does not exist");
+        }
+    }
+
     private object?[] TryGetConstructorArgs(Type handler)
     {
         ConstructorInfo[] constructors = handler.GetConstructors();

+ 0 - 20
src/PixiEditor/ViewModels/SubViewModels/AdditionalContent/AdditionalContentViewModel.cs

@@ -9,25 +9,5 @@ internal class AdditionalContentViewModel : SubViewModel<ViewModelMain>
     public AdditionalContentViewModel(ViewModelMain owner, IAdditionalContentProvider additionalContentProvider) : base(owner)
     {
         AdditionalContentProvider = additionalContentProvider;
-        Owner.ExtensionsSubViewModel.ExtensionLoader.ExtensionLoaded += OnExtensionLoaded;
-        IPlatform.Current.IdentityProvider.OwnedProductsUpdated += ProviderOnOwnedProductsUpdated;
-    }
-
-
-    public bool IsFoundersPackAvailable => AdditionalContentProvider != null
-                                           && AdditionalContentProvider.IsContentOwned("PixiEditor.FoundersPack")
-                                           && ViewModelMain.Current.ExtensionsSubViewModel.ExtensionLoader.LoadedExtensions.Any(x => x.Metadata.UniqueName == "PixiEditor.FoundersPack");
-
-    private void ProviderOnOwnedProductsUpdated(List<ProductData> obj)
-    {
-        OnPropertyChanged(nameof(IsFoundersPackAvailable));
-    }
-
-    private void OnExtensionLoaded(string uniqueId)
-    {
-        if (uniqueId == "PixiEditor.FoundersPack")
-        {
-            OnPropertyChanged(nameof(IsFoundersPackAvailable));
-        }
     }
 }

+ 33 - 2
src/PixiEditor/ViewModels/SubViewModels/ExtensionsViewModel.cs

@@ -1,9 +1,13 @@
-using Microsoft.Extensions.DependencyInjection;
+using Avalonia;
+using Avalonia.Interactivity;
+using Microsoft.Extensions.DependencyInjection;
 using PixiEditor.Extensions;
 using PixiEditor.Extensions.CommonApi.Windowing;
 using PixiEditor.Extensions.Runtime;
+using PixiEditor.Helpers.Extensions;
 using PixiEditor.Models.ExtensionServices;
 using PixiEditor.UI.Common.Localization;
+using PixiEditor.Views;
 using PixiEditor.Views.Windows;
 
 namespace PixiEditor.ViewModels.SubViewModels;
@@ -23,6 +27,26 @@ internal class ExtensionsViewModel : SubViewModel<ViewModelMain>
         RegisterCoreWindows(windowProvider);
         Owner.OnEarlyStartupEvent += Owner_OnEarlyStartupEvent;
         Owner.OnUserReady += Owner_OnUserReady;
+        if (Owner.AttachedWindow != null)
+        {
+            OwnerOnAttachedToWindow(Owner.AttachedWindow);
+        }
+        else
+        {
+            Owner.AttachedToWindow += OwnerOnAttachedToWindow;
+        }
+    }
+
+    private void OwnerOnAttachedToWindow(MainWindow obj)
+    {
+        if (obj.IsLoaded)
+        {
+            MainWindowLoaded(obj, null);
+        }
+        else
+        {
+            obj.Loaded += MainWindowLoaded;
+        }
     }
 
     public void LoadExtensionAdHoc(string extension)
@@ -35,7 +59,8 @@ internal class ExtensionsViewModel : SubViewModel<ViewModelMain>
                 return;
             }
 
-            ILocalizationProvider.Current.LoadExtensionData(loadedExtension.Metadata.Localization?.Languages, loadedExtension.Location);
+            ILocalizationProvider.Current.LoadExtensionData(loadedExtension.Metadata.Localization?.Languages,
+                loadedExtension.Location);
             loadedExtension.Initialize(new ExtensionServices(Owner.Services));
         }
     }
@@ -43,6 +68,7 @@ internal class ExtensionsViewModel : SubViewModel<ViewModelMain>
     private void RegisterCoreWindows(WindowProvider? windowProvider)
     {
         windowProvider?.RegisterWindow<PalettesBrowser>();
+        windowProvider?.RegisterWindow<HelloTherePopup>();
     }
 
     private void Owner_OnEarlyStartupEvent()
@@ -54,4 +80,9 @@ internal class ExtensionsViewModel : SubViewModel<ViewModelMain>
     {
         ExtensionLoader.InvokeOnUserReady();
     }
+
+    private void MainWindowLoaded(object? sender, RoutedEventArgs e)
+    {
+        ExtensionLoader.InvokeMainWindowLoaded();
+    }
 }

+ 3 - 0
src/PixiEditor/ViewModels/ViewModelMain.cs

@@ -103,6 +103,8 @@ internal partial class ViewModelMain : ViewModelBase, ICommandsHandler
     
     public event Action<MainWindow> AttachedToWindow;
 
+    public MainWindow? AttachedWindow { get; private set; }
+
     public ViewModelMain()
     {
         Current = this;
@@ -407,6 +409,7 @@ internal partial class ViewModelMain : ViewModelBase, ICommandsHandler
 
     public void AttachToWindow(MainWindow mainWindow)
     {
+        AttachedWindow = mainWindow;
         AttachedToWindow?.Invoke(mainWindow);
     }
 

+ 15 - 2
src/PixiEditor/Views/Dialogs/PixiEditorPopup.cs

@@ -14,6 +14,7 @@ using PixiEditor.Extensions.CommonApi;
 using PixiEditor.Extensions.CommonApi.Async;
 using PixiEditor.Extensions.CommonApi.Windowing;
 using PixiEditor.Extensions.UI;
+using PixiEditor.ViewModels;
 
 namespace PixiEditor.Views.Dialogs;
 
@@ -21,8 +22,8 @@ namespace PixiEditor.Views.Dialogs;
 [TemplatePart("Part_TitleBar", typeof(DialogTitleBar))]
 public partial class PixiEditorPopup : Window, IPopupWindow
 {
-    public string UniqueId => "PixiEditor.Popup";
-
+    public static event Action<PixiEditorPopup> PopupLoaded;
+    public static event Action<PixiEditorPopup> PopupClosed;
     public static readonly StyledProperty<bool> CanMinimizeProperty = AvaloniaProperty.Register<PixiEditorPopup, bool>(
         nameof(CanMinimize), defaultValue: true);
 
@@ -136,6 +137,18 @@ public partial class PixiEditorPopup : Window, IPopupWindow
         return AsyncCall<bool?>.FromTask(ShowDialog<bool?>(MainWindow.Current));
     }
 
+    protected override void OnLoaded(RoutedEventArgs e)
+    {
+        base.OnLoaded(e);
+        PopupLoaded?.Invoke(this);
+    }
+
+    protected override void OnClosed(EventArgs e)
+    {
+        base.OnClosed(e);
+        PopupClosed?.Invoke(this);
+    }
+
     [RelayCommand]
     public void SetResultAndCloseCommand()
     {

+ 1 - 6
src/PixiEditor/Views/Main/MainTitleBar.axaml

@@ -24,13 +24,8 @@
             DockPanel.Dock="Top">
             <dialogs:DialogTitleBar.AdditionalElement>
                 <StackPanel Margin="5 0" Spacing="5" Orientation="Horizontal"
+                            Name="MainBarAdditionalElementPanel"
                             DataContext="{Binding Path=DataContext, RelativeSource={RelativeSource FindAncestor, AncestorType=main:MainTitleBar}}">
-                    <Border BorderThickness="1" BorderBrush="{DynamicResource FoundersPurpleBrush}"
-                            Padding="5 0" CornerRadius="5" Height="25"
-                            IsVisible="{Binding Path=AdditionalContentSubViewModel.IsFoundersPackAvailable, FallbackValue=False}">
-                        <TextBlock VerticalAlignment="Center" Foreground="{DynamicResource FoundersPurpleBrush}"
-                                   ui:Translator.Key="PixiEditor.FoundersPack:AWESOME_SUPPORTER" />
-                    </Border>
                     <auth:UserAvatarToggle Width="26" Height="26" DataContext="{Binding Path=UserViewModel}" />
                 </StackPanel>
             </dialogs:DialogTitleBar.AdditionalElement>

+ 8 - 15
src/PixiEditor/Views/Windows/HelloTherePopup.axaml

@@ -71,7 +71,7 @@
                                    Text="{Binding VersionText}" />
                     </StackPanel>
 
-                    <StackPanel Grid.Row="1" Orientation="Vertical" HorizontalAlignment="Center">
+                    <StackPanel Name="FilesRow" Grid.Row="1" Orientation="Vertical" HorizontalAlignment="Center">
                         <StackPanel Orientation="Horizontal">
                             <Button Command="{Binding OpenFileCommand}" MinWidth="150" Margin="10"
                                     ui:Translator.Key="OPEN_FILE" />
@@ -82,25 +82,18 @@
                                     ui:Translator.TooltipKey="NEW_FROM_CLIPBOARD" />
                         </StackPanel>
 
-                        <StackPanel Orientation="Horizontal">
-                            <StackPanel.Styles>
-                                <Style Selector="Button.fullWidth">
-                                    <Setter Property="Width" Value="320" />
-                                </Style>
-                            </StackPanel.Styles>
-                            <Button Command="{xaml:Command Name=PixiEditor.FoundersPack:Workspaces.Browse}"
-                                    IsVisible="{Binding Path=FileViewModel.Owner.AdditionalContentSubViewModel.IsFoundersPackAvailable, FallbackValue=False, RelativeSource={RelativeSource FindAncestor, AncestorType=windows:HelloTherePopup}}"
-                                    Name="workspacesButton"
-                                    MinWidth="150" Margin="10, 0"
-                                    ui:Translator.Key="PixiEditor.FoundersPack:BROWSE_TEMPLATES" />
+                        <UniformGrid Width="320"
+                                     Margin="10,0,0,0"
+                                     HorizontalAlignment="Left"
+                                     ColumnSpacing="20"
+                                     Columns="{Binding ElementName=ExampleFilesGrid, Path=Children.Count}"
+                                     Name="ExampleFilesGrid">
                             <Button
                                     Background="{DynamicResource ThemeAccentBrush2}"
                                     ui:Translator.Key="EXAMPLE_FILES"
-                                    Margin="10, 0" MinWidth="150"
-                                    Classes.fullWidth="{Binding !FileViewModel.Owner.AdditionalContentSubViewModel.IsFoundersPackAvailable, FallbackValue=False, RelativeSource={RelativeSource FindAncestor, AncestorType=windows:HelloTherePopup}}"
                                     Command="{Binding SetShowAllBetaExamplesCommand, RelativeSource={RelativeSource AncestorType=windows:HelloTherePopup}}"
                                     CommandParameter="{x:True}" />
-                        </StackPanel>
+                        </UniformGrid>
                     </StackPanel>
 
                     <StackPanel Grid.Row="2" HorizontalAlignment="Center">