flabbet 1 год назад
Родитель
Сommit
df17edb693
38 измененных файлов с 798 добавлено и 30 удалено
  1. 97 0
      samples/Custom.ruleset
  2. 103 0
      samples/Directory.Build.props
  3. 6 0
      samples/PixiEditorExtensionSamples.sln
  4. 25 0
      samples/Sample6_Palettes/ExamplePaletteDataSource.cs
  5. 27 0
      samples/Sample6_Palettes/PalettesSampleExtension.cs
  6. 14 0
      samples/Sample6_Palettes/Program.cs
  7. 42 0
      samples/Sample6_Palettes/Sample6_Palettes.csproj
  8. 27 0
      samples/Sample6_Palettes/extension.json
  9. 20 0
      samples/stylecop.json
  10. 1 1
      src/PixiEditor.AvaloniaUI/Helpers/ServiceCollectionHelpers.cs
  11. 12 3
      src/PixiEditor.AvaloniaUI/Models/ExtensionServices/PaletteProvider.cs
  12. 1 1
      src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/ColorsViewModel.cs
  13. 2 2
      src/PixiEditor.AvaloniaUI/Views/Palettes/PaletteItem.axaml
  14. 1 0
      src/PixiEditor.AvaloniaUI/Views/Windows/PalettesBrowser.axaml.cs
  15. 90 8
      src/PixiEditor.Extensions.CommonApi/Async/AsyncCall.cs
  16. 7 0
      src/PixiEditor.Extensions.CommonApi/IByteSerializable.cs
  17. 18 2
      src/PixiEditor.Extensions.CommonApi/Palettes/ExtensionPalette.cs
  18. 36 0
      src/PixiEditor.Extensions.CommonApi/Palettes/FetchPaletteListQuery.cs
  19. 17 1
      src/PixiEditor.Extensions.CommonApi/Palettes/FilteringSettings.cs
  20. 10 0
      src/PixiEditor.Extensions.CommonApi/Palettes/IPalettesProvider.cs
  21. 10 1
      src/PixiEditor.Extensions.CommonApi/Palettes/PaletteColor.cs
  22. 20 0
      src/PixiEditor.Extensions.CommonApi/Palettes/PaletteListResult.cs
  23. 4 0
      src/PixiEditor.Extensions.CommonApi/PixiEditor.Extensions.CommonApi.csproj
  24. 12 0
      src/PixiEditor.Extensions.Wasm/Api/Palettes/PalettesProvider.cs
  25. 55 0
      src/PixiEditor.Extensions.Wasm/Bridge/Interop.Palettes.cs
  26. 4 2
      src/PixiEditor.Extensions.Wasm/Bridge/Interop.cs
  27. 25 0
      src/PixiEditor.Extensions.Wasm/Bridge/Native.Palettes.cs
  28. 0 4
      src/PixiEditor.Extensions.Wasm/PixiEditor.Extensions.Wasm.csproj
  29. 3 0
      src/PixiEditor.Extensions.Wasm/PixiEditorApi.cs
  30. 7 0
      src/PixiEditor.Extensions.Wasm/Utilities/InteropUtility.cs
  31. 1 1
      src/PixiEditor.Extensions.WasmRuntime/Api/ApiGroupHandler.cs
  32. 39 0
      src/PixiEditor.Extensions.WasmRuntime/Api/Palettes/ExtensionPalettesDataSource.cs
  33. 29 0
      src/PixiEditor.Extensions.WasmRuntime/Api/Palettes/PalettesApi.cs
  34. 17 0
      src/PixiEditor.Extensions.WasmRuntime/Management/AsyncManager.cs
  35. 1 0
      src/PixiEditor.Extensions.WasmRuntime/PixiEditor.Extensions.WasmRuntime.csproj
  36. 5 4
      src/PixiEditor.Extensions.WasmRuntime/WasmExtensionInstance.cs
  37. 8 0
      src/PixiEditor.Extensions.WasmRuntime/WasmMemoryUtility.cs
  38. 2 0
      src/PixiEditor.Extensions/ExtensionServices.cs

+ 97 - 0
samples/Custom.ruleset

@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RuleSet Name="Name" Description="Description" ToolsVersion="17.0">
+  <Rules AnalyzerId="Microsoft.CodeAnalysis.CSharp" RuleNamespace="Microsoft.CodeAnalysis.CSharp">
+    <Rule Id="AD0001" Action="None" />
+  </Rules>
+  <Rules AnalyzerId="Microsoft.CodeAnalysis.CSharp.Features" RuleNamespace="Microsoft.CodeAnalysis.CSharp.Features">
+    <Rule Id="IDE0090" Action="None" />
+  </Rules>
+  <Rules AnalyzerId="Microsoft.CodeAnalysis.NetAnalyzers" RuleNamespace="Microsoft.CodeAnalysis.NetAnalyzers">
+    <Rule Id="CA1416" Action="None" />
+  </Rules>
+  <Rules AnalyzerId="Microsoft.NetCore.Analyzers" RuleNamespace="Microsoft.NetCore.Analyzers">
+    <Rule Id="CA1303" Action="None" />
+    <Rule Id="CA1416" Action="None" />
+  </Rules>
+  <Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.Analyzers">
+    <Rule Id="SA0001" Action="None" />
+    <Rule Id="SA1000" Action="None" />
+    <Rule Id="SA1005" Action="None" />
+    <Rule Id="SA1008" Action="None" />
+    <Rule Id="SA1009" Action="None" />
+    <Rule Id="SA1011" Action="None" />
+    <Rule Id="SA1023" Action="None" />
+    <Rule Id="SA1028" Action="None" />
+    <Rule Id="SA1101" Action="None" />
+    <Rule Id="SA1110" Action="None" />
+    <Rule Id="SA1111" Action="None" />
+    <Rule Id="SA1112" Action="None" />
+    <Rule Id="SA1117" Action="None" />
+    <Rule Id="SA1119" Action="None" />
+    <Rule Id="SA1121" Action="None" />
+    <Rule Id="SA1122" Action="None" />
+    <Rule Id="SA1124" Action="None" />
+    <Rule Id="SA1127" Action="None" />
+    <Rule Id="SA1128" Action="None" />
+    <Rule Id="SA1129" Action="None" />
+    <Rule Id="SA1130" Action="None" />
+    <Rule Id="SA1132" Action="None" />
+    <Rule Id="SA1135" Action="None" />
+    <Rule Id="SA1136" Action="None" />
+    <Rule Id="SA1139" Action="None" />
+    <Rule Id="SA1200" Action="None" />
+    <Rule Id="SA1201" Action="None" />
+    <Rule Id="SA1202" Action="None" />
+    <Rule Id="SA1204" Action="None" />
+    <Rule Id="SA1207" Action="None" />
+    <Rule Id="SA1208" Action="None" />
+    <Rule Id="SA1209" Action="None" />
+    <Rule Id="SA1210" Action="None" />
+    <Rule Id="SA1211" Action="None" />
+    <Rule Id="SA1214" Action="None" />
+    <Rule Id="SA1216" Action="None" />
+    <Rule Id="SA1217" Action="None" />
+    <Rule Id="SA1303" Action="None" />
+    <Rule Id="SA1304" Action="None" />
+    <Rule Id="SA1307" Action="None" />
+    <Rule Id="SA1309" Action="None" />
+    <Rule Id="SA1310" Action="None" />
+    <Rule Id="SA1311" Action="None" />
+    <Rule Id="SA1313" Action="None" />
+    <Rule Id="SA1316" Action="None" />
+    <Rule Id="SA1400" Action="None" />
+    <Rule Id="SA1401" Action="None" />
+    <Rule Id="SA1402" Action="None" />
+    <Rule Id="SA1405" Action="None" />
+    <Rule Id="SA1406" Action="None" />
+    <Rule Id="SA1407" Action="None" />
+    <Rule Id="SA1408" Action="None" />
+    <Rule Id="SA1410" Action="None" />
+    <Rule Id="SA1411" Action="None" />
+    <Rule Id="SA1413" Action="None" />
+    <Rule Id="SA1501" Action="None" />
+    <Rule Id="SA1502" Action="None" />
+    <Rule Id="SA1503" Action="None" />
+    <Rule Id="SA1505" Action="None" />
+    <Rule Id="SA1507" Action="None" />
+    <Rule Id="SA1508" Action="None" />
+    <Rule Id="SA1512" Action="None" />
+    <Rule Id="SA1513" Action="None" />
+    <Rule Id="SA1515" Action="None" />
+    <Rule Id="SA1516" Action="None" />
+    <Rule Id="SA1518" Action="None" />
+    <Rule Id="SA1600" Action="None" />
+    <Rule Id="SA1601" Action="None" />
+    <Rule Id="SA1602" Action="None" />
+    <Rule Id="SA1604" Action="None" />
+    <Rule Id="SA1605" Action="None" />
+    <Rule Id="SA1606" Action="None" />
+    <Rule Id="SA1607" Action="None" />
+    <Rule Id="SA1623" Action="None" />
+    <Rule Id="SA1629" Action="None" />
+    <Rule Id="SA1633" Action="None" />
+    <Rule Id="SA1642" Action="None" />
+    <Rule Id="SA1643" Action="None" />
+    <Rule Id="SA1648" Action="None" />
+  </Rules>
+</RuleSet>

+ 103 - 0
samples/Directory.Build.props

@@ -0,0 +1,103 @@
+<Project>
+    <PropertyGroup>
+        <CodeAnalysisRuleSet>../Custom.ruleset</CodeAnalysisRuleSet>
+		    <AvaloniaVersion>11.0.10</AvaloniaVersion>
+    </PropertyGroup>
+    <ItemGroup>
+        <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" />
+    </ItemGroup>
+    <ItemGroup>
+        <AdditionalFiles Include="../stylecop.json" />
+    </ItemGroup>
+
+  <PropertyGroup Condition="$([MSBuild]::IsOsPlatform('Windows')) AND '$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)' == 'X64'">
+    <RuntimeIdentifier>win-x64</RuntimeIdentifier>
+  </PropertyGroup>
+  <PropertyGroup Condition="$([MSBuild]::IsOsPlatform('Windows')) AND '$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)' == 'Arm64'">
+    <RuntimeIdentifier>win-arm64</RuntimeIdentifier>
+  </PropertyGroup>
+  <PropertyGroup Condition="$([MSBuild]::IsOsPlatform('Linux')) AND '$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)' == 'X64'">
+    <RuntimeIdentifier>linux-x64</RuntimeIdentifier>
+  </PropertyGroup>
+  <PropertyGroup Condition="$([MSBuild]::IsOsPlatform('Linux')) AND '$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)' == 'Arm64'">
+    <RuntimeIdentifier>linux-arm64</RuntimeIdentifier>
+  </PropertyGroup>
+  <PropertyGroup Condition="$([MSBuild]::IsOsPlatform('OSX')) AND '$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)' == 'X64'">
+    <RuntimeIdentifier>osx-x64</RuntimeIdentifier>
+  </PropertyGroup>
+  <PropertyGroup Condition="$([MSBuild]::IsOsPlatform('OSX')) AND '$([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture)' == 'Arm64'">
+    <RuntimeIdentifier>osx-arm64</RuntimeIdentifier>
+  </PropertyGroup>
+
+  <PropertyGroup Condition="'$(Platform)'=='x64'">
+    <PlatformTarget>x64</PlatformTarget>
+  </PropertyGroup>
+
+  <PropertyGroup Condition="'$(Platform)'=='arm64'">
+    <PlatformTarget>arm64</PlatformTarget>
+  </PropertyGroup>
+
+  <PropertyGroup Condition="'$(Configuration)'=='MSIX Debug'">
+    <DebugType>full</DebugType>
+    <DebugSymbols>true</DebugSymbols>
+    <Optimize>false</Optimize>
+    <DefineConstants>DEBUG;TRACE</DefineConstants>
+  </PropertyGroup>
+
+  <PropertyGroup Condition="'$(Configuration)'=='MSIX'">
+    <DefineConstants>TRACE;RELEASE</DefineConstants>
+    <Optimize>true</Optimize>
+  </PropertyGroup>
+
+  <PropertyGroup Condition="'$(Configuration)'=='Release'">
+    <DefineConstants>TRACE;UPDATE</DefineConstants>
+    <Optimize>true</Optimize>
+  </PropertyGroup>
+
+  <PropertyGroup Condition="'$(Configuration)'=='Debug'">
+    <DebugType>full</DebugType>
+    <DebugSymbols>true</DebugSymbols>
+    <WarningLevel>0</WarningLevel>
+    <DefineConstants>DEBUG;TRACE</DefineConstants>
+  </PropertyGroup>
+
+  <PropertyGroup Condition="'$(Configuration)'=='Steam'">
+    <DefineConstants>TRACE;RELEASE;STEAM</DefineConstants>
+    <Optimize>True</Optimize>
+  </PropertyGroup>
+
+  <PropertyGroup Condition="'$(Configuration)'=='DevSteam'">
+    <DefineConstants>TRACE;RELEASE;STEAM</DefineConstants>
+    <Optimize>True</Optimize>
+  </PropertyGroup>
+
+  <PropertyGroup Condition=" '$(Configuration)' == 'DevRelease' ">
+    <DefineConstants>TRACE;UPDATE;RELEASE</DefineConstants>
+    <Optimize>True</Optimize>
+  </PropertyGroup>
+
+  <PropertyGroup Condition="'$(RuntimeIdentifier)'=='win-x64'">
+    <DefineConstants>WINDOWS</DefineConstants>
+  </PropertyGroup>
+
+  <PropertyGroup Condition="'$(RuntimeIdentifier)'=='win-arm64'">
+    <DefineConstants>WINDOWS</DefineConstants>
+  </PropertyGroup>
+
+  <PropertyGroup Condition="'$(RuntimeIdentifier)'=='linux-x64'">
+    <DefineConstants>LINUX</DefineConstants>
+  </PropertyGroup>
+
+  <PropertyGroup Condition="'$(RuntimeIdentifier)'=='linux-arm64'">
+    <DefineConstants>LINUX</DefineConstants>
+  </PropertyGroup>
+
+  <PropertyGroup Condition="'$(RuntimeIdentifier)'=='osx-x64'">
+    <DefineConstants>MACOS</DefineConstants>
+  </PropertyGroup>
+
+  <PropertyGroup Condition="'$(RuntimeIdentifier)'=='osx-arm64'">
+    <DefineConstants>MACOS</DefineConstants>
+  </PropertyGroup>
+  
+</Project>

+ 6 - 0
samples/PixiEditorExtensionSamples.sln

@@ -17,6 +17,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample4_CreatePopup", "Samp
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample5_Resources", "Sample5_Resources\Sample5_Resources.csproj", "{51E1742D-132F-4CE9-9313-67FF1AC785D6}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample6_Palettes", "Sample6_Palettes\Sample6_Palettes.csproj", "{6FF1D3AB-358A-4D78-8877-DACC01D34B87}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -50,6 +52,10 @@ Global
 		{51E1742D-132F-4CE9-9313-67FF1AC785D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{51E1742D-132F-4CE9-9313-67FF1AC785D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{51E1742D-132F-4CE9-9313-67FF1AC785D6}.Release|Any CPU.Build.0 = Release|Any CPU
+		{6FF1D3AB-358A-4D78-8877-DACC01D34B87}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{6FF1D3AB-358A-4D78-8877-DACC01D34B87}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{6FF1D3AB-358A-4D78-8877-DACC01D34B87}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{6FF1D3AB-358A-4D78-8877-DACC01D34B87}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(NestedProjects) = preSolution
 		{FD9B4C32-4D2E-410E-BC6B-787779BEB6E2} = {7CC35BC4-829F-4EF4-8EB6-E1D46206E7DC}

+ 25 - 0
samples/Sample6_Palettes/ExamplePaletteDataSource.cs

@@ -0,0 +1,25 @@
+using System.Collections.Generic;
+using PixiEditor.Extensions.CommonApi.Async;
+using PixiEditor.Extensions.CommonApi.Palettes;
+using PixiEditor.Extensions.Wasm;
+
+namespace PalettesSample;
+
+public class ExamplePaletteDataSource : PaletteListDataSource
+{
+    public ExamplePaletteDataSource(string name) : base(name)
+    {
+    }
+
+    public override AsyncCall<List<IPalette>> FetchPaletteList(int startIndex, int items, FilteringSettings filtering)
+    {
+        return AsyncCall<List<IPalette>>.FromResult([
+            new ExtensionPalette("Example Palette", new List<PaletteColor>
+            {
+                new PaletteColor(255, 0, 0),
+                new PaletteColor(0, 255, 0),
+                new PaletteColor(0, 0, 255)
+            }, this)
+        ]);
+    }
+}

+ 27 - 0
samples/Sample6_Palettes/PalettesSampleExtension.cs

@@ -0,0 +1,27 @@
+using System.IO;
+using PixiEditor.Extensions.Wasm;
+
+namespace PalettesSample;
+
+public class PalettesSampleExtension : WasmExtension
+{
+    /// <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()
+    {
+        ExamplePaletteDataSource dataSource = new ExamplePaletteDataSource("Palettes Sample");
+        Api.Palettes.RegisterDataSource(dataSource);
+        
+        Api.Logger.Log("Palettes registered!");
+    }
+}

+ 14 - 0
samples/Sample6_Palettes/Program.cs

@@ -0,0 +1,14 @@
+namespace PalettesSample;
+
+public static class Program
+{
+    /// <summary>
+    ///     The entry point of the application. This will be executed when extension is loaded.
+    /// You can use this method, but there are special methods that are used for initialization. You won't be able to access PixiEditor Api at this point.
+    /// See <see cref="PalettesSampleExtension"/> for more information.
+    /// </summary>
+    public static void Main()
+    {
+
+    }
+}

+ 42 - 0
samples/Sample6_Palettes/Sample6_Palettes.csproj

@@ -0,0 +1,42 @@
+<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.AvaloniaUI.Desktop\bin\Debug\net8.0\win-x64\Extensions</PixiExtOutputPath>
+        <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
+        <RootNamespace>PalettesSample</RootNamespace>
+    </PropertyGroup>
+
+    <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-->
+    <ItemGroup>
+        <ProjectReference Include="..\..\src\PixiEditor.Extensions.Wasm\PixiEditor.Extensions.Wasm.csproj" />
+    </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.Wasm\build\PixiEditor.Extensions.Wasm.props"/>
+    <Import Project="..\..\src\PixiEditor.Extensions.Wasm\build\PixiEditor.Extensions.Wasm.targets" />
+
+</Project>

+ 27 - 0
samples/Sample6_Palettes/extension.json

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

+ 20 - 0
samples/stylecop.json

@@ -0,0 +1,20 @@
+{
+  "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
+  "settings": {
+    "indentation": {
+      "indentationSize": 4
+    },
+    "maintainabilityRules": {
+      "topLevelTypes": [ "class", "interface", "enum", "struct" ]
+    },
+    "readabilityRules": {
+      "allowBuiltInTypeAliases": false
+    },
+    "documentationRules": {
+      "xmlHeader": false,
+      "documentInterfaces": false,
+      "documentExposedElements": false,
+      "documentInternalElements": false
+    }
+  }
+}

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

@@ -100,7 +100,7 @@ internal static class ServiceCollectionHelpers
             .AddSingleton<IToolHandler, BrightnessToolViewModel>(x => (BrightnessToolViewModel)x.GetService<IBrightnessToolHandler>())
             .AddSingleton<IToolHandler, ZoomToolViewModel>()
             // Palette Parsers
-            .AddSingleton<PaletteProvider>()
+            .AddSingleton<IPalettesProvider, PaletteProvider>()
             .AddSingleton<PaletteFileParser, JascFileParser>()
             .AddSingleton<PaletteFileParser, ClsFileParser>()
             .AddSingleton<PaletteFileParser, DeluxePaintParser>()

+ 12 - 3
src/PixiEditor.AvaloniaUI/Models/ExtensionServices/PaletteProvider.cs

@@ -10,7 +10,7 @@ using PixiEditor.Platform;
 
 namespace PixiEditor.AvaloniaUI.Models.ExtensionServices;
 
-internal sealed class PaletteProvider
+internal sealed class PaletteProvider : IPalettesProvider
 {
     public ObservableCollection<PaletteFileParser> AvailableParsers { get; set; }
     public ObservableCollection<PaletteListDataSource> DataSources => dataSources;
@@ -33,8 +33,17 @@ internal sealed class PaletteProvider
         List<IPalette> allPalettes = new();
         foreach (PaletteListDataSource dataSource in dataSources)
         {
-            var palettes = await dataSource.FetchPaletteList(startIndex, items, filtering);
-            allPalettes.AddRange(palettes);
+            try
+            {
+                var palettes = await dataSource.FetchPaletteList(startIndex, items, filtering);
+                allPalettes.AddRange(palettes);
+            }
+            catch
+            {
+#if DEBUG
+                throw;
+#endif
+            }
         }
 
         return allPalettes;

+ 1 - 1
src/PixiEditor.AvaloniaUI/ViewModels/SubViewModels/ColorsViewModel.cs

@@ -369,7 +369,7 @@ internal class ColorsViewModel : SubViewModel<ViewModelMain>, IColorsHandler
 
     public void SetupPaletteProviders(IServiceProvider services)
     {
-        PaletteProvider = services.GetService<PaletteProvider>();
+        PaletteProvider = (PaletteProvider)services.GetService<IPalettesProvider>();
         PaletteProvider.AvailableParsers =
             new ObservableCollection<PaletteFileParser>(services.GetServices<PaletteFileParser>());
         var dataSources = services.GetServices<PaletteListDataSource>();

+ 2 - 2
src/PixiEditor.AvaloniaUI/Views/Palettes/PaletteItem.axaml

@@ -28,7 +28,7 @@
                     <input:EditableTextBlock x:Name="titleTextBlock" OnSubmit="EditableTextBlock_OnSubmit"
                                                     Text="{Binding Palette.Name, ElementName=paletteItem, Mode=TwoWay}"
                                                     FontSize="20" MaxChars="50"/>
-                <Button IsVisible="{Binding ElementName=paletteItem, Path=IsMouseOver}"
+                <Button IsVisible="{Binding ElementName=paletteItem, Path=IsPointerOver}"
                         Click="RenameButton_Click"
                         Cursor="Hand" Width="20" Height="20">
                     <Image Source="/Images/Edit.png"/>
@@ -38,7 +38,7 @@
                        Source="/Images/SupperterPack.png" Width="24"
                        DockPanel.Dock="Right" HorizontalAlignment="Right"/>-->
                 <decorators:Chip Margin="0 5 5 0"
-                                   ui:Translator.Key="{Binding ElementName=paletteItem, Path=Palette.Source.Name.Key}"
+                                   ui:Translator.Key="{Binding ElementName=paletteItem, Path=Palette.Source.Name}"
                                    DockPanel.Dock="Right" HorizontalAlignment="Right"/>
             </DockPanel>
             <TextBlock Margin="0 5 0 0" />

+ 1 - 0
src/PixiEditor.AvaloniaUI/Views/Windows/PalettesBrowser.axaml.cs

@@ -423,6 +423,7 @@ internal partial class PalettesBrowser : PixiEditorPopup, IPopupWindow
 
     public async Task UpdatePaletteList()
     {
+        if (IsFetching) return;
         IsFetching = true;
         _lastScrolledOffset = -1;
         PaletteList?.Palettes?.Clear();

+ 90 - 8
src/PixiEditor.Extensions.CommonApi/Async/AsyncCall.cs

@@ -9,6 +9,8 @@ public delegate void AsyncCallFailed(Exception exception);
 [AsyncMethodBuilder(typeof(AsyncCallAsyncMethodBuilder))]
 public class AsyncCall
 {
+    private List<AsyncCallCompleted> completedCalls = new List<AsyncCallCompleted>();
+    private List<AsyncCallFailed> failedCalls = new List<AsyncCallFailed>();
     private object? _result;
     protected Action continuation;
     public AsyncCallState State { get; protected set; } = AsyncCallState.Pending;
@@ -27,8 +29,42 @@ public class AsyncCall
         }
     }
     
-    public event AsyncCallCompleted Completed;
-    public event AsyncCallFailed Failed;
+    public event AsyncCallCompleted Completed
+    {
+        add
+        {
+            if (State == AsyncCallState.Completed)
+            {
+                value();
+            }
+            else
+            {
+                completedCalls.Add(value);
+            }
+        }
+        remove
+        {
+            completedCalls.Remove(value);
+        }
+    }
+    public event AsyncCallFailed Failed
+    {
+        add
+        {
+            if (State == AsyncCallState.Failed)
+            {
+                value(Exception);
+            }
+            else
+            {
+                failedCalls.Add(value);
+            }
+        }
+        remove
+        {
+            failedCalls.Remove(value);
+        }
+    }
     
     public void SetException(Exception exception)
     {
@@ -40,9 +76,10 @@ public class AsyncCall
         State = AsyncCallState.Failed;
         Exception = exception;
         this.continuation?.Invoke();
-        Failed?.Invoke(exception);
+
+        InvokeFailed(exception);
     }
-    
+
     public void SetResult(object? result)
     {
         if (State != AsyncCallState.Pending)
@@ -53,7 +90,7 @@ public class AsyncCall
         State = AsyncCallState.Completed;
         Result = result;
         this.continuation?.Invoke();
-        Completed?.Invoke();
+        InvokeCompleted();
     }
     
     public AsyncCallAwaiter GetAwaiter()
@@ -65,6 +102,22 @@ public class AsyncCall
     {
         return result;
     }
+
+    private void InvokeCompleted()
+    {
+        foreach (var completedCall in completedCalls)
+        {
+            completedCall();
+        }
+    }
+    
+    private void InvokeFailed(Exception exception)
+    {
+        foreach (var failedCall in failedCalls)
+        {
+            failedCall(exception);
+        }
+    }
     
     internal void RegisterContinuation(Action cont)
     {
@@ -94,17 +147,38 @@ public class AsyncCall
 [AsyncMethodBuilder(typeof(AsyncCallAsyncMethodBuilder<>))]
 public class AsyncCall<TResult> : AsyncCall
 {
+    private List<AsyncCallCompleted<TResult>> completedCalls = new List<AsyncCallCompleted<TResult>>();
     public new TResult Result
     {
         get => (TResult) base.Result;
         protected set => base.Result = value;
     }
     
-    public new event AsyncCallCompleted<TResult> Completed;
+    public new event AsyncCallCompleted<TResult> Completed
+    {
+        add
+        {
+            if (State == AsyncCallState.Completed)
+            {
+                value(Result);
+            }
+            else
+            {
+                completedCalls.Add(value);
+            }
+        }
+        remove
+        {
+            completedCalls.Remove(value);
+        }
+    }
     
     public AsyncCall()
     {
-        base.Completed += () => Completed?.Invoke(Result);
+        base.Completed += () =>
+        {
+            InvokeCompleted(Result);
+        };
     }
     
     public AsyncCallAwaiter<TResult> GetAwaiter()
@@ -130,7 +204,7 @@ public class AsyncCall<TResult> : AsyncCall
         State = AsyncCallState.Completed;
         Result = result;
         continuation?.Invoke();
-        Completed?.Invoke(result);
+        InvokeCompleted(result);
     }
     
     public AsyncCall<T> ContinueWith<T>(Func<AsyncCall<TResult>, T> action)
@@ -176,6 +250,14 @@ public class AsyncCall<TResult> : AsyncCall
         });
         return asyncCall;
     }
+    
+    private void InvokeCompleted(TResult result)
+    {
+        foreach (var completedCall in completedCalls)
+        {
+            completedCall(result);
+        }
+    }
 }
 
 public enum AsyncCallState

+ 7 - 0
src/PixiEditor.Extensions.CommonApi/IByteSerializable.cs

@@ -0,0 +1,7 @@
+namespace PixiEditor.Extensions.CommonApi;
+
+public interface IByteSerializable
+{
+    byte[] Serialize();
+    void Deserialize(byte[] data);
+}

+ 18 - 2
src/PixiEditor.Extensions.CommonApi/Palettes/ExtensionPalette.cs

@@ -1,12 +1,24 @@
-namespace PixiEditor.Extensions.CommonApi.Palettes;
+using ProtoBuf;
 
+namespace PixiEditor.Extensions.CommonApi.Palettes;
+
+[ProtoContract]
 public class ExtensionPalette : IPalette
 {
+    [ProtoMember(1)]
     public string Name { get; }
+    
+    [ProtoMember(2)]
     public List<PaletteColor> Colors { get; }
+    
+    [ProtoMember(3)]
     public bool IsFavourite { get; set; }
+    
+    [ProtoMember(4)]
     public string FileName { get; set; }
-    public PaletteListDataSource Source { get; }
+    
+    [ProtoIgnore]
+    public PaletteListDataSource Source { get; set; }
 
     public ExtensionPalette(string name, List<PaletteColor> colors, PaletteListDataSource source)
     {
@@ -14,4 +26,8 @@ public class ExtensionPalette : IPalette
         Colors = colors;
         Source = source;
     }
+    
+    public ExtensionPalette()
+    {
+    }
 }

+ 36 - 0
src/PixiEditor.Extensions.CommonApi/Palettes/FetchPaletteListQuery.cs

@@ -0,0 +1,36 @@
+using ProtoBuf;
+
+namespace PixiEditor.Extensions.CommonApi.Palettes;
+
+[ProtoContract]
+public class FetchPaletteListQuery
+{
+    [ProtoMember(1)] 
+    public string DataSourceName { get; set; }
+
+    [ProtoMember(2)] 
+    public int StartIndex { get; set; }
+
+    [ProtoMember(3)] 
+    public int Items { get; set; }
+
+    [ProtoMember(4)] 
+    public FilteringSettings Filtering { get; set; }
+
+    public FetchPaletteListQuery()
+    {
+    }
+    
+    public FetchPaletteListQuery(string dataSourceName, int startIndex, int items, FilteringSettings filtering)
+    {
+        DataSourceName = dataSourceName;
+        StartIndex = startIndex;
+        Items = items;
+        Filtering = filtering;
+    }
+    
+    public override string ToString()
+    {
+        return $"DataSourceName: {DataSourceName}, StartIndex: {StartIndex}, Items: {Items}, Filtering: {Filtering}";
+    }
+}

+ 17 - 1
src/PixiEditor.Extensions.CommonApi/Palettes/FilteringSettings.cs

@@ -1,12 +1,28 @@
-namespace PixiEditor.Extensions.CommonApi.Palettes;
+using ProtoBuf;
 
+namespace PixiEditor.Extensions.CommonApi.Palettes;
+
+[ProtoContract]
 public sealed class FilteringSettings
 {
+    [ProtoMember(1)]
     public ColorsNumberMode ColorsNumberMode { get; set; }
+    
+    [ProtoMember(2)]
     public int ColorsCount { get; set; }
+    
+    [ProtoMember(3)]
     public string Name { get; set; }
+    
+    [ProtoMember(4)]
     public bool ShowOnlyFavourites { get; set; }
+    
+    [ProtoMember(5)]
     public List<string> Favourites { get; set; }
+    
+    public FilteringSettings()
+    {
+    }
 
     public FilteringSettings(ColorsNumberMode colorsNumberMode, int colorsCount, string name, bool showOnlyFavourites, List<string> favourites)
     {

+ 10 - 0
src/PixiEditor.Extensions.CommonApi/Palettes/IPalettesProvider.cs

@@ -0,0 +1,10 @@
+namespace PixiEditor.Extensions.CommonApi.Palettes;
+
+public interface IPalettesProvider
+{
+    /// <summary>
+    ///     Registers a data source of palettes.
+    /// </summary>
+    /// <param name="dataSource">Palettes data source</param>
+    public void RegisterDataSource(PaletteListDataSource dataSource);
+}

+ 10 - 1
src/PixiEditor.Extensions.CommonApi/Palettes/PaletteColor.cs

@@ -1,12 +1,21 @@
-namespace PixiEditor.Extensions.CommonApi.Palettes;
+using ProtoBuf;
 
+namespace PixiEditor.Extensions.CommonApi.Palettes;
+
+[ProtoContract]
 public struct PaletteColor
 {
     public static PaletteColor Empty => new(0, 0, 0);
     public static PaletteColor Black => new(0, 0, 0);
     public static PaletteColor White => new(255, 255, 255);
+    
+    [ProtoMember(1)]
     public byte R { get; set; }
+    
+    [ProtoMember(2)]
     public byte G { get; set; }
+    
+    [ProtoMember(3)]
     public byte B { get; set; }
 
     public string Hex => $"#{R:X2}{G:X2}{B:X2}";

+ 20 - 0
src/PixiEditor.Extensions.CommonApi/Palettes/PaletteListResult.cs

@@ -0,0 +1,20 @@
+using System.Runtime.Serialization;
+using ProtoBuf;
+
+namespace PixiEditor.Extensions.CommonApi.Palettes;
+
+[ProtoContract]
+public class PaletteListResult
+{
+    [ProtoMember(1)]
+    public ExtensionPalette[] Palettes { get; set; }
+
+    public PaletteListResult(ExtensionPalette[] palettes)
+    {
+        Palettes = palettes;
+    }
+    
+    public PaletteListResult()
+    {
+    }
+}

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

@@ -10,4 +10,8 @@
       <Folder Include="Preferences\" />
     </ItemGroup>
 
+    <ItemGroup>
+      <PackageReference Include="protobuf-net" Version="3.2.30" />
+    </ItemGroup>
+
 </Project>

+ 12 - 0
src/PixiEditor.Extensions.Wasm/Api/Palettes/PalettesProvider.cs

@@ -0,0 +1,12 @@
+using PixiEditor.Extensions.CommonApi.Palettes;
+using PixiEditor.Extensions.Wasm.Bridge;
+
+namespace PixiEditor.Extensions.Wasm.Api.Palettes;
+
+public class PalettesProvider : IPalettesProvider
+{
+    public void RegisterDataSource(PaletteListDataSource dataSource)
+    {
+        Interop.RegisterDataSource(dataSource);
+    }
+}

+ 55 - 0
src/PixiEditor.Extensions.Wasm/Bridge/Interop.Palettes.cs

@@ -0,0 +1,55 @@
+using PixiEditor.Extensions.CommonApi.Palettes;
+using PixiEditor.Extensions.Wasm.Utilities;
+using ProtoBuf;
+
+namespace PixiEditor.Extensions.Wasm.Bridge;
+
+internal static partial class Interop
+{
+    private static Dictionary<int, PaletteListDataSource> dataSources = new();
+    public static void RegisterDataSource(PaletteListDataSource dataSource)
+    {
+        if (dataSources.ContainsValue(dataSource))
+        {
+            throw new InvalidOperationException("Data source is already registered.");
+        }
+
+        int handle = Native.register_palette_data_source(dataSource.Name);
+        dataSources[handle] = dataSource;
+    }
+
+    public static void FetchPaletteList(FetchPaletteListQuery query, int asyncHandle)
+    {
+        foreach (var source in dataSources)
+        {
+            if(source.Value.Name == query.DataSourceName)
+            {
+                source.Value.FetchPaletteList(query.StartIndex, query.Items, query.Filtering).Completed += (result) =>
+                {
+                    List<ExtensionPalette> palettes = new List<ExtensionPalette>();
+                    foreach (var palette in result)
+                    {
+                        if (palette is ExtensionPalette extensionPalette)
+                        {
+                            palettes.Add(extensionPalette);
+                        }
+                        else
+                        {
+                            palettes.Add(new ExtensionPalette(palette.Name, palette.Colors, source.Value));
+                        }
+                    }
+                    SetFetchPaletteResult(asyncHandle, new PaletteListResult(palettes.ToArray()));
+                };
+                return;
+            }
+        }
+    }
+    
+    public static void SetFetchPaletteResult(int asyncHandle, PaletteListResult result)
+    {
+        using MemoryStream stream = new();
+        Serializer.Serialize(stream, result);
+        byte[] bytes = stream.ToArray();
+        Native.set_fetch_palette_result(asyncHandle, InteropUtility.ByteArrayToIntPtr(bytes), bytes.Length);
+    }
+}

+ 4 - 2
src/PixiEditor.Extensions.Wasm/Bridge/Interop.cs

@@ -1,6 +1,8 @@
-namespace PixiEditor.Extensions.Wasm.Bridge;
+using PixiEditor.Extensions.CommonApi.Palettes;
 
-internal static class Interop
+namespace PixiEditor.Extensions.Wasm.Bridge;
+
+internal static partial class Interop
 {
     public static void UpdateUserPreference<T>(string name, T value)
     {

+ 25 - 0
src/PixiEditor.Extensions.Wasm/Bridge/Native.Palettes.cs

@@ -0,0 +1,25 @@
+using System.Runtime.CompilerServices;
+using PixiEditor.Extensions.CommonApi.Palettes;
+using PixiEditor.Extensions.Wasm.Utilities;
+using ProtoBuf;
+
+namespace PixiEditor.Extensions.Wasm.Bridge;
+
+internal static partial class Native
+{
+    [MethodImpl(MethodImplOptions.InternalCall)]
+    public static extern int register_palette_data_source(string dataSourceName);
+
+    [MethodImpl(MethodImplOptions.InternalCall)]
+    public static extern void set_fetch_palette_result(int asyncHandle, IntPtr ptr, int length);
+
+    [ApiExport("fetch_palette_list")]
+    public static void FetchPaletteList(IntPtr queryPtr, int length, int asyncHandle)
+    {
+        byte[] bytes = InteropUtility.IntPtrToByteArray(queryPtr, length);
+        using MemoryStream stream = new(bytes);
+        FetchPaletteListQuery query = Serializer.Deserialize<FetchPaletteListQuery>(stream);
+        Interop.FetchPaletteList(query, asyncHandle);
+    }
+}
+

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

@@ -25,10 +25,6 @@
     <PackageReference Include="Microsoft.Build.Utilities.Core" Version="17.8.3"/>
   </ItemGroup>
 
-  <ItemGroup>
-    <Folder Include="Api\Palettes\" />
-  </ItemGroup>
-
   <Target Name="PackTaskDependencies" BeforeTargets="GenerateNuspec">
     <ItemGroup>
       <_PackageFiles Include="build\**" BuildAction="Content" PackagePath="build"/>

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

@@ -1,6 +1,7 @@
 using PixiEditor.Extensions.CommonApi.Windowing;
 using PixiEditor.Extensions.Wasm.Api;
 using PixiEditor.Extensions.Wasm.Api.Logging;
+using PixiEditor.Extensions.Wasm.Api.Palettes;
 using PixiEditor.Extensions.Wasm.Api.UserPreferences;
 using PixiEditor.Extensions.Wasm.Api.Window;
 
@@ -11,11 +12,13 @@ public class PixiEditorApi
     public Logger Logger { get; }
     public WindowProvider WindowProvider { get; }
     public Preferences Preferences { get; }
+    public PalettesProvider Palettes { get; }
 
     public PixiEditorApi()
     {
         Logger = new Logger();
         WindowProvider = new WindowProvider();
         Preferences = new Preferences();
+        Palettes = new PalettesProvider();
     }
 }

+ 7 - 0
src/PixiEditor.Extensions.Wasm/Utilities/InteropUtility.cs

@@ -10,4 +10,11 @@ public static class InteropUtility
         Marshal.Copy(array, 0, ptr, array.Length);
         return ptr;
     }
+
+    public static byte[] IntPtrToByteArray(IntPtr ptr, int length)
+    {
+        byte[] array = new byte[length];
+        Marshal.Copy(ptr, array, 0, length);
+        return array;
+    }
 }

+ 1 - 1
src/PixiEditor.Extensions.WasmRuntime/Api/ApiGroupHandler.cs

@@ -16,5 +16,5 @@ internal class ApiGroupHandler
     protected Instance? Instance { get; }
     protected WasmMemoryUtility WasmMemoryUtility { get; }
     protected ExtensionMetadata Metadata { get; }
-    protected Extension Extension { get; }
+    protected WasmExtensionInstance Extension { get; }
 }

+ 39 - 0
src/PixiEditor.Extensions.WasmRuntime/Api/Palettes/ExtensionPalettesDataSource.cs

@@ -0,0 +1,39 @@
+using PixiEditor.Extensions.CommonApi.Async;
+using PixiEditor.Extensions.CommonApi.Palettes;
+using ProtoBuf;
+
+namespace PixiEditor.Extensions.WasmRuntime.Api.Palettes;
+
+public class ExtensionPalettesDataSource : PaletteListDataSource
+{
+    private WasmExtensionInstance extension;
+    
+    public ExtensionPalettesDataSource(string name, WasmExtensionInstance instance) : base(name)
+    {
+        extension = instance;
+    }
+
+    public override async AsyncCall<List<IPalette>> FetchPaletteList(int startIndex, int items, FilteringSettings filtering)
+    {
+        PaletteListResult result = await extension.AsyncHandleManager.InvokeAsync<PaletteListResult>(
+            (int asyncHandle) =>
+            {
+                var action = extension.Instance.GetAction<int, int, int>("fetch_palette_list");
+                
+                using MemoryStream stream = new();
+                Serializer.Serialize(stream, new FetchPaletteListQuery(Name, startIndex, items, filtering));
+                byte[] bytes = stream.ToArray();
+                int ptr = extension.WasmMemoryUtility.WriteBytes(bytes);
+                
+                action.Invoke(ptr, bytes.Length, asyncHandle);
+                extension.WasmMemoryUtility.Free(ptr);
+            });
+
+        foreach (var palette in result.Palettes)
+        {
+            palette.Source = this;
+        }
+        
+        return await AsyncCall<List<IPalette>>.FromResult(result.Palettes.Cast<IPalette>().ToList());
+    }
+}

+ 29 - 0
src/PixiEditor.Extensions.WasmRuntime/Api/Palettes/PalettesApi.cs

@@ -0,0 +1,29 @@
+using CommunityToolkit.HighPerformance;
+using PixiEditor.Extensions.CommonApi.Palettes;
+using PixiEditor.Extensions.WasmRuntime.Api.Palettes;
+using ProtoBuf;
+
+namespace PixiEditor.Extensions.WasmRuntime.Api;
+
+internal class PalettesApi : ApiGroupHandler
+{
+    [ApiFunction("register_palette_data_source")]
+    public int RegisterPaletteDataSource(string name)
+    {
+        ExtensionPalettesDataSource dataSource = new(name, Extension);
+        Api.Palettes.RegisterDataSource(dataSource);
+        return NativeObjectManager.AddObject(dataSource);
+    }
+    
+    [ApiFunction("set_fetch_palette_result")]
+    public void SetFetchPaletteResult(int asyncHandle, Span<byte> paletteBytes)
+    {
+        using MemoryStream stream = new();
+        
+        stream.Write(paletteBytes);
+        stream.Seek(0, SeekOrigin.Begin);
+        
+        PaletteListResult listResult = Serializer.Deserialize<PaletteListResult>(stream);
+        AsyncHandleManager.SetAsyncCallResult(asyncHandle, listResult);
+    }
+}

+ 17 - 0
src/PixiEditor.Extensions.WasmRuntime/Management/AsyncManager.cs

@@ -37,6 +37,23 @@ internal class AsyncCallsManager
         asyncCalls.Remove(asyncHandle);
     }
     
+    public async AsyncCall<T> InvokeAsync<T>(Action<int> invokeAction)
+    {
+        int asyncHandle = GetNextAsyncHandle();
+        AsyncCall<T> task = new();
+        asyncCalls[asyncHandle] = task;
+        invokeAction(asyncHandle);
+        return await task;
+    }
+    
+    public void SetAsyncCallResult<T>(int asyncHandle, T result)
+    {
+        if (asyncCalls.TryGetValue(asyncHandle, out AsyncCall asyncCall))
+        {
+            asyncCall.SetResult(result);
+        }
+    }
+
     private int GetNextAsyncHandle()
     {
         int asyncHandle = 0;

+ 1 - 0
src/PixiEditor.Extensions.WasmRuntime/PixiEditor.Extensions.WasmRuntime.csproj

@@ -8,6 +8,7 @@
     </PropertyGroup>
 
     <ItemGroup>
+      <PackageReference Include="CommunityToolkit.HighPerformance" Version="8.2.2" />
       <PackageReference Include="Wasmtime" Version="16.0.0" />
     </ItemGroup>
 

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

@@ -2,6 +2,7 @@ using System.Runtime.InteropServices;
 using System.Text;
 using Avalonia.Controls;
 using Avalonia.Threading;
+using PixiEditor.Extensions.CommonApi.Palettes;
 using PixiEditor.Extensions.FlyUI;
 using PixiEditor.Extensions.FlyUI.Elements;
 using PixiEditor.Extensions.WasmRuntime.Management;
@@ -13,6 +14,7 @@ namespace PixiEditor.Extensions.WasmRuntime;
 public partial class WasmExtensionInstance : Extension
 {
     public Instance? Instance { get; private set; }
+    internal WasmMemoryUtility WasmMemoryUtility { get; set; }
 
     private Linker Linker { get; }
     private Store Store { get; }
@@ -20,11 +22,10 @@ public partial class WasmExtensionInstance : Extension
 
     private Memory memory = null!;
     private LayoutBuilder LayoutBuilder { get; set; }
-    private ObjectManager NativeObjectManager { get; set; }
-    private AsyncCallsManager AsyncHandleManager { get; set; }
-    private WasmMemoryUtility WasmMemoryUtility { get; set; }
+    internal ObjectManager NativeObjectManager { get; set; }
+    internal AsyncCallsManager AsyncHandleManager { get; set; }
 
-    private Extension Extension => this; // api group handler needs this property
+    private WasmExtensionInstance Extension => this; // api group handler needs this property
 
     private string modulePath;
     

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

@@ -40,6 +40,14 @@ public class WasmMemoryUtility
     {
         return memory.ReadInt32(offset);
     }
+    
+    public int Write<T>(T arg) where T : unmanaged
+    {
+        var length = Marshal.SizeOf<T>();
+        var ptr = malloc.Invoke(length);
+        memory.Write(ptr, arg);
+        return ptr;
+    }
 
     public int WriteSpan(Span<byte> span)
     {

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

@@ -12,6 +12,8 @@ public class ExtensionServices
     public IWindowProvider? Windowing => Services.GetService<IWindowProvider>();
     public IFileSystemProvider? FileSystem => Services.GetService<IFileSystemProvider>();
     public IPreferences? Preferences => Services.GetService<IPreferences>();
+    
+    public IPalettesProvider? Palettes => Services.GetService<IPalettesProvider>();
 
     public ExtensionServices(IServiceProvider services)
     {