From 6d05bbbc35b45546b66f10992e79cf19fa571357 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 23 Sep 2022 11:48:15 +0100 Subject: [PATCH 001/169] add initial work towards blazorless wasm --- Avalonia.sln | 18 +- global.json | 4 - .../Avalonia.Web.Sample.csproj | 24 + src/Web/Avalonia.Web.Sample/Program.cs | 27 + src/Web/Avalonia.Web.Sample/index.html | 20 + src/Web/Avalonia.Web.Sample/main.js | 24 + .../runtimeconfig.template.json | 11 + src/Web/Avalonia.Web/Avalonia.Web.csproj | 41 ++ src/Web/Avalonia.Web/Avalonia.Web.targets | 6 + src/Web/Avalonia.Web/AvaloniaRuntime.cs | 51 ++ src/Web/Avalonia.Web/webapp/build.js | 16 + .../Avalonia.Web/webapp/modules/avalonia.ts | 7 + .../webapp/modules/avalonia/canvas.ts | 119 ++++ .../webapp/modules/avalonia/runtime.ts | 23 + .../Avalonia.Web/webapp/modules/storage.ts | 5 + src/Web/Avalonia.Web/webapp/package-lock.json | 634 ++++++++++++++++++ src/Web/Avalonia.Web/webapp/package.json | 13 + src/Web/Avalonia.Web/webapp/tsconfig.json | 19 + src/Web/Avalonia.Web/webapp/types/dotnet.d.ts | 249 +++++++ src/Web/Avalonia.Web/wwwroot/avalonia.js | 122 ++++ src/Web/Avalonia.Web/wwwroot/storage.js | 10 + 21 files changed, 1437 insertions(+), 6 deletions(-) create mode 100644 src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj create mode 100644 src/Web/Avalonia.Web.Sample/Program.cs create mode 100644 src/Web/Avalonia.Web.Sample/index.html create mode 100644 src/Web/Avalonia.Web.Sample/main.js create mode 100644 src/Web/Avalonia.Web.Sample/runtimeconfig.template.json create mode 100644 src/Web/Avalonia.Web/Avalonia.Web.csproj create mode 100644 src/Web/Avalonia.Web/Avalonia.Web.targets create mode 100644 src/Web/Avalonia.Web/AvaloniaRuntime.cs create mode 100644 src/Web/Avalonia.Web/webapp/build.js create mode 100644 src/Web/Avalonia.Web/webapp/modules/avalonia.ts create mode 100644 src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts create mode 100644 src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts create mode 100644 src/Web/Avalonia.Web/webapp/modules/storage.ts create mode 100644 src/Web/Avalonia.Web/webapp/package-lock.json create mode 100644 src/Web/Avalonia.Web/webapp/package.json create mode 100644 src/Web/Avalonia.Web/webapp/tsconfig.json create mode 100644 src/Web/Avalonia.Web/webapp/types/dotnet.d.ts create mode 100644 src/Web/Avalonia.Web/wwwroot/avalonia.js create mode 100644 src/Web/Avalonia.Web/wwwroot/storage.js diff --git a/Avalonia.sln b/Avalonia.sln index 35b6b2108a..228052c2d5 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -212,7 +212,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ColorPick EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.DesignerSupport.Tests", "tests\Avalonia.DesignerSupport.Tests\Avalonia.DesignerSupport.Tests.csproj", "{EABE2161-989B-42BF-BD8D-1E34B20C21F1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DevGenerators", "src\tools\DevGenerators\DevGenerators.csproj", "{1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DevGenerators", "src\tools\DevGenerators\DevGenerators.csproj", "{1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Web", "src\Web\Avalonia.Web\Avalonia.Web.csproj", "{76D39FF6-6B4F-46C4-93CD-E6FC4665739E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Web.Sample", "src\Web\Avalonia.Web.Sample\Avalonia.Web.Sample.csproj", "{1F61B6F1-B881-4E27-A5B0-09D1F543F7AC}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -502,6 +506,14 @@ Global {1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}.Debug|Any CPU.Build.0 = Debug|Any CPU {1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}.Release|Any CPU.ActiveCfg = Release|Any CPU {1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB}.Release|Any CPU.Build.0 = Release|Any CPU + {76D39FF6-6B4F-46C4-93CD-E6FC4665739E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {76D39FF6-6B4F-46C4-93CD-E6FC4665739E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {76D39FF6-6B4F-46C4-93CD-E6FC4665739E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {76D39FF6-6B4F-46C4-93CD-E6FC4665739E}.Release|Any CPU.Build.0 = Release|Any CPU + {1F61B6F1-B881-4E27-A5B0-09D1F543F7AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1F61B6F1-B881-4E27-A5B0-09D1F543F7AC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1F61B6F1-B881-4E27-A5B0-09D1F543F7AC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1F61B6F1-B881-4E27-A5B0-09D1F543F7AC}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -545,6 +557,7 @@ Global {41B02319-965D-4945-8005-C1A3D1224165} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B} {D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B} = {9B9E3891-2366-4253-A952-D08BCEB71098} {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} + {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B} {351337F5-D66F-461B-A957-4EF60BDB4BA6} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {909A8CBD-7D0E-42FD-B841-022AD8925820} = {8B6A8209-894F-4BA1-B880-965FD453982C} {11BE52AF-E2DD-4CF0-B19A-05285ACAF571} = {9B9E3891-2366-4253-A952-D08BCEB71098} @@ -559,7 +572,8 @@ Global {2B390431-288C-435C-BB6B-A374033BD8D1} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} {EABE2161-989B-42BF-BD8D-1E34B20C21F1} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {1BBFAD42-B99E-47E0-B00A-A4BC6B6BB4BB} = {4ED8B739-6F4E-4CD4-B993-545E6B5CE637} - {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B} + {76D39FF6-6B4F-46C4-93CD-E6FC4665739E} = {86A3F706-DC3C-43C6-BE1B-B98F5BAAA268} + {1F61B6F1-B881-4E27-A5B0-09D1F543F7AC} = {86A3F706-DC3C-43C6-BE1B-B98F5BAAA268} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/global.json b/global.json index a6792b05c7..44d4e10dbf 100644 --- a/global.json +++ b/global.json @@ -1,8 +1,4 @@ { - "sdk": { - "version": "6.0.202", - "rollForward": "latestFeature" - }, "msbuild-sdks": { "Microsoft.Build.Traversal": "1.0.43", "MSBuild.Sdk.Extras": "3.0.22", diff --git a/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj b/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj new file mode 100644 index 0000000000..8f5912a16f --- /dev/null +++ b/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj @@ -0,0 +1,24 @@ + + + net7.0 + browser-wasm + main.js + Exe + true + true + + + + + + + + + + + + + + + + diff --git a/src/Web/Avalonia.Web.Sample/Program.cs b/src/Web/Avalonia.Web.Sample/Program.cs new file mode 100644 index 0000000000..b880d61ab5 --- /dev/null +++ b/src/Web/Avalonia.Web.Sample/Program.cs @@ -0,0 +1,27 @@ +using System; +using System.Runtime.InteropServices.JavaScript; +using System.Threading.Tasks; +using Avalonia.Web; +//using SkiaSharp; + +internal class Program +{ + private static void Main(string[] args) + { + Console.WriteLine("Hello, Browser!"); + + AvaloniaRuntime.Init(); + } +} + +public partial class MyClass +{ + [JSExport] + internal static async Task TestDynamicModule() + { + await JSHost.ImportAsync("storage.ts", "./storage.js"); + var fileApiSupported = AvaloniaRuntime.IsFileApiSupported(); + + Console.WriteLine("DynamicModule result: " + fileApiSupported); + } +} diff --git a/src/Web/Avalonia.Web.Sample/index.html b/src/Web/Avalonia.Web.Sample/index.html new file mode 100644 index 0000000000..25b92798ad --- /dev/null +++ b/src/Web/Avalonia.Web.Sample/index.html @@ -0,0 +1,20 @@ + + + + + + + Avalonia.Web.Sample + + + + + + + + +
+ + + + diff --git a/src/Web/Avalonia.Web.Sample/main.js b/src/Web/Avalonia.Web.Sample/main.js new file mode 100644 index 0000000000..9973b469c7 --- /dev/null +++ b/src/Web/Avalonia.Web.Sample/main.js @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { dotnet } from './dotnet.js' +import { createAvaloniaRuntime } from './avalonia.js' + +const is_browser = typeof window != "undefined"; +if (!is_browser) throw new Error(`Expected to be running in a browser`); + +const dotnetRuntime = await dotnet + .withDiagnosticTracing(false) + .withApplicationArgumentsFromQuery() + .create(); + +const avaloniaRuntime = await createAvaloniaRuntime(dotnetRuntime); + +const outDiv = document.getElementById("out"); +avaloniaRuntime.createAvaloniaView(outDiv); + +const config = dotnetRuntime.getConfig(); +const exports = await dotnetRuntime.getAssemblyExports(config.mainAssemblyName); +await exports.MyClass.TestDynamicModule(); + +await dotnetRuntime.runMainAndExit(config.mainAssemblyName, ["dotnet", "is", "great!"]); \ No newline at end of file diff --git a/src/Web/Avalonia.Web.Sample/runtimeconfig.template.json b/src/Web/Avalonia.Web.Sample/runtimeconfig.template.json new file mode 100644 index 0000000000..8f0557352c --- /dev/null +++ b/src/Web/Avalonia.Web.Sample/runtimeconfig.template.json @@ -0,0 +1,11 @@ +{ + "wasmHostProperties": { + "perHostConfig": [ + { + "name": "browser", + "html-path": "index.html", + "Host": "browser" + } + ] + } +} diff --git a/src/Web/Avalonia.Web/Avalonia.Web.csproj b/src/Web/Avalonia.Web/Avalonia.Web.csproj new file mode 100644 index 0000000000..51b1d70173 --- /dev/null +++ b/src/Web/Avalonia.Web/Avalonia.Web.csproj @@ -0,0 +1,41 @@ + + + net7.0 + enable + true + + + + + + + + + + + + + + true + build\ + + + true + build\;buildTransitive\ + + + + + + + + + + + + + + + + + diff --git a/src/Web/Avalonia.Web/Avalonia.Web.targets b/src/Web/Avalonia.Web/Avalonia.Web.targets new file mode 100644 index 0000000000..97c8c29ee4 --- /dev/null +++ b/src/Web/Avalonia.Web/Avalonia.Web.targets @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/Web/Avalonia.Web/AvaloniaRuntime.cs b/src/Web/Avalonia.Web/AvaloniaRuntime.cs new file mode 100644 index 0000000000..c8ff75a77f --- /dev/null +++ b/src/Web/Avalonia.Web/AvaloniaRuntime.cs @@ -0,0 +1,51 @@ +using System; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.JavaScript; +using static System.Runtime.InteropServices.JavaScript.JSType; + +namespace Avalonia.Web; + +public partial class AvaloniaRuntime +{ + public record GLInfo(int ContextId, uint FboId, int Stencils, int Samples, int Depth); + + [JSExport] + internal static void StartAvaloniaView(JSObject canvas) + { + Init(); + // setup, get gl context... + var info = InitGL(canvas, "testCanvas"); + + + /* var glInfo = new GLInfo( + info.GetPropertyAsInt32("context"), + (uint)info.GetPropertyAsInt32("fboId"), + info.GetPropertyAsInt32("stencil"), + info.GetPropertyAsInt32("sample"), + info.GetPropertyAsInt32("depth")); + + Console.WriteLine($"{glInfo.ContextId}, {glInfo.FboId}");*/ + } + + public static void Init () + { + if (false) + { + SkiaSharp.GRGlInterface.Create(); + } + } + + [JSImport("Canvas.Foo", "avalonia.ts")] + internal static partial void Foo(JSObject canvas); + + [JSImport("Canvas.initGL", "avalonia.ts")] + internal static partial JSObject InitGL(JSObject canvas, string canvasId); + + [JSImport("StorageProvider.isFileApiSupported", "storage.ts")] + public static partial bool IsFileApiSupported(); + + //[DllImport("__Internal")] + //public + +} + diff --git a/src/Web/Avalonia.Web/webapp/build.js b/src/Web/Avalonia.Web/webapp/build.js new file mode 100644 index 0000000000..32f79b0709 --- /dev/null +++ b/src/Web/Avalonia.Web/webapp/build.js @@ -0,0 +1,16 @@ +require("esbuild").build({ + entryPoints: [ + "./modules/avalonia.ts", + "./modules/storage.ts" + ], + outdir: "../wwwroot", + bundle: true, + minify: false, + format: "esm", + target: "es2016", + platform: "browser", + sourcemap: "linked", + loader: {".ts": "ts"} + }) + .then(() => console.log("⚡ Done")) + .catch(() => process.exit(1)); \ No newline at end of file diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia.ts new file mode 100644 index 0000000000..bf9ef48772 --- /dev/null +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia.ts @@ -0,0 +1,7 @@ +import { RuntimeAPI } from "../types/dotnet"; +import { AvaloniaRuntime } from "./avalonia/runtime"; + +export async function createAvaloniaRuntime(api: RuntimeAPI): Promise { + const dotnetAssembly = await api.getAssemblyExports("Avalonia.Web.dll"); + return new AvaloniaRuntime(dotnetAssembly, api); +} \ No newline at end of file diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts new file mode 100644 index 0000000000..c8acf5e428 --- /dev/null +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts @@ -0,0 +1,119 @@ +declare let GL: any; +declare let GLctx: WebGLRenderingContext; +declare let Module: EmscriptenModule; + +type SKGLViewInfo = { + context: WebGLRenderingContext | WebGL2RenderingContext | undefined; + fboId: number; + stencil: number; + sample: number; + depth: number; +} + +type SKHtmlCanvasElement = { + SKHtmlCanvas: Canvas | undefined +} & HTMLCanvasElement + +export class Canvas { + static elements: Map; + + //htmlCanvas: HTMLCanvasElement; + glInfo?: SKGLViewInfo; + //renderFrameCallback: DotNet.DotNetObject; + renderLoopEnabled: boolean = false; + renderLoopRequest: number = 0; + newWidth?: number; + newHeight?: number; + + public static initGL(element: HTMLCanvasElement, elementId: string): SKGLViewInfo | null { + console.log("inside initGL"); + var view = Canvas.init(true, element, elementId); + if (!view || !view.glInfo) + return null; + + return view.glInfo; + } + + static init(useGL: boolean, element: HTMLCanvasElement, elementId: string): Canvas | null { + var htmlCanvas = element as SKHtmlCanvasElement; + if (!htmlCanvas) { + console.error(`No canvas element was provided.`); + return null; + } + + if (!Canvas.elements) + Canvas.elements = new Map(); + Canvas.elements.set(elementId, element); + + const view = new Canvas(useGL, element); + + htmlCanvas.SKHtmlCanvas = view; + + return view; + } + + + public constructor(useGL: boolean, element: HTMLCanvasElement) { + //this.htmlCanvas = element; + //this.renderFrameCallback = callback; + + if (useGL) { + const ctx = Canvas.createWebGLContext(element); + if (!ctx) { + console.error(`Failed to create WebGL context: err ${ctx}`); + return; + } + + // make current + GL.makeContextCurrent(ctx); + + // read values + const fbo = GLctx.getParameter(GLctx.FRAMEBUFFER_BINDING); + this.glInfo = { + context: ctx, + fboId: fbo ? fbo.id : 0, + stencil: GLctx.getParameter(GLctx.STENCIL_BITS), + sample: 0, // TODO: GLctx.getParameter(GLctx.SAMPLES) + depth: GLctx.getParameter(GLctx.DEPTH_BITS), + }; + } + } + + + static Foo(canvas: HTMLCanvasElement) { + const ctx = canvas.getContext("2d")!; + ctx.fillStyle = "#FF0000"; + ctx.fillRect(0, 0, 150, 75); + } + + static createWebGLContext(htmlCanvas: HTMLCanvasElement): WebGLRenderingContext | WebGL2RenderingContext { + const contextAttributes = { + alpha: 1, + depth: 1, + stencil: 8, + antialias: 0, + premultipliedAlpha: 1, + preserveDrawingBuffer: 0, + preferLowPowerToHighPerformance: 0, + failIfMajorPerformanceCaveat: 0, + majorVersion: 2, + minorVersion: 0, + enableExtensionsByDefault: 1, + explicitSwapControl: 0, + renderViaOffscreenBackBuffer: 1, + }; + + + var context = htmlCanvas.getContext("webgl2", contextAttributes); + + let ctx: WebGLRenderingContext = GL.createContext(htmlCanvas, contextAttributes); + if (!ctx && contextAttributes.majorVersion > 1) { + console.warn('Falling back to WebGL 1.0'); + contextAttributes.majorVersion = 1; + contextAttributes.minorVersion = 0; + ctx = GL.createContext(htmlCanvas, contextAttributes); + } + + return ctx; + } +} \ No newline at end of file diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts new file mode 100644 index 0000000000..bd1989e935 --- /dev/null +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts @@ -0,0 +1,23 @@ +import { RuntimeAPI } from "../../types/dotnet"; + +import { Canvas } from "./canvas"; + +export class AvaloniaRuntime { + constructor( + private dotnetAssembly: any, + api: RuntimeAPI + ) { + api.setModuleImports("avalonia.ts", { + Canvas + }); + } + + createAvaloniaView(element: HTMLDivElement): void { + const canvas = document.createElement("canvas"); + element.appendChild(canvas); + + + + this.dotnetAssembly.Avalonia.Web.AvaloniaRuntime.StartAvaloniaView(canvas); + } +} \ No newline at end of file diff --git a/src/Web/Avalonia.Web/webapp/modules/storage.ts b/src/Web/Avalonia.Web/webapp/modules/storage.ts new file mode 100644 index 0000000000..7b6cd816de --- /dev/null +++ b/src/Web/Avalonia.Web/webapp/modules/storage.ts @@ -0,0 +1,5 @@ +export class StorageProvider { + static isFileApiSupported(): boolean { + return (globalThis as any).showOpenFilePicker !== undefined; + } +} \ No newline at end of file diff --git a/src/Web/Avalonia.Web/webapp/package-lock.json b/src/Web/Avalonia.Web/webapp/package-lock.json new file mode 100644 index 0000000000..5df9471195 --- /dev/null +++ b/src/Web/Avalonia.Web/webapp/package-lock.json @@ -0,0 +1,634 @@ +{ + "name": "avalonia.web", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "avalonia.web", + "devDependencies": { + "@types/emscripten": "^1.39.6", + "@types/wicg-file-system-access": "^2020.9.5", + "esbuild": "^0.15.7", + "typescript": "^4.7.4" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.9.tgz", + "integrity": "sha512-VZPy/ETF3fBG5PiinIkA0W/tlsvlEgJccyN2DzWZEl0DlVKRbu91PvY2D6Lxgluj4w9QtYHjOWjAT44C+oQ+EQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.9.tgz", + "integrity": "sha512-O+NfmkfRrb3uSsTa4jE3WApidSe3N5++fyOVGP1SmMZi4A3BZELkhUUvj5hwmMuNdlpzAZ8iAPz2vmcR7DCFQA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@types/emscripten": { + "version": "1.39.6", + "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.39.6.tgz", + "integrity": "sha512-H90aoynNhhkQP6DRweEjJp5vfUVdIj7tdPLsu7pq89vODD/lcugKfZOsfgwpvM6XUewEp2N5dCg1Uf3Qe55Dcg==", + "dev": true + }, + "node_modules/@types/wicg-file-system-access": { + "version": "2020.9.5", + "resolved": "https://registry.npmjs.org/@types/wicg-file-system-access/-/wicg-file-system-access-2020.9.5.tgz", + "integrity": "sha512-UYK244awtmcUYQfs7FR8710MJcefL2WvkyHMjA8yJzxd1mo0Gfn88sRZ1Bls7hiUhA2w7ne1gpJ9T5g3G0wOyA==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.9.tgz", + "integrity": "sha512-OnYr1rkMVxtmMHIAKZLMcEUlJmqcbxBz9QoBU8G9v455na0fuzlT/GLu6l+SRghrk0Mm2fSSciMmzV43Q8e0Gg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.15.9", + "@esbuild/linux-loong64": "0.15.9", + "esbuild-android-64": "0.15.9", + "esbuild-android-arm64": "0.15.9", + "esbuild-darwin-64": "0.15.9", + "esbuild-darwin-arm64": "0.15.9", + "esbuild-freebsd-64": "0.15.9", + "esbuild-freebsd-arm64": "0.15.9", + "esbuild-linux-32": "0.15.9", + "esbuild-linux-64": "0.15.9", + "esbuild-linux-arm": "0.15.9", + "esbuild-linux-arm64": "0.15.9", + "esbuild-linux-mips64le": "0.15.9", + "esbuild-linux-ppc64le": "0.15.9", + "esbuild-linux-riscv64": "0.15.9", + "esbuild-linux-s390x": "0.15.9", + "esbuild-netbsd-64": "0.15.9", + "esbuild-openbsd-64": "0.15.9", + "esbuild-sunos-64": "0.15.9", + "esbuild-windows-32": "0.15.9", + "esbuild-windows-64": "0.15.9", + "esbuild-windows-arm64": "0.15.9" + } + }, + "node_modules/esbuild-android-64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.9.tgz", + "integrity": "sha512-HQCX7FJn9T4kxZQkhPjNZC7tBWZqJvhlLHPU2SFzrQB/7nDXjmTIFpFTjt7Bd1uFpeXmuwf5h5fZm+x/hLnhbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-android-arm64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.9.tgz", + "integrity": "sha512-E6zbLfqbFVCNEKircSHnPiSTsm3fCRxeIMPfrkS33tFjIAoXtwegQfVZqMGR0FlsvVxp2NEDOUz+WW48COCjSg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.9.tgz", + "integrity": "sha512-gI7dClcDN/HHVacZhTmGjl0/TWZcGuKJ0I7/xDGJwRQQn7aafZGtvagOFNmuOq+OBFPhlPv1T6JElOXb0unkSQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-arm64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.9.tgz", + "integrity": "sha512-VZIMlcRN29yg/sv7DsDwN+OeufCcoTNaTl3Vnav7dL/nvsApD7uvhVRbgyMzv0zU/PP0xRhhIpTyc7lxEzHGSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.9.tgz", + "integrity": "sha512-uM4z5bTvuAXqPxrI204txhlsPIolQPWRMLenvGuCPZTnnGlCMF2QLs0Plcm26gcskhxewYo9LkkmYSS5Czrb5A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-arm64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.9.tgz", + "integrity": "sha512-HHDjT3O5gWzicGdgJ5yokZVN9K9KG05SnERwl9nBYZaCjcCgj/sX8Ps1jvoFSfNCO04JSsHSOWo4qvxFuj8FoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-32": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.9.tgz", + "integrity": "sha512-AQIdE8FugGt1DkcekKi5ycI46QZpGJ/wqcMr7w6YUmOmp2ohQ8eO4sKUsOxNOvYL7hGEVwkndSyszR6HpVHLFg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.9.tgz", + "integrity": "sha512-4RXjae7g6Qs7StZyiYyXTZXBlfODhb1aBVAjd+ANuPmMhWthQilWo7rFHwJwL7DQu1Fjej2sODAVwLbcIVsAYQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.9.tgz", + "integrity": "sha512-3Zf2GVGUOI7XwChH3qrnTOSqfV1V4CAc/7zLVm4lO6JT6wbJrTgEYCCiNSzziSju+J9Jhf9YGWk/26quWPC6yQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.9.tgz", + "integrity": "sha512-a+bTtxJmYmk9d+s2W4/R1SYKDDAldOKmWjWP0BnrWtDbvUBNOm++du0ysPju4mZVoEFgS1yLNW+VXnG/4FNwdQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-mips64le": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.9.tgz", + "integrity": "sha512-Zn9HSylDp89y+TRREMDoGrc3Z4Hs5u56ozZLQCiZAUx2+HdbbXbWdjmw3FdTJ/i7t5Cew6/Q+6kfO3KCcFGlyw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-ppc64le": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.9.tgz", + "integrity": "sha512-OEiOxNAMH9ENFYqRsWUj3CWyN3V8P3ZXyfNAtX5rlCEC/ERXrCEFCJji/1F6POzsXAzxvUJrTSTCy7G6BhA6Fw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-riscv64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.9.tgz", + "integrity": "sha512-ukm4KsC3QRausEFjzTsOZ/qqazw0YvJsKmfoZZm9QW27OHjk2XKSQGGvx8gIEswft/Sadp03/VZvAaqv5AIwNA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-s390x": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.9.tgz", + "integrity": "sha512-uDOQEH55wQ6ahcIKzQr3VyjGc6Po/xblLGLoUk3fVL1qjlZAibtQr6XRfy5wPJLu/M2o0vQKLq4lyJ2r1tWKcw==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-netbsd-64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.9.tgz", + "integrity": "sha512-yWgxaYTQz+TqX80wXRq6xAtb7GSBAp6gqLKfOdANg9qEmAI1Bxn04IrQr0Mzm4AhxvGKoHzjHjMgXbCCSSDxcw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-openbsd-64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.9.tgz", + "integrity": "sha512-JmS18acQl4iSAjrEha1MfEmUMN4FcnnrtTaJ7Qg0tDCOcgpPPQRLGsZqhes0vmx8VA6IqRyScqXvaL7+Q0Uf3A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-sunos-64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.9.tgz", + "integrity": "sha512-UKynGSWpzkPmXW3D2UMOD9BZPIuRaSqphxSCwScfEE05Be3KAmvjsBhht1fLzKpiFVJb0BYMd4jEbWMyJ/z1hQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-32": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.9.tgz", + "integrity": "sha512-aqXvu4/W9XyTVqO/hw3rNxKE1TcZiEYHPsXM9LwYmKSX9/hjvfIJzXwQBlPcJ/QOxedfoMVH0YnhhQ9Ffb0RGA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.9.tgz", + "integrity": "sha512-zm7h91WUmlS4idMtjvCrEeNhlH7+TNOmqw5dJPJZrgFaxoFyqYG6CKDpdFCQXdyKpD5yvzaQBOMVTCBVKGZDEg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-arm64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.9.tgz", + "integrity": "sha512-yQEVIv27oauAtvtuhJVfSNMztJJX47ismRS6Sv2QMVV9RM+6xjbMWuuwM2nxr5A2/gj/mu2z9YlQxiwoFRCfZA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/typescript": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.3.tgz", + "integrity": "sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + } + }, + "dependencies": { + "@esbuild/android-arm": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.9.tgz", + "integrity": "sha512-VZPy/ETF3fBG5PiinIkA0W/tlsvlEgJccyN2DzWZEl0DlVKRbu91PvY2D6Lxgluj4w9QtYHjOWjAT44C+oQ+EQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.9.tgz", + "integrity": "sha512-O+NfmkfRrb3uSsTa4jE3WApidSe3N5++fyOVGP1SmMZi4A3BZELkhUUvj5hwmMuNdlpzAZ8iAPz2vmcR7DCFQA==", + "dev": true, + "optional": true + }, + "@types/emscripten": { + "version": "1.39.6", + "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.39.6.tgz", + "integrity": "sha512-H90aoynNhhkQP6DRweEjJp5vfUVdIj7tdPLsu7pq89vODD/lcugKfZOsfgwpvM6XUewEp2N5dCg1Uf3Qe55Dcg==", + "dev": true + }, + "@types/wicg-file-system-access": { + "version": "2020.9.5", + "resolved": "https://registry.npmjs.org/@types/wicg-file-system-access/-/wicg-file-system-access-2020.9.5.tgz", + "integrity": "sha512-UYK244awtmcUYQfs7FR8710MJcefL2WvkyHMjA8yJzxd1mo0Gfn88sRZ1Bls7hiUhA2w7ne1gpJ9T5g3G0wOyA==", + "dev": true + }, + "esbuild": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.9.tgz", + "integrity": "sha512-OnYr1rkMVxtmMHIAKZLMcEUlJmqcbxBz9QoBU8G9v455na0fuzlT/GLu6l+SRghrk0Mm2fSSciMmzV43Q8e0Gg==", + "dev": true, + "requires": { + "@esbuild/android-arm": "0.15.9", + "@esbuild/linux-loong64": "0.15.9", + "esbuild-android-64": "0.15.9", + "esbuild-android-arm64": "0.15.9", + "esbuild-darwin-64": "0.15.9", + "esbuild-darwin-arm64": "0.15.9", + "esbuild-freebsd-64": "0.15.9", + "esbuild-freebsd-arm64": "0.15.9", + "esbuild-linux-32": "0.15.9", + "esbuild-linux-64": "0.15.9", + "esbuild-linux-arm": "0.15.9", + "esbuild-linux-arm64": "0.15.9", + "esbuild-linux-mips64le": "0.15.9", + "esbuild-linux-ppc64le": "0.15.9", + "esbuild-linux-riscv64": "0.15.9", + "esbuild-linux-s390x": "0.15.9", + "esbuild-netbsd-64": "0.15.9", + "esbuild-openbsd-64": "0.15.9", + "esbuild-sunos-64": "0.15.9", + "esbuild-windows-32": "0.15.9", + "esbuild-windows-64": "0.15.9", + "esbuild-windows-arm64": "0.15.9" + } + }, + "esbuild-android-64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.9.tgz", + "integrity": "sha512-HQCX7FJn9T4kxZQkhPjNZC7tBWZqJvhlLHPU2SFzrQB/7nDXjmTIFpFTjt7Bd1uFpeXmuwf5h5fZm+x/hLnhbw==", + "dev": true, + "optional": true + }, + "esbuild-android-arm64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.9.tgz", + "integrity": "sha512-E6zbLfqbFVCNEKircSHnPiSTsm3fCRxeIMPfrkS33tFjIAoXtwegQfVZqMGR0FlsvVxp2NEDOUz+WW48COCjSg==", + "dev": true, + "optional": true + }, + "esbuild-darwin-64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.9.tgz", + "integrity": "sha512-gI7dClcDN/HHVacZhTmGjl0/TWZcGuKJ0I7/xDGJwRQQn7aafZGtvagOFNmuOq+OBFPhlPv1T6JElOXb0unkSQ==", + "dev": true, + "optional": true + }, + "esbuild-darwin-arm64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.9.tgz", + "integrity": "sha512-VZIMlcRN29yg/sv7DsDwN+OeufCcoTNaTl3Vnav7dL/nvsApD7uvhVRbgyMzv0zU/PP0xRhhIpTyc7lxEzHGSw==", + "dev": true, + "optional": true + }, + "esbuild-freebsd-64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.9.tgz", + "integrity": "sha512-uM4z5bTvuAXqPxrI204txhlsPIolQPWRMLenvGuCPZTnnGlCMF2QLs0Plcm26gcskhxewYo9LkkmYSS5Czrb5A==", + "dev": true, + "optional": true + }, + "esbuild-freebsd-arm64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.9.tgz", + "integrity": "sha512-HHDjT3O5gWzicGdgJ5yokZVN9K9KG05SnERwl9nBYZaCjcCgj/sX8Ps1jvoFSfNCO04JSsHSOWo4qvxFuj8FoA==", + "dev": true, + "optional": true + }, + "esbuild-linux-32": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.9.tgz", + "integrity": "sha512-AQIdE8FugGt1DkcekKi5ycI46QZpGJ/wqcMr7w6YUmOmp2ohQ8eO4sKUsOxNOvYL7hGEVwkndSyszR6HpVHLFg==", + "dev": true, + "optional": true + }, + "esbuild-linux-64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.9.tgz", + "integrity": "sha512-4RXjae7g6Qs7StZyiYyXTZXBlfODhb1aBVAjd+ANuPmMhWthQilWo7rFHwJwL7DQu1Fjej2sODAVwLbcIVsAYQ==", + "dev": true, + "optional": true + }, + "esbuild-linux-arm": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.9.tgz", + "integrity": "sha512-3Zf2GVGUOI7XwChH3qrnTOSqfV1V4CAc/7zLVm4lO6JT6wbJrTgEYCCiNSzziSju+J9Jhf9YGWk/26quWPC6yQ==", + "dev": true, + "optional": true + }, + "esbuild-linux-arm64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.9.tgz", + "integrity": "sha512-a+bTtxJmYmk9d+s2W4/R1SYKDDAldOKmWjWP0BnrWtDbvUBNOm++du0ysPju4mZVoEFgS1yLNW+VXnG/4FNwdQ==", + "dev": true, + "optional": true + }, + "esbuild-linux-mips64le": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.9.tgz", + "integrity": "sha512-Zn9HSylDp89y+TRREMDoGrc3Z4Hs5u56ozZLQCiZAUx2+HdbbXbWdjmw3FdTJ/i7t5Cew6/Q+6kfO3KCcFGlyw==", + "dev": true, + "optional": true + }, + "esbuild-linux-ppc64le": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.9.tgz", + "integrity": "sha512-OEiOxNAMH9ENFYqRsWUj3CWyN3V8P3ZXyfNAtX5rlCEC/ERXrCEFCJji/1F6POzsXAzxvUJrTSTCy7G6BhA6Fw==", + "dev": true, + "optional": true + }, + "esbuild-linux-riscv64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.9.tgz", + "integrity": "sha512-ukm4KsC3QRausEFjzTsOZ/qqazw0YvJsKmfoZZm9QW27OHjk2XKSQGGvx8gIEswft/Sadp03/VZvAaqv5AIwNA==", + "dev": true, + "optional": true + }, + "esbuild-linux-s390x": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.9.tgz", + "integrity": "sha512-uDOQEH55wQ6ahcIKzQr3VyjGc6Po/xblLGLoUk3fVL1qjlZAibtQr6XRfy5wPJLu/M2o0vQKLq4lyJ2r1tWKcw==", + "dev": true, + "optional": true + }, + "esbuild-netbsd-64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.9.tgz", + "integrity": "sha512-yWgxaYTQz+TqX80wXRq6xAtb7GSBAp6gqLKfOdANg9qEmAI1Bxn04IrQr0Mzm4AhxvGKoHzjHjMgXbCCSSDxcw==", + "dev": true, + "optional": true + }, + "esbuild-openbsd-64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.9.tgz", + "integrity": "sha512-JmS18acQl4iSAjrEha1MfEmUMN4FcnnrtTaJ7Qg0tDCOcgpPPQRLGsZqhes0vmx8VA6IqRyScqXvaL7+Q0Uf3A==", + "dev": true, + "optional": true + }, + "esbuild-sunos-64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.9.tgz", + "integrity": "sha512-UKynGSWpzkPmXW3D2UMOD9BZPIuRaSqphxSCwScfEE05Be3KAmvjsBhht1fLzKpiFVJb0BYMd4jEbWMyJ/z1hQ==", + "dev": true, + "optional": true + }, + "esbuild-windows-32": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.9.tgz", + "integrity": "sha512-aqXvu4/W9XyTVqO/hw3rNxKE1TcZiEYHPsXM9LwYmKSX9/hjvfIJzXwQBlPcJ/QOxedfoMVH0YnhhQ9Ffb0RGA==", + "dev": true, + "optional": true + }, + "esbuild-windows-64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.9.tgz", + "integrity": "sha512-zm7h91WUmlS4idMtjvCrEeNhlH7+TNOmqw5dJPJZrgFaxoFyqYG6CKDpdFCQXdyKpD5yvzaQBOMVTCBVKGZDEg==", + "dev": true, + "optional": true + }, + "esbuild-windows-arm64": { + "version": "0.15.9", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.9.tgz", + "integrity": "sha512-yQEVIv27oauAtvtuhJVfSNMztJJX47ismRS6Sv2QMVV9RM+6xjbMWuuwM2nxr5A2/gj/mu2z9YlQxiwoFRCfZA==", + "dev": true, + "optional": true + }, + "typescript": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.3.tgz", + "integrity": "sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==", + "dev": true + } + } +} diff --git a/src/Web/Avalonia.Web/webapp/package.json b/src/Web/Avalonia.Web/webapp/package.json new file mode 100644 index 0000000000..ca029ce534 --- /dev/null +++ b/src/Web/Avalonia.Web/webapp/package.json @@ -0,0 +1,13 @@ +{ + "name": "avalonia.web", + "scripts": { + "prebuild": "node ./node_modules/typescript/bin/tsc -noEmit", + "build": "node build.js" + }, + "devDependencies": { + "@types/emscripten": "^1.39.6", + "@types/wicg-file-system-access": "^2020.9.5", + "typescript": "^4.7.4", + "esbuild": "^0.15.7" + } +} diff --git a/src/Web/Avalonia.Web/webapp/tsconfig.json b/src/Web/Avalonia.Web/webapp/tsconfig.json new file mode 100644 index 0000000000..ad0e727150 --- /dev/null +++ b/src/Web/Avalonia.Web/webapp/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "es2016", + "module": "es2020", + "strict": true, + "sourceMap": true, + "noEmitOnError": true, + "isolatedModules": true, // we need it for esbuild + "lib": [ + "dom", + "es2016", + "esnext.asynciterable" + ] + }, + "exclude": [ + "node_modules" + ] + } + \ No newline at end of file diff --git a/src/Web/Avalonia.Web/webapp/types/dotnet.d.ts b/src/Web/Avalonia.Web/webapp/types/dotnet.d.ts new file mode 100644 index 0000000000..bc2068cb59 --- /dev/null +++ b/src/Web/Avalonia.Web/webapp/types/dotnet.d.ts @@ -0,0 +1,249 @@ +// See https://raw.githubusercontent.com/dotnet/runtime/main/src/mono/wasm/runtime/dotnet.d.ts + +//! Licensed to the .NET Foundation under one or more agreements. +//! The .NET Foundation licenses this file to you under the MIT license. +//! +//! This is generated file, see src/mono/wasm/runtime/rollup.config.js + +//! This is not considered public API with backward compatibility guarantees. + +interface DotnetHostBuilder { + withConfig(config: MonoConfig): DotnetHostBuilder; + withConfigSrc(configSrc: string): DotnetHostBuilder; + withApplicationArguments(...args: string[]): DotnetHostBuilder; + withEnvironmentVariable(name: string, value: string): DotnetHostBuilder; + withEnvironmentVariables(variables: { + [i: string]: string; + }): DotnetHostBuilder; + withVirtualWorkingDirectory(vfsPath: string): DotnetHostBuilder; + withDiagnosticTracing(enabled: boolean): DotnetHostBuilder; + withDebugging(level: number): DotnetHostBuilder; + withMainAssembly(mainAssemblyName: string): DotnetHostBuilder; + withApplicationArgumentsFromQuery(): DotnetHostBuilder; + create(): Promise; + run(): Promise; +} + +declare interface NativePointer { + __brandNativePointer: "NativePointer"; +} +declare interface VoidPtr extends NativePointer { + __brand: "VoidPtr"; +} +declare interface CharPtr extends NativePointer { + __brand: "CharPtr"; +} +declare interface Int32Ptr extends NativePointer { + __brand: "Int32Ptr"; +} +declare interface EmscriptenModule { + HEAP8: Int8Array; + HEAP16: Int16Array; + HEAP32: Int32Array; + HEAPU8: Uint8Array; + HEAPU16: Uint16Array; + HEAPU32: Uint32Array; + HEAPF32: Float32Array; + HEAPF64: Float64Array; + _malloc(size: number): VoidPtr; + _free(ptr: VoidPtr): void; + print(message: string): void; + printErr(message: string): void; + ccall(ident: string, returnType?: string | null, argTypes?: string[], args?: any[], opts?: any): T; + cwrap(ident: string, returnType: string, argTypes?: string[], opts?: any): T; + cwrap(ident: string, ...args: any[]): T; + setValue(ptr: VoidPtr, value: number, type: string, noSafe?: number | boolean): void; + setValue(ptr: Int32Ptr, value: number, type: string, noSafe?: number | boolean): void; + getValue(ptr: number, type: string, noSafe?: number | boolean): number; + UTF8ToString(ptr: CharPtr, maxBytesToRead?: number): string; + UTF8ArrayToString(u8Array: Uint8Array, idx?: number, maxBytesToRead?: number): string; + FS_createPath(parent: string, path: string, canRead?: boolean, canWrite?: boolean): string; + FS_createDataFile(parent: string, name: string, data: TypedArray, canRead: boolean, canWrite: boolean, canOwn?: boolean): string; + FS_readFile(filename: string, opts: any): any; + removeRunDependency(id: string): void; + addRunDependency(id: string): void; + stackSave(): VoidPtr; + stackRestore(stack: VoidPtr): void; + stackAlloc(size: number): VoidPtr; + ready: Promise; + instantiateWasm?: InstantiateWasmCallBack; + preInit?: (() => any)[] | (() => any); + preRun?: (() => any)[] | (() => any); + onRuntimeInitialized?: () => any; + postRun?: (() => any)[] | (() => any); + onAbort?: { + (error: any): void; + }; +} +declare type InstantiateWasmSuccessCallback = (instance: WebAssembly.Instance, module: WebAssembly.Module) => void; +declare type InstantiateWasmCallBack = (imports: WebAssembly.Imports, successCallback: InstantiateWasmSuccessCallback) => any; +declare type TypedArray = Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array; + +declare type MonoConfig = { + /** + * The subfolder containing managed assemblies and pdbs. This is relative to dotnet.js script. + */ + assemblyRootFolder?: string; + /** + * A list of assets to load along with the runtime. + */ + assets?: AssetEntry[]; + /** + * Additional search locations for assets. + */ + remoteSources?: string[]; + /** + * It will not fail the startup is .pdb files can't be downloaded + */ + ignorePdbLoadErrors?: boolean; + /** + * We are throttling parallel downloads in order to avoid net::ERR_INSUFFICIENT_RESOURCES on chrome. The default value is 16. + */ + maxParallelDownloads?: number; + /** + * Name of the assembly with main entrypoint + */ + mainAssemblyName?: string; + /** + * Configures the runtime's globalization mode + */ + globalizationMode?: GlobalizationMode; + /** + * debugLevel > 0 enables debugging and sets the debug log level to debugLevel + * debugLevel == 0 disables debugging and enables interpreter optimizations + * debugLevel < 0 enabled debugging and disables debug logging. + */ + debugLevel?: number; + /** + * Enables diagnostic log messages during startup + */ + diagnosticTracing?: boolean; + /** + * Dictionary-style Object containing environment variables + */ + environmentVariables?: { + [i: string]: string; + }; + /** + * initial number of workers to add to the emscripten pthread pool + */ + pthreadPoolSize?: number; +}; +interface ResourceRequest { + name: string; + behavior: AssetBehaviours; + resolvedUrl?: string; + hash?: string; +} +interface LoadingResource { + name: string; + url: string; + response: Promise; +} +interface AssetEntry extends ResourceRequest { + /** + * If specified, overrides the path of the asset in the virtual filesystem and similar data structures once downloaded. + */ + virtualPath?: string; + /** + * Culture code + */ + culture?: string; + /** + * If true, an attempt will be made to load the asset from each location in MonoConfig.remoteSources. + */ + loadRemote?: boolean; + /** + * If true, the runtime startup would not fail if the asset download was not successful. + */ + isOptional?: boolean; + /** + * If provided, runtime doesn't have to fetch the data. + * Runtime would set the buffer to null after instantiation to free the memory. + */ + buffer?: ArrayBuffer; + /** + * It's metadata + fetch-like Promise + * If provided, the runtime doesn't have to initiate the download. It would just await the response. + */ + pendingDownload?: LoadingResource; +} +declare type AssetBehaviours = "resource" | "assembly" | "pdb" | "heap" | "icu" | "vfs" | "dotnetwasm" | "js-module-threads"; +declare type GlobalizationMode = "icu" | // load ICU globalization data from any runtime assets with behavior "icu". +"invariant" | // operate in invariant globalization mode. +"auto"; +declare type DotnetModuleConfig = { + disableDotnet6Compatibility?: boolean; + config?: MonoConfig; + configSrc?: string; + onConfigLoaded?: (config: MonoConfig) => void | Promise; + onDotnetReady?: () => void | Promise; + imports?: any; + exports?: string[]; + downloadResource?: (request: ResourceRequest) => LoadingResource | undefined; +} & Partial; +declare type APIType = { + runMain: (mainAssemblyName: string, args: string[]) => Promise; + runMainAndExit: (mainAssemblyName: string, args: string[]) => Promise; + setEnvironmentVariable: (name: string, value: string) => void; + getAssemblyExports(assemblyName: string): Promise; + setModuleImports(moduleName: string, moduleImports: any): void; + getConfig: () => MonoConfig; + setHeapB32: (offset: NativePointer, value: number | boolean) => void; + setHeapU8: (offset: NativePointer, value: number) => void; + setHeapU16: (offset: NativePointer, value: number) => void; + setHeapU32: (offset: NativePointer, value: NativePointer | number) => void; + setHeapI8: (offset: NativePointer, value: number) => void; + setHeapI16: (offset: NativePointer, value: number) => void; + setHeapI32: (offset: NativePointer, value: number) => void; + setHeapI52: (offset: NativePointer, value: number) => void; + setHeapU52: (offset: NativePointer, value: number) => void; + setHeapI64Big: (offset: NativePointer, value: bigint) => void; + setHeapF32: (offset: NativePointer, value: number) => void; + setHeapF64: (offset: NativePointer, value: number) => void; + getHeapB32: (offset: NativePointer) => boolean; + getHeapU8: (offset: NativePointer) => number; + getHeapU16: (offset: NativePointer) => number; + getHeapU32: (offset: NativePointer) => number; + getHeapI8: (offset: NativePointer) => number; + getHeapI16: (offset: NativePointer) => number; + getHeapI32: (offset: NativePointer) => number; + getHeapI52: (offset: NativePointer) => number; + getHeapU52: (offset: NativePointer) => number; + getHeapI64Big: (offset: NativePointer) => bigint; + getHeapF32: (offset: NativePointer) => number; + getHeapF64: (offset: NativePointer) => number; +}; +declare type RuntimeAPI = { + /** + * @deprecated Please use API object instead. See also MONOType in dotnet-legacy.d.ts + */ + MONO: any; + /** + * @deprecated Please use API object instead. See also BINDINGType in dotnet-legacy.d.ts + */ + BINDING: any; + INTERNAL: any; + Module: EmscriptenModule; + runtimeId: number; + runtimeBuildInfo: { + productVersion: string; + gitHash: string; + buildConfiguration: string; + }; +} & APIType; +declare type ModuleAPI = { + dotnet: DotnetHostBuilder; + exit: (code: number, reason?: any) => void; +}; +declare function createDotnetRuntime(moduleFactory: DotnetModuleConfig | ((api: RuntimeAPI) => DotnetModuleConfig)): Promise; +declare type CreateDotnetRuntimeType = typeof createDotnetRuntime; + +declare global { + function getDotnetRuntime(runtimeId: number): RuntimeAPI | undefined; +} + +declare const dotnet: ModuleAPI["dotnet"]; +declare const exit: ModuleAPI["exit"]; + +export { CreateDotnetRuntimeType, DotnetModuleConfig, EmscriptenModule, ModuleAPI, MonoConfig, RuntimeAPI, createDotnetRuntime as default, dotnet, exit }; \ No newline at end of file diff --git a/src/Web/Avalonia.Web/wwwroot/avalonia.js b/src/Web/Avalonia.Web/wwwroot/avalonia.js new file mode 100644 index 0000000000..8a44381e52 --- /dev/null +++ b/src/Web/Avalonia.Web/wwwroot/avalonia.js @@ -0,0 +1,122 @@ +var __async = (__this, __arguments, generator) => { + return new Promise((resolve, reject) => { + var fulfilled = (value) => { + try { + step(generator.next(value)); + } catch (e) { + reject(e); + } + }; + var rejected = (value) => { + try { + step(generator.throw(value)); + } catch (e) { + reject(e); + } + }; + var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected); + step((generator = generator.apply(__this, __arguments)).next()); + }); +}; + +// modules/avalonia/canvas.ts +var Canvas = class { + constructor(useGL, element) { + this.renderLoopEnabled = false; + this.renderLoopRequest = 0; + if (useGL) { + const ctx = Canvas.createWebGLContext(element); + if (!ctx) { + console.error(`Failed to create WebGL context: err ${ctx}`); + return; + } + GL.makeContextCurrent(ctx); + const fbo = GLctx.getParameter(GLctx.FRAMEBUFFER_BINDING); + this.glInfo = { + context: ctx, + fboId: fbo ? fbo.id : 0, + stencil: GLctx.getParameter(GLctx.STENCIL_BITS), + sample: 0, + depth: GLctx.getParameter(GLctx.DEPTH_BITS) + }; + } + } + static initGL(element, elementId) { + console.log("inside initGL"); + var view = Canvas.init(true, element, elementId); + if (!view || !view.glInfo) + return null; + return view.glInfo; + } + static init(useGL, element, elementId) { + var htmlCanvas = element; + if (!htmlCanvas) { + console.error(`No canvas element was provided.`); + return null; + } + if (!Canvas.elements) + Canvas.elements = /* @__PURE__ */ new Map(); + Canvas.elements.set(elementId, element); + const view = new Canvas(useGL, element); + htmlCanvas.SKHtmlCanvas = view; + return view; + } + static Foo(canvas) { + const ctx = canvas.getContext("2d"); + ctx.fillStyle = "#FF0000"; + ctx.fillRect(0, 0, 150, 75); + } + static createWebGLContext(htmlCanvas) { + const contextAttributes = { + alpha: 1, + depth: 1, + stencil: 8, + antialias: 0, + premultipliedAlpha: 1, + preserveDrawingBuffer: 0, + preferLowPowerToHighPerformance: 0, + failIfMajorPerformanceCaveat: 0, + majorVersion: 2, + minorVersion: 0, + enableExtensionsByDefault: 1, + explicitSwapControl: 0, + renderViaOffscreenBackBuffer: 1 + }; + var context = htmlCanvas.getContext("webgl2", contextAttributes); + let ctx = GL.createContext(htmlCanvas, contextAttributes); + if (!ctx && contextAttributes.majorVersion > 1) { + console.warn("Falling back to WebGL 1.0"); + contextAttributes.majorVersion = 1; + contextAttributes.minorVersion = 0; + ctx = GL.createContext(htmlCanvas, contextAttributes); + } + return ctx; + } +}; + +// modules/avalonia/runtime.ts +var AvaloniaRuntime = class { + constructor(dotnetAssembly, api) { + this.dotnetAssembly = dotnetAssembly; + api.setModuleImports("avalonia.ts", { + Canvas + }); + } + createAvaloniaView(element) { + const canvas = document.createElement("canvas"); + element.appendChild(canvas); + this.dotnetAssembly.Avalonia.Web.AvaloniaRuntime.StartAvaloniaView(canvas); + } +}; + +// modules/avalonia.ts +function createAvaloniaRuntime(api) { + return __async(this, null, function* () { + const dotnetAssembly = yield api.getAssemblyExports("Avalonia.Web.dll"); + return new AvaloniaRuntime(dotnetAssembly, api); + }); +} +export { + createAvaloniaRuntime +}; +//# sourceMappingURL=avalonia.js.map diff --git a/src/Web/Avalonia.Web/wwwroot/storage.js b/src/Web/Avalonia.Web/wwwroot/storage.js new file mode 100644 index 0000000000..a2042aaa11 --- /dev/null +++ b/src/Web/Avalonia.Web/wwwroot/storage.js @@ -0,0 +1,10 @@ +// modules/storage.ts +var StorageProvider = class { + static isFileApiSupported() { + return globalThis.showOpenFilePicker !== void 0; + } +}; +export { + StorageProvider +}; +//# sourceMappingURL=storage.js.map From 08336ae4b3c5fb11783355e59c14a438d2f333cf Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 23 Sep 2022 17:54:30 +0100 Subject: [PATCH 002/169] add js emscripten injection. --- .../Avalonia.Web.Sample/Avalonia.Web.Sample.csproj | 5 ++++- src/Web/Avalonia.Web.Sample/Program.cs | 13 +++++++++++++ src/Web/Avalonia.Web.Sample/interop.js | 13 +++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 src/Web/Avalonia.Web.Sample/interop.js diff --git a/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj b/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj index 8f5912a16f..80be694741 100644 --- a/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj +++ b/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj @@ -1,4 +1,4 @@ - + net7.0 browser-wasm @@ -6,6 +6,9 @@ Exe true true + true + -sVERBOSE + --js-library="C:\Users\User\repos\Avalonia\src\Web\Avalonia.Web.Sample\interop.js" diff --git a/src/Web/Avalonia.Web.Sample/Program.cs b/src/Web/Avalonia.Web.Sample/Program.cs index b880d61ab5..93516e65ed 100644 --- a/src/Web/Avalonia.Web.Sample/Program.cs +++ b/src/Web/Avalonia.Web.Sample/Program.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.InteropServices; using System.Runtime.InteropServices.JavaScript; using System.Threading.Tasks; using Avalonia.Web; @@ -6,10 +7,22 @@ using Avalonia.Web; internal class Program { + + [DllImport("libSkiaSharp", CallingConvention = CallingConvention.Cdecl)] + static extern JSObject example_initialize(); private static void Main(string[] args) { Console.WriteLine("Hello, Browser!"); + example_initialize(); + + Console.WriteLine(); + + + foreach(var arg in args) + { + Console.WriteLine(arg); + } AvaloniaRuntime.Init(); } } diff --git a/src/Web/Avalonia.Web.Sample/interop.js b/src/Web/Avalonia.Web.Sample/interop.js new file mode 100644 index 0000000000..2358c357d3 --- /dev/null +++ b/src/Web/Avalonia.Web.Sample/interop.js @@ -0,0 +1,13 @@ +var LibraryExample = { + // Internal functions + $EXAMPLE: { + internal_func: function () { + } + }, + example_initialize: function () { + window["avalonia-helper-GL"] = GL + } +} + +autoAddDeps(LibraryExample, '$EXAMPLE') +mergeInto(LibraryManager.library, LibraryExample) From e3e0f3caef6d430ebeb67ba991ca9954bd12b6da Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 23 Sep 2022 18:46:13 +0100 Subject: [PATCH 003/169] working webgl and GRGl context initialisation. --- src/Web/Avalonia.Web.Sample/Program.cs | 5 +---- src/Web/Avalonia.Web.Sample/interop.js | 2 +- src/Web/Avalonia.Web/AvaloniaRuntime.cs | 5 ++++- .../Avalonia.Web/webapp/modules/avalonia/canvas.ts | 13 +++++++------ src/Web/Avalonia.Web/wwwroot/avalonia.js | 4 +++- 5 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/Web/Avalonia.Web.Sample/Program.cs b/src/Web/Avalonia.Web.Sample/Program.cs index 93516e65ed..3ad6db15dc 100644 --- a/src/Web/Avalonia.Web.Sample/Program.cs +++ b/src/Web/Avalonia.Web.Sample/Program.cs @@ -8,14 +8,11 @@ using Avalonia.Web; internal class Program { - [DllImport("libSkiaSharp", CallingConvention = CallingConvention.Cdecl)] - static extern JSObject example_initialize(); + private static void Main(string[] args) { Console.WriteLine("Hello, Browser!"); - example_initialize(); - Console.WriteLine(); diff --git a/src/Web/Avalonia.Web.Sample/interop.js b/src/Web/Avalonia.Web.Sample/interop.js index 2358c357d3..b75212e7a7 100644 --- a/src/Web/Avalonia.Web.Sample/interop.js +++ b/src/Web/Avalonia.Web.Sample/interop.js @@ -5,7 +5,7 @@ var LibraryExample = { } }, example_initialize: function () { - window["avalonia-helper-GL"] = GL + globalThis.AvaloniaGL = GL } } diff --git a/src/Web/Avalonia.Web/AvaloniaRuntime.cs b/src/Web/Avalonia.Web/AvaloniaRuntime.cs index c8ff75a77f..d5bcd7dd98 100644 --- a/src/Web/Avalonia.Web/AvaloniaRuntime.cs +++ b/src/Web/Avalonia.Web/AvaloniaRuntime.cs @@ -9,10 +9,13 @@ public partial class AvaloniaRuntime { public record GLInfo(int ContextId, uint FboId, int Stencils, int Samples, int Depth); + [DllImport("libSkiaSharp", CallingConvention = CallingConvention.Cdecl)] + static extern JSObject example_initialize(); + [JSExport] internal static void StartAvaloniaView(JSObject canvas) { - Init(); + example_initialize(); // setup, get gl context... var info = InitGL(canvas, "testCanvas"); diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts index c8acf5e428..2e1443074d 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts @@ -1,6 +1,4 @@ -declare let GL: any; -declare let GLctx: WebGLRenderingContext; -declare let Module: EmscriptenModule; +declare let Module: EmscriptenModule; type SKGLViewInfo = { context: WebGLRenderingContext | WebGL2RenderingContext | undefined; @@ -64,9 +62,13 @@ export class Canvas { return; } + var GL = (globalThis as any).AvaloniaGL; + // make current GL.makeContextCurrent(ctx); + var GLctx = GL.currentContext.GLctx as WebGLRenderingContext; + // read values const fbo = GLctx.getParameter(GLctx.FRAMEBUFFER_BINDING); this.glInfo = { @@ -103,8 +105,7 @@ export class Canvas { renderViaOffscreenBackBuffer: 1, }; - - var context = htmlCanvas.getContext("webgl2", contextAttributes); + var GL = (globalThis as any).AvaloniaGL; let ctx: WebGLRenderingContext = GL.createContext(htmlCanvas, contextAttributes); if (!ctx && contextAttributes.majorVersion > 1) { @@ -116,4 +117,4 @@ export class Canvas { return ctx; } -} \ No newline at end of file +} diff --git a/src/Web/Avalonia.Web/wwwroot/avalonia.js b/src/Web/Avalonia.Web/wwwroot/avalonia.js index 8a44381e52..94c5e9d254 100644 --- a/src/Web/Avalonia.Web/wwwroot/avalonia.js +++ b/src/Web/Avalonia.Web/wwwroot/avalonia.js @@ -30,7 +30,9 @@ var Canvas = class { console.error(`Failed to create WebGL context: err ${ctx}`); return; } + var GL = globalThis.AvaloniaGL; GL.makeContextCurrent(ctx); + var GLctx = GL.currentContext.GLctx; const fbo = GLctx.getParameter(GLctx.FRAMEBUFFER_BINDING); this.glInfo = { context: ctx, @@ -82,7 +84,7 @@ var Canvas = class { explicitSwapControl: 0, renderViaOffscreenBackBuffer: 1 }; - var context = htmlCanvas.getContext("webgl2", contextAttributes); + var GL = globalThis.AvaloniaGL; let ctx = GL.createContext(htmlCanvas, contextAttributes); if (!ctx && contextAttributes.majorVersion > 1) { console.warn("Falling back to WebGL 1.0"); From 43017d159029d5903c446140eb40b58a726b0535 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 23 Sep 2022 18:50:58 +0100 Subject: [PATCH 004/169] remove and rename code. --- src/Web/Avalonia.Web.Sample/Program.cs | 2 +- src/Web/Avalonia.Web.Sample/interop.js | 2 +- src/Web/Avalonia.Web/AvaloniaRuntime.cs | 20 ++++---------------- 3 files changed, 6 insertions(+), 18 deletions(-) diff --git a/src/Web/Avalonia.Web.Sample/Program.cs b/src/Web/Avalonia.Web.Sample/Program.cs index 3ad6db15dc..18a593c9e8 100644 --- a/src/Web/Avalonia.Web.Sample/Program.cs +++ b/src/Web/Avalonia.Web.Sample/Program.cs @@ -14,12 +14,12 @@ internal class Program Console.WriteLine("Hello, Browser!"); Console.WriteLine(); - foreach(var arg in args) { Console.WriteLine(arg); } + AvaloniaRuntime.Init(); } } diff --git a/src/Web/Avalonia.Web.Sample/interop.js b/src/Web/Avalonia.Web.Sample/interop.js index b75212e7a7..c7ae3a56c7 100644 --- a/src/Web/Avalonia.Web.Sample/interop.js +++ b/src/Web/Avalonia.Web.Sample/interop.js @@ -4,7 +4,7 @@ var LibraryExample = { internal_func: function () { } }, - example_initialize: function () { + InterceptGLObject: function () { globalThis.AvaloniaGL = GL } } diff --git a/src/Web/Avalonia.Web/AvaloniaRuntime.cs b/src/Web/Avalonia.Web/AvaloniaRuntime.cs index d5bcd7dd98..0d21ae096d 100644 --- a/src/Web/Avalonia.Web/AvaloniaRuntime.cs +++ b/src/Web/Avalonia.Web/AvaloniaRuntime.cs @@ -1,7 +1,6 @@ using System; using System.Runtime.InteropServices; using System.Runtime.InteropServices.JavaScript; -using static System.Runtime.InteropServices.JavaScript.JSType; namespace Avalonia.Web; @@ -10,32 +9,24 @@ public partial class AvaloniaRuntime public record GLInfo(int ContextId, uint FboId, int Stencils, int Samples, int Depth); [DllImport("libSkiaSharp", CallingConvention = CallingConvention.Cdecl)] - static extern JSObject example_initialize(); + static extern JSObject InterceptGLObject(); [JSExport] internal static void StartAvaloniaView(JSObject canvas) { - example_initialize(); + InterceptGLObject(); // setup, get gl context... var info = InitGL(canvas, "testCanvas"); - /* var glInfo = new GLInfo( + var glInfo = new GLInfo( info.GetPropertyAsInt32("context"), (uint)info.GetPropertyAsInt32("fboId"), info.GetPropertyAsInt32("stencil"), info.GetPropertyAsInt32("sample"), info.GetPropertyAsInt32("depth")); - Console.WriteLine($"{glInfo.ContextId}, {glInfo.FboId}");*/ - } - - public static void Init () - { - if (false) - { - SkiaSharp.GRGlInterface.Create(); - } + Console.WriteLine($"{glInfo.ContextId}, {glInfo.FboId}"); } [JSImport("Canvas.Foo", "avalonia.ts")] @@ -47,8 +38,5 @@ public partial class AvaloniaRuntime [JSImport("StorageProvider.isFileApiSupported", "storage.ts")] public static partial bool IsFileApiSupported(); - //[DllImport("__Internal")] - //public - } From 39aa7f513c744809e5cf9930c76aa98ae1eee80a Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 23 Sep 2022 19:33:31 +0100 Subject: [PATCH 005/169] move initialisation to managed side. --- src/Web/Avalonia.Web.Sample/Program.cs | 16 +++++++++++++--- src/Web/Avalonia.Web.Sample/main.js | 10 +--------- src/Web/Avalonia.Web/AvaloniaRuntime.cs | 3 +++ .../webapp/modules/avalonia/canvas.ts | 8 ++++++++ .../webapp/modules/avalonia/runtime.ts | 6 +----- src/Web/Avalonia.Web/wwwroot/avalonia.js | 6 +++++- 6 files changed, 31 insertions(+), 18 deletions(-) diff --git a/src/Web/Avalonia.Web.Sample/Program.cs b/src/Web/Avalonia.Web.Sample/Program.cs index 18a593c9e8..e03a48ac5e 100644 --- a/src/Web/Avalonia.Web.Sample/Program.cs +++ b/src/Web/Avalonia.Web.Sample/Program.cs @@ -1,14 +1,18 @@ using System; +using System.Reflection.Metadata; using System.Runtime.InteropServices; using System.Runtime.InteropServices.JavaScript; using System.Threading.Tasks; +using System.Xml.Linq; using Avalonia.Web; //using SkiaSharp; -internal class Program +internal partial class Program { - + [JSImport("globalThis.document.getElementById")] + internal static partial JSObject GetElementById(string id); + private static void Main(string[] args) { Console.WriteLine("Hello, Browser!"); @@ -20,7 +24,13 @@ internal class Program Console.WriteLine(arg); } - AvaloniaRuntime.Init(); + var div = GetElementById("out"); + Console.WriteLine("got div"); + + var canvas = AvaloniaRuntime.CreateCanvas(div); + + Console.WriteLine("Created canvas"); + } } diff --git a/src/Web/Avalonia.Web.Sample/main.js b/src/Web/Avalonia.Web.Sample/main.js index 9973b469c7..9d90db8bd2 100644 --- a/src/Web/Avalonia.Web.Sample/main.js +++ b/src/Web/Avalonia.Web.Sample/main.js @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. import { dotnet } from './dotnet.js' -import { createAvaloniaRuntime } from './avalonia.js' const is_browser = typeof window != "undefined"; if (!is_browser) throw new Error(`Expected to be running in a browser`); @@ -12,13 +11,6 @@ const dotnetRuntime = await dotnet .withApplicationArgumentsFromQuery() .create(); -const avaloniaRuntime = await createAvaloniaRuntime(dotnetRuntime); - -const outDiv = document.getElementById("out"); -avaloniaRuntime.createAvaloniaView(outDiv); - const config = dotnetRuntime.getConfig(); -const exports = await dotnetRuntime.getAssemblyExports(config.mainAssemblyName); -await exports.MyClass.TestDynamicModule(); -await dotnetRuntime.runMainAndExit(config.mainAssemblyName, ["dotnet", "is", "great!"]); \ No newline at end of file +await dotnetRuntime.runMainAndExit(config.mainAssemblyName, ["dotnet", "is", "great!"]); diff --git a/src/Web/Avalonia.Web/AvaloniaRuntime.cs b/src/Web/Avalonia.Web/AvaloniaRuntime.cs index 0d21ae096d..d6f4b2dae1 100644 --- a/src/Web/Avalonia.Web/AvaloniaRuntime.cs +++ b/src/Web/Avalonia.Web/AvaloniaRuntime.cs @@ -29,6 +29,9 @@ public partial class AvaloniaRuntime Console.WriteLine($"{glInfo.ContextId}, {glInfo.FboId}"); } + [JSImport("Canvas.createCanvas", "avalonia.js")] + public static partial JSObject CreateCanvas(JSObject container); + [JSImport("Canvas.Foo", "avalonia.ts")] internal static partial void Foo(JSObject canvas); diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts index 2e1443074d..ad39d58a8c 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts @@ -23,6 +23,14 @@ export class Canvas { newWidth?: number; newHeight?: number; + public static createCanvas(element: HTMLDivElement): HTMLCanvasElement { + var canvas = document.createElement("canvas"); + + element.appendChild(canvas); + + return canvas; + } + public static initGL(element: HTMLCanvasElement, elementId: string): SKGLViewInfo | null { console.log("inside initGL"); var view = Canvas.init(true, element, elementId); diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts index bd1989e935..61e7b71797 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts @@ -15,9 +15,5 @@ export class AvaloniaRuntime { createAvaloniaView(element: HTMLDivElement): void { const canvas = document.createElement("canvas"); element.appendChild(canvas); - - - - this.dotnetAssembly.Avalonia.Web.AvaloniaRuntime.StartAvaloniaView(canvas); } -} \ No newline at end of file +} diff --git a/src/Web/Avalonia.Web/wwwroot/avalonia.js b/src/Web/Avalonia.Web/wwwroot/avalonia.js index 94c5e9d254..896ad807ff 100644 --- a/src/Web/Avalonia.Web/wwwroot/avalonia.js +++ b/src/Web/Avalonia.Web/wwwroot/avalonia.js @@ -43,6 +43,11 @@ var Canvas = class { }; } } + static createCanvas(element) { + var canvas = document.createElement("canvas"); + element.appendChild(canvas); + return canvas; + } static initGL(element, elementId) { console.log("inside initGL"); var view = Canvas.init(true, element, elementId); @@ -107,7 +112,6 @@ var AvaloniaRuntime = class { createAvaloniaView(element) { const canvas = document.createElement("canvas"); element.appendChild(canvas); - this.dotnetAssembly.Avalonia.Web.AvaloniaRuntime.StartAvaloniaView(canvas); } }; From 8efd4bb04d4b2d19a77e31825d8a570b7fa414f0 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 23 Sep 2022 15:48:51 -0400 Subject: [PATCH 006/169] Fix --- src/Web/Avalonia.Web.Sample/Program.cs | 3 ++- src/Web/Avalonia.Web.Sample/main.js | 3 +++ src/Web/Avalonia.Web/AvaloniaRuntime.cs | 4 ++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Web/Avalonia.Web.Sample/Program.cs b/src/Web/Avalonia.Web.Sample/Program.cs index e03a48ac5e..2d5bb6bec9 100644 --- a/src/Web/Avalonia.Web.Sample/Program.cs +++ b/src/Web/Avalonia.Web.Sample/Program.cs @@ -30,7 +30,8 @@ internal partial class Program var canvas = AvaloniaRuntime.CreateCanvas(div); Console.WriteLine("Created canvas"); - + + AvaloniaRuntime.Foo(canvas); } } diff --git a/src/Web/Avalonia.Web.Sample/main.js b/src/Web/Avalonia.Web.Sample/main.js index 9d90db8bd2..3683aea181 100644 --- a/src/Web/Avalonia.Web.Sample/main.js +++ b/src/Web/Avalonia.Web.Sample/main.js @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. import { dotnet } from './dotnet.js' +import { createAvaloniaRuntime } from './avalonia.js'; const is_browser = typeof window != "undefined"; if (!is_browser) throw new Error(`Expected to be running in a browser`); @@ -11,6 +12,8 @@ const dotnetRuntime = await dotnet .withApplicationArgumentsFromQuery() .create(); +await createAvaloniaRuntime(dotnetRuntime); + const config = dotnetRuntime.getConfig(); await dotnetRuntime.runMainAndExit(config.mainAssemblyName, ["dotnet", "is", "great!"]); diff --git a/src/Web/Avalonia.Web/AvaloniaRuntime.cs b/src/Web/Avalonia.Web/AvaloniaRuntime.cs index d6f4b2dae1..e5c315af4c 100644 --- a/src/Web/Avalonia.Web/AvaloniaRuntime.cs +++ b/src/Web/Avalonia.Web/AvaloniaRuntime.cs @@ -29,11 +29,11 @@ public partial class AvaloniaRuntime Console.WriteLine($"{glInfo.ContextId}, {glInfo.FboId}"); } - [JSImport("Canvas.createCanvas", "avalonia.js")] + [JSImport("Canvas.createCanvas", "avalonia.ts")] public static partial JSObject CreateCanvas(JSObject container); [JSImport("Canvas.Foo", "avalonia.ts")] - internal static partial void Foo(JSObject canvas); + public static partial void Foo(JSObject canvas); [JSImport("Canvas.initGL", "avalonia.ts")] internal static partial JSObject InitGL(JSObject canvas, string canvasId); From 2a67b7950b79667e1bbe53ab6988b4de5158da25 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 23 Sep 2022 23:18:47 +0100 Subject: [PATCH 007/169] temporarilty trim down control catalog. --- samples/ControlCatalog/MainView.xaml | 217 +----------------------- samples/ControlCatalog/MainView.xaml.cs | 99 +---------- 2 files changed, 4 insertions(+), 312 deletions(-) diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 7f5a191519..15cdb93896 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -4,218 +4,7 @@ xmlns:controls="clr-namespace:ControlSamples;assembly=ControlSamples" xmlns:models="clr-namespace:ControlCatalog.Models" xmlns:pages="clr-namespace:ControlCatalog.Pages"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - None - BorderOnly - Full - - - - - FluentLight - FluentDark - SimpleLight - SimpleDark - - - - - None - Transparent - Blur - AcrylicBlur - Mica - - - - - LeftToRight - RightToLeft - - - - - - - - + + + diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index 7133ddaa6a..52dbd25c8c 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -18,104 +18,7 @@ namespace ControlCatalog { AvaloniaXamlLoader.Load(this); - var sideBar = this.Get("Sidebar"); - - if (AvaloniaLocator.Current?.GetService()?.GetRuntimeInfo().IsDesktop == true) - { - var tabItems = (sideBar.Items as IList); - tabItems?.Add(new TabItem() - { - Header = "Screens", - Content = new ScreenPage() - }); - - } - - var themes = this.Get("Themes"); - themes.SelectionChanged += (sender, e) => - { - if (themes.SelectedItem is CatalogTheme theme) - { - var themeStyle = Application.Current!.Styles[0]; - if (theme == CatalogTheme.FluentLight) - { - if (App.Fluent.Mode != FluentThemeMode.Light) - { - App.Fluent.Mode = FluentThemeMode.Light; - } - Application.Current.Styles[0] = App.Fluent; - Application.Current.Styles[1] = App.ColorPickerFluent; - Application.Current.Styles[2] = App.DataGridFluent; - } - else if (theme == CatalogTheme.FluentDark) - { - - if (App.Fluent.Mode != FluentThemeMode.Dark) - { - App.Fluent.Mode = FluentThemeMode.Dark; - } - Application.Current.Styles[0] = App.Fluent; - Application.Current.Styles[1] = App.ColorPickerFluent; - Application.Current.Styles[2] = App.DataGridFluent; - } - else if (theme == CatalogTheme.SimpleLight) - { - App.Simple.Mode = Avalonia.Themes.Simple.SimpleThemeMode.Light; - Application.Current.Styles[0] = App.SimpleLight; - Application.Current.Styles[1] = App.ColorPickerSimple; - Application.Current.Styles[2] = App.DataGridSimple; - } - else if (theme == CatalogTheme.SimpleDark) - { - App.Simple.Mode = Avalonia.Themes.Simple.SimpleThemeMode.Dark; - Application.Current.Styles[0] = App.SimpleDark; - Application.Current.Styles[1] = App.ColorPickerSimple; - Application.Current.Styles[2] = App.DataGridSimple; - } - } - }; - - var flowDirections = this.Get("FlowDirection"); - flowDirections.SelectionChanged += (sender, e) => - { - if (flowDirections.SelectedItem is FlowDirection flowDirection) - { - this.FlowDirection = flowDirection; - } - }; - - var decorations = this.Get("Decorations"); - decorations.SelectionChanged += (sender, e) => - { - if (VisualRoot is Window window - && decorations.SelectedItem is SystemDecorations systemDecorations) - { - window.SystemDecorations = systemDecorations; - } - }; - - var transparencyLevels = this.Get("TransparencyLevels"); - IDisposable? backgroundSetter = null, paneBackgroundSetter = null; - transparencyLevels.SelectionChanged += (sender, e) => - { - backgroundSetter?.Dispose(); - paneBackgroundSetter?.Dispose(); - if (transparencyLevels.SelectedItem is WindowTransparencyLevel selected - && selected != WindowTransparencyLevel.None) - { - var semiTransparentBrush = new ImmutableSolidColorBrush(Colors.Gray, 0.5); - backgroundSetter = sideBar.SetValue(BackgroundProperty, semiTransparentBrush, Avalonia.Data.BindingPriority.Style); - paneBackgroundSetter = sideBar.SetValue(SplitView.PaneBackgroundProperty, semiTransparentBrush, Avalonia.Data.BindingPriority.Style); - } - }; - } - - protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) - { - base.OnAttachedToVisualTree(e); - var decorations = this.Get("Decorations"); - if (VisualRoot is Window window) - decorations.SelectedIndex = (int)window.SystemDecorations; + Console.WriteLine("Main view loaded"); } } } From 25878fa83855bf68d82d70e74e41f784cc20a02c Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 23 Sep 2022 23:19:11 +0100 Subject: [PATCH 008/169] partially implement wasm backend initialisation. --- .../Avalonia.Web.Sample.csproj | 1 + src/Web/Avalonia.Web.Sample/Program.cs | 24 +- src/Web/Avalonia.Web/Avalonia.Web.csproj | 5 + src/Web/Avalonia.Web/AvaloniaRuntime.cs | 35 ++- src/Web/Avalonia.Web/AvaloniaView.cs | 236 ++++++++++++++++++ .../Avalonia.Web/BlazorSingleViewLifetime.cs | 46 ++++ src/Web/Avalonia.Web/BlazorSkiaGpu.cs | 26 ++ .../BlazorSkiaGpuRenderSession.cs | 37 +++ .../Avalonia.Web/BlazorSkiaGpuRenderTarget.cs | 39 +++ .../Avalonia.Web/BlazorSkiaRasterSurface.cs | 88 +++++++ src/Web/Avalonia.Web/BlazorSkiaSurface.cs | 30 +++ src/Web/Avalonia.Web/IBlazorSkiaSurface.cs | 9 + .../Avalonia.Web/ManualTriggerRenderTimer.cs | 18 ++ src/Web/Avalonia.Web/RazorViewTopLevelImpl.cs | 217 ++++++++++++++++ src/Web/Avalonia.Web/WinStubs.cs | 52 ++++ src/Web/Avalonia.Web/WindowingPlatform.cs | 110 ++++++++ .../webapp/modules/avalonia/runtime.ts | 5 - src/Web/Avalonia.Web/wwwroot/avalonia.js | 4 - 18 files changed, 948 insertions(+), 34 deletions(-) create mode 100644 src/Web/Avalonia.Web/AvaloniaView.cs create mode 100644 src/Web/Avalonia.Web/BlazorSingleViewLifetime.cs create mode 100644 src/Web/Avalonia.Web/BlazorSkiaGpu.cs create mode 100644 src/Web/Avalonia.Web/BlazorSkiaGpuRenderSession.cs create mode 100644 src/Web/Avalonia.Web/BlazorSkiaGpuRenderTarget.cs create mode 100644 src/Web/Avalonia.Web/BlazorSkiaRasterSurface.cs create mode 100644 src/Web/Avalonia.Web/BlazorSkiaSurface.cs create mode 100644 src/Web/Avalonia.Web/IBlazorSkiaSurface.cs create mode 100644 src/Web/Avalonia.Web/ManualTriggerRenderTimer.cs create mode 100644 src/Web/Avalonia.Web/RazorViewTopLevelImpl.cs create mode 100644 src/Web/Avalonia.Web/WinStubs.cs create mode 100644 src/Web/Avalonia.Web/WindowingPlatform.cs diff --git a/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj b/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj index 80be694741..1d03118993 100644 --- a/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj +++ b/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj @@ -12,6 +12,7 @@ + diff --git a/src/Web/Avalonia.Web.Sample/Program.cs b/src/Web/Avalonia.Web.Sample/Program.cs index 2d5bb6bec9..2a807b8cdf 100644 --- a/src/Web/Avalonia.Web.Sample/Program.cs +++ b/src/Web/Avalonia.Web.Sample/Program.cs @@ -4,7 +4,10 @@ using System.Runtime.InteropServices; using System.Runtime.InteropServices.JavaScript; using System.Threading.Tasks; using System.Xml.Linq; +using Avalonia; using Avalonia.Web; +using Avalonia.Web.Blazor; +using ControlCatalog; //using SkiaSharp; internal partial class Program @@ -15,24 +18,11 @@ internal partial class Program private static void Main(string[] args) { - Console.WriteLine("Hello, Browser!"); - - Console.WriteLine(); - - foreach(var arg in args) - { - Console.WriteLine(arg); - } - - var div = GetElementById("out"); - Console.WriteLine("got div"); - - var canvas = AvaloniaRuntime.CreateCanvas(div); - - Console.WriteLine("Created canvas"); - - AvaloniaRuntime.Foo(canvas); + BuildAvaloniaApp().UseBrowserWasm(); } + + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure(); } public partial class MyClass diff --git a/src/Web/Avalonia.Web/Avalonia.Web.csproj b/src/Web/Avalonia.Web/Avalonia.Web.csproj index 51b1d70173..13f311ba53 100644 --- a/src/Web/Avalonia.Web/Avalonia.Web.csproj +++ b/src/Web/Avalonia.Web/Avalonia.Web.csproj @@ -14,6 +14,11 @@ + + + + + true diff --git a/src/Web/Avalonia.Web/AvaloniaRuntime.cs b/src/Web/Avalonia.Web/AvaloniaRuntime.cs index e5c315af4c..2580f52666 100644 --- a/src/Web/Avalonia.Web/AvaloniaRuntime.cs +++ b/src/Web/Avalonia.Web/AvaloniaRuntime.cs @@ -1,11 +1,23 @@ using System; using System.Runtime.InteropServices; using System.Runtime.InteropServices.JavaScript; +using Avalonia.Controls; +using Avalonia.Controls.Platform; +using Avalonia.Input; +using Avalonia.Input.Platform; +using Avalonia.Platform; +using Avalonia.Rendering; +using Avalonia.Threading; +using Avalonia.Web.Blazor; namespace Avalonia.Web; + public partial class AvaloniaRuntime { + + + public record GLInfo(int ContextId, uint FboId, int Stencils, int Samples, int Depth); [DllImport("libSkiaSharp", CallingConvention = CallingConvention.Cdecl)] @@ -14,19 +26,26 @@ public partial class AvaloniaRuntime [JSExport] internal static void StartAvaloniaView(JSObject canvas) { - InterceptGLObject(); + // setup, get gl context... + + } + + public static GLInfo InitialiseGL (JSObject canvas) + { + InterceptGLObject(); + var info = InitGL(canvas, "testCanvas"); - + var glInfo = new GLInfo( - info.GetPropertyAsInt32("context"), - (uint)info.GetPropertyAsInt32("fboId"), - info.GetPropertyAsInt32("stencil"), - info.GetPropertyAsInt32("sample"), + info.GetPropertyAsInt32("context"), + (uint)info.GetPropertyAsInt32("fboId"), + info.GetPropertyAsInt32("stencil"), + info.GetPropertyAsInt32("sample"), info.GetPropertyAsInt32("depth")); - Console.WriteLine($"{glInfo.ContextId}, {glInfo.FboId}"); + return glInfo; } [JSImport("Canvas.createCanvas", "avalonia.ts")] @@ -36,7 +55,7 @@ public partial class AvaloniaRuntime public static partial void Foo(JSObject canvas); [JSImport("Canvas.initGL", "avalonia.ts")] - internal static partial JSObject InitGL(JSObject canvas, string canvasId); + private static partial JSObject InitGL(JSObject canvas, string canvasId); [JSImport("StorageProvider.isFileApiSupported", "storage.ts")] public static partial bool IsFileApiSupported(); diff --git a/src/Web/Avalonia.Web/AvaloniaView.cs b/src/Web/Avalonia.Web/AvaloniaView.cs new file mode 100644 index 0000000000..ec462893df --- /dev/null +++ b/src/Web/Avalonia.Web/AvaloniaView.cs @@ -0,0 +1,236 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Runtime.InteropServices.JavaScript; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Controls.Embedding; +using Avalonia.Controls.Platform; +using Avalonia.Input.TextInput; +using Avalonia.Platform.Storage; +using Avalonia.Rendering.Composition; +using Avalonia.Web.Blazor; +using SkiaSharp; +using static Avalonia.Web.AvaloniaRuntime; + +namespace Avalonia.Web +{ + public partial class AvaloniaView : ITextInputMethodImpl + { + [JSImport("globalThis.document.getElementById")] + internal static partial JSObject GetElementById(string id); + + private readonly RazorViewTopLevelImpl _topLevelImpl; + private EmbeddableControlRoot _topLevel; + + // Interop + /*private SKHtmlCanvasInterop? _interop = null; + private SizeWatcherInterop? _sizeWatcher = null; + private DpiWatcherInterop? _dpiWatcher = null;*/ + private GLInfo? _jsGlInfo = null; + /*private AvaloniaModule? _avaloniaModule = null; + private InputHelperInterop? _inputHelper = null; + private InputHelperInterop? _canvasHelper = null; + private InputHelperInterop? _containerHelper = null; + private NativeControlHostInterop? _nativeControlHost = null; + private StorageProviderInterop? _storageProvider = null; + private ElementReference _htmlCanvas; + private ElementReference _inputElement; + private ElementReference _containerElement; + private ElementReference _nativeControlsContainer;*/ + private double _dpi = 1; + private SKSize _canvasSize = new(100, 100); + + private GRContext? _context; + private GRGlInterface? _glInterface; + private const SKColorType ColorType = SKColorType.Rgba8888; + + private bool _useGL; + private bool _inputElementFocused; + + public AvaloniaView() + { + Console.WriteLine("In AvaloniaView"); + var div = GetElementById("out"); + Console.WriteLine("got div"); + + var canvas = AvaloniaRuntime.CreateCanvas(div); + canvas.SetProperty("id", "mycanvas"); + + AvaloniaRuntime.Foo(canvas); + + _topLevelImpl = new RazorViewTopLevelImpl(this); + + _topLevel = new EmbeddableControlRoot(_topLevelImpl); + + Console.WriteLine("created toplevel"); + + _topLevel.Prepare(); + + Console.WriteLine("Prepped"); + + _topLevel.Renderer.Start(); + + + + + //_interop = new SKHtmlCanvasInterop(_avaloniaModule, _htmlCanvas, OnRenderFrame); + + + var skiaOptions = AvaloniaLocator.Current.GetService(); + _useGL = skiaOptions?.CustomGpuFactory != null; + + if (_useGL) + { + _jsGlInfo = AvaloniaRuntime.InitialiseGL(canvas); + Console.WriteLine("jsglinfo created - init gl"); + } + else + { + throw new NotImplementedException(); + //var rasterInitialized = _interop.InitRaster(); + //Console.WriteLine("raster initialized: {0}", rasterInitialized); + } + + if (_useGL) + { + // create the SkiaSharp context + if (_context == null) + { + Console.WriteLine("create glcontext"); + _glInterface = GRGlInterface.Create(); + _context = GRContext.CreateGl(_glInterface); + + + // bump the default resource cache limit + _context.SetResourceCacheLimit(skiaOptions?.MaxGpuResourceSizeBytes ?? 32 * 1024 * 1024); + Console.WriteLine("glcontext created and resource limit set"); + } + + _topLevelImpl.Surfaces = new[] { new BlazorSkiaSurface(_context, _jsGlInfo, ColorType, new PixelSize(100, 100), 1, GRSurfaceOrigin.BottomLeft) }; + } + else + { + //_topLevelImpl.SetSurface(ColorType, + // new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi, _interop.PutImageData); + } + + // _interop.SetCanvasSize((int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); + + Threading.Dispatcher.UIThread.Post(async () => + { + //_interop.RequestAnimationFrame(true); + + // _sizeWatcher = new SizeWatcherInterop(_avaloniaModule, _htmlCanvas, OnSizeChanged); + // _dpiWatcher = new DpiWatcherInterop(_avaloniaModule, OnDpiChanged); + + //_sizeWatcher.Start(); + }); + } + + internal void SetSurface(SKColorType colorType, PixelSize size, double scaling, Action blitCallback) + { + //_currentSurface = new BlazorSkiaRasterSurface(colorType, size, scaling, blitCallback); + } + + public Control? Content + { + get => (Control)_topLevel.Content!; + set => _topLevel.Content = value; + } + + public bool KeyPreventDefault { get; set; } + + internal INativeControlHostImpl GetNativeControlHostImpl() + { + throw new NotImplementedException(); + //return _nativeControlHost ?? throw new InvalidOperationException("Blazor View wasn't initialized yet"); + } + + internal IStorageProvider GetStorageProvider() + { + throw new NotImplementedException(); + //return _storageProvider ?? throw new InvalidOperationException("Blazor View wasn't initialized yet"); + } + + private void OnRenderFrame() + { + if (_useGL && (_jsGlInfo == null)) + { + Console.WriteLine("nothing to render"); + return; + } + if (_canvasSize.Width <= 0 || _canvasSize.Height <= 0 || _dpi <= 0) + { + Console.WriteLine("nothing to render"); + return; + } + + ManualTriggerRenderTimer.Instance.RaiseTick(); + } + + private void ForceBlit() + { + // Note: this is technically a hack, but it's a kinda unique use case when + // we want to blit the previous frame + // renderer doesn't have much control over the render target + // we render on the UI thread + // We also don't want to have it as a meaningful public API. + // Therefore we have InternalsVisibleTo hack here. + + if (_topLevel.Renderer is CompositingRenderer dr) + { + //dr.CompositionTarget.ImmediateUIThreadRender(); + } + } + + private void OnDpiChanged(double newDpi) + { + if (Math.Abs(_dpi - newDpi) > 0.0001) + { + _dpi = newDpi; + + //_interop!.SetCanvasSize((int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); + + _topLevelImpl.SetClientSize(_canvasSize, _dpi); + + ForceBlit(); + } + } + + private void OnSizeChanged(SKSize newSize) + { + if (_canvasSize != newSize) + { + _canvasSize = newSize; + + //_interop!.SetCanvasSize((int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); + + _topLevelImpl.SetClientSize(_canvasSize, _dpi); + + ForceBlit(); + } + } + + public void SetClient(ITextInputMethodClient? client) + { + + } + + public void SetCursorRect(Rect rect) + { + } + + public void SetOptions(TextInputOptions options) + { + } + + public void Reset() + { + + } + } +} diff --git a/src/Web/Avalonia.Web/BlazorSingleViewLifetime.cs b/src/Web/Avalonia.Web/BlazorSingleViewLifetime.cs new file mode 100644 index 0000000000..04c1117510 --- /dev/null +++ b/src/Web/Avalonia.Web/BlazorSingleViewLifetime.cs @@ -0,0 +1,46 @@ +using System.Runtime.InteropServices.JavaScript; +using System; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Media; + +namespace Avalonia.Web.Blazor +{ + public class BlazorSingleViewLifetime : ISingleViewApplicationLifetime + { + public AvaloniaView? View; + + public Control? MainView + { + get => View!.Content; + set => View!.Content = value; + } + } + + public static partial class WebAppBuilder + { + + + public static T UseBrowserWasm( + this T builder) + where T : AppBuilderBase, new() + { + Console.WriteLine("In UseBrowserWasm"); + var lifetime = new BlazorSingleViewLifetime(); + + return builder + .UseWindowingSubsystem(BlazorWindowingPlatform.Register) + .UseSkia() + .With(new SkiaOptions { CustomGpuFactory = () => new BlazorSkiaGpu() }) + .AfterSetup(b => + { + var view = new AvaloniaView(); + lifetime.View = view; + Console.WriteLine("After setup"); + + }) + .SetupWithLifetime(lifetime); + + } + } +} diff --git a/src/Web/Avalonia.Web/BlazorSkiaGpu.cs b/src/Web/Avalonia.Web/BlazorSkiaGpu.cs new file mode 100644 index 0000000000..7191a00a66 --- /dev/null +++ b/src/Web/Avalonia.Web/BlazorSkiaGpu.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using Avalonia.Skia; + +namespace Avalonia.Web.Blazor +{ + public class BlazorSkiaGpu : ISkiaGpu + { + public ISkiaGpuRenderTarget? TryCreateRenderTarget(IEnumerable surfaces) + { + foreach (var surface in surfaces) + { + if (surface is BlazorSkiaSurface blazorSkiaSurface) + { + return new BlazorSkiaGpuRenderTarget(blazorSkiaSurface); + } + } + + return null; + } + + public ISkiaSurface? TryCreateSurface(PixelSize size, ISkiaGpuRenderSession session) + { + return null; + } + } +} diff --git a/src/Web/Avalonia.Web/BlazorSkiaGpuRenderSession.cs b/src/Web/Avalonia.Web/BlazorSkiaGpuRenderSession.cs new file mode 100644 index 0000000000..0c53825131 --- /dev/null +++ b/src/Web/Avalonia.Web/BlazorSkiaGpuRenderSession.cs @@ -0,0 +1,37 @@ +using Avalonia.Skia; +using SkiaSharp; + +namespace Avalonia.Web.Blazor +{ + internal class BlazorSkiaGpuRenderSession : ISkiaGpuRenderSession + { + private readonly SKSurface _surface; + + + public BlazorSkiaGpuRenderSession(BlazorSkiaSurface blazorSkiaSurface, GRBackendRenderTarget renderTarget) + { + _surface = SKSurface.Create(blazorSkiaSurface.Context, renderTarget, blazorSkiaSurface.Origin, blazorSkiaSurface.ColorType); + + GrContext = blazorSkiaSurface.Context; + + ScaleFactor = blazorSkiaSurface.Scaling; + + SurfaceOrigin = blazorSkiaSurface.Origin; + } + + public void Dispose() + { + _surface.Flush(); + + _surface.Dispose(); + } + + public GRContext GrContext { get; } + + public SKSurface SkSurface => _surface; + + public double ScaleFactor { get; } + + public GRSurfaceOrigin SurfaceOrigin { get; } + } +} diff --git a/src/Web/Avalonia.Web/BlazorSkiaGpuRenderTarget.cs b/src/Web/Avalonia.Web/BlazorSkiaGpuRenderTarget.cs new file mode 100644 index 0000000000..fa6a39f210 --- /dev/null +++ b/src/Web/Avalonia.Web/BlazorSkiaGpuRenderTarget.cs @@ -0,0 +1,39 @@ +using Avalonia.Skia; +using SkiaSharp; + +namespace Avalonia.Web.Blazor +{ + internal class BlazorSkiaGpuRenderTarget : ISkiaGpuRenderTarget + { + private readonly GRBackendRenderTarget _renderTarget; + private readonly BlazorSkiaSurface _blazorSkiaSurface; + private readonly PixelSize _size; + + public BlazorSkiaGpuRenderTarget(BlazorSkiaSurface blazorSkiaSurface) + { + _size = blazorSkiaSurface.Size; + + var glFbInfo = new GRGlFramebufferInfo(blazorSkiaSurface.GlInfo.FboId, blazorSkiaSurface.ColorType.ToGlSizedFormat()); + { + _blazorSkiaSurface = blazorSkiaSurface; + _renderTarget = new GRBackendRenderTarget( + (int)(blazorSkiaSurface.Size.Width * blazorSkiaSurface.Scaling), + (int)(blazorSkiaSurface.Size.Height * blazorSkiaSurface.Scaling), + blazorSkiaSurface.GlInfo.Samples, + blazorSkiaSurface.GlInfo.Stencils, glFbInfo); + } + } + + public void Dispose() + { + _renderTarget.Dispose(); + } + + public ISkiaGpuRenderSession BeginRenderingSession() + { + return new BlazorSkiaGpuRenderSession(_blazorSkiaSurface, _renderTarget); + } + + public bool IsCorrupted => _blazorSkiaSurface.Size != _size; + } +} diff --git a/src/Web/Avalonia.Web/BlazorSkiaRasterSurface.cs b/src/Web/Avalonia.Web/BlazorSkiaRasterSurface.cs new file mode 100644 index 0000000000..a1d58cdda0 --- /dev/null +++ b/src/Web/Avalonia.Web/BlazorSkiaRasterSurface.cs @@ -0,0 +1,88 @@ +using System; +using System.Runtime.InteropServices; +using Avalonia.Controls.Platform.Surfaces; +using Avalonia.Platform; +using Avalonia.Skia; +using SkiaSharp; + +namespace Avalonia.Web.Blazor +{ + internal class BlazorSkiaRasterSurface : IBlazorSkiaSurface, IFramebufferPlatformSurface, IDisposable + { + public SKColorType ColorType { get; set; } + + public PixelSize Size { get; set; } + + public double Scaling { get; set; } + + private FramebufferData? _fbData; + private readonly Action _blitCallback; + private readonly Action _onDisposeAction; + + public BlazorSkiaRasterSurface( + SKColorType colorType, PixelSize size, double scaling, Action blitCallback) + { + ColorType = colorType; + Size = size; + Scaling = scaling; + _blitCallback = blitCallback; + _onDisposeAction = Blit; + } + + public void Dispose() + { + _fbData?.Dispose(); + _fbData = null; + } + + public ILockedFramebuffer Lock() + { + var bytesPerPixel = 4; // TODO: derive from ColorType + var dpi = Scaling * 96.0; + var width = (int)(Size.Width * Scaling); + var height = (int)(Size.Height * Scaling); + + if (_fbData is null || _fbData?.Size.Width != width || _fbData?.Size.Height != height) + { + _fbData?.Dispose(); + _fbData = new FramebufferData(width, height, bytesPerPixel); + } + + var pixelFormat = ColorType.ToPixelFormat(); + var data = _fbData.Value; + return new LockedFramebuffer( + data.Address, data.Size, data.RowBytes, + new Vector(dpi, dpi), pixelFormat, _onDisposeAction); + } + + private void Blit() + { + if (_fbData != null) + { + var data = _fbData.Value; + _blitCallback(data.Address, new SKSizeI(data.Size.Width, data.Size.Height)); + } + } + + private readonly struct FramebufferData + { + public PixelSize Size { get; } + + public int RowBytes { get; } + + public IntPtr Address { get; } + + public FramebufferData(int width, int height, int bytesPerPixel) + { + Size = new PixelSize(width, height); + RowBytes = width * bytesPerPixel; + Address = Marshal.AllocHGlobal(width * height * bytesPerPixel); + } + + public void Dispose() + { + Marshal.FreeHGlobal(Address); + } + } + } +} diff --git a/src/Web/Avalonia.Web/BlazorSkiaSurface.cs b/src/Web/Avalonia.Web/BlazorSkiaSurface.cs new file mode 100644 index 0000000000..30dcddc66a --- /dev/null +++ b/src/Web/Avalonia.Web/BlazorSkiaSurface.cs @@ -0,0 +1,30 @@ +using SkiaSharp; +using static Avalonia.Web.AvaloniaRuntime; + +namespace Avalonia.Web.Blazor +{ + internal class BlazorSkiaSurface : IBlazorSkiaSurface + { + public BlazorSkiaSurface(GRContext context, GLInfo glInfo, SKColorType colorType, PixelSize size, double scaling, GRSurfaceOrigin origin) + { + Context = context; + GlInfo = glInfo; + ColorType = colorType; + Size = size; + Scaling = scaling; + Origin = origin; + } + + public SKColorType ColorType { get; set; } + + public PixelSize Size { get; set; } + + public GRContext Context { get; set; } + + public GRSurfaceOrigin Origin { get; set; } + + public double Scaling { get; set; } + + public GLInfo GlInfo { get; set; } + } +} diff --git a/src/Web/Avalonia.Web/IBlazorSkiaSurface.cs b/src/Web/Avalonia.Web/IBlazorSkiaSurface.cs new file mode 100644 index 0000000000..5463893e27 --- /dev/null +++ b/src/Web/Avalonia.Web/IBlazorSkiaSurface.cs @@ -0,0 +1,9 @@ +namespace Avalonia.Web.Blazor +{ + internal interface IBlazorSkiaSurface + { + public PixelSize Size { get; set; } + + public double Scaling { get; set; } + } +} diff --git a/src/Web/Avalonia.Web/ManualTriggerRenderTimer.cs b/src/Web/Avalonia.Web/ManualTriggerRenderTimer.cs new file mode 100644 index 0000000000..505b978a91 --- /dev/null +++ b/src/Web/Avalonia.Web/ManualTriggerRenderTimer.cs @@ -0,0 +1,18 @@ +using System; +using System.Diagnostics; +using Avalonia.Rendering; + +namespace Avalonia.Web.Blazor +{ + public class ManualTriggerRenderTimer : IRenderTimer + { + private static readonly Stopwatch s_sw = Stopwatch.StartNew(); + + public static ManualTriggerRenderTimer Instance { get; } = new(); + + public void RaiseTick() => Tick?.Invoke(s_sw.Elapsed); + + public event Action? Tick; + public bool RunsInBackground => false; + } +} diff --git a/src/Web/Avalonia.Web/RazorViewTopLevelImpl.cs b/src/Web/Avalonia.Web/RazorViewTopLevelImpl.cs new file mode 100644 index 0000000000..82f4064e1a --- /dev/null +++ b/src/Web/Avalonia.Web/RazorViewTopLevelImpl.cs @@ -0,0 +1,217 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Avalonia.Controls; +using Avalonia.Controls.Platform; +using Avalonia.Input; +using Avalonia.Input.Raw; +using Avalonia.Input.TextInput; +using Avalonia.Platform; +using Avalonia.Platform.Storage; +using Avalonia.Rendering; +using Avalonia.Rendering.Composition; +using SkiaSharp; +using static Avalonia.Web.AvaloniaRuntime; + +#nullable enable + +namespace Avalonia.Web.Blazor +{ + internal class RazorViewTopLevelImpl : ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost, ITopLevelImplWithStorageProvider + { + private Size _clientSize; + private IBlazorSkiaSurface? _currentSurface; + private IInputRoot? _inputRoot; + private readonly Stopwatch _sw = Stopwatch.StartNew(); + private readonly AvaloniaView _avaloniaView; + private readonly TouchDevice _touchDevice; + private readonly PenDevice _penDevice; + //private string _currentCursor = CssCursor.Default; + + public RazorViewTopLevelImpl(AvaloniaView avaloniaView) + { + Surfaces = Enumerable.Empty(); + _avaloniaView = avaloniaView; + TransparencyLevel = WindowTransparencyLevel.None; + AcrylicCompensationLevels = new AcrylicPlatformCompensationLevels(1, 1, 1); + _touchDevice = new TouchDevice(); + _penDevice = new PenDevice(); + } + + public ulong Timestamp => (ulong)_sw.ElapsedMilliseconds; + + + + + public void SetClientSize(SKSize size, double dpi) + { + var newSize = new Size(size.Width, size.Height); + + if (Math.Abs(RenderScaling - dpi) > 0.0001) + { + if (_currentSurface is { }) + { + _currentSurface.Scaling = dpi; + } + + ScalingChanged?.Invoke(dpi); + } + + if (newSize != _clientSize) + { + _clientSize = newSize; + + if (_currentSurface is { }) + { + _currentSurface.Size = new PixelSize((int)size.Width, (int)size.Height); + } + + Resized?.Invoke(newSize, PlatformResizeReason.User); + } + } + + public void RawPointerEvent( + RawPointerEventType eventType, string pointerType, + RawPointerPoint p, RawInputModifiers modifiers, long touchPointId) + { + if (_inputRoot is { } + && Input is { } input) + { + var device = GetPointerDevice(pointerType); + var args = device is TouchDevice ? + new RawTouchEventArgs(device, Timestamp, _inputRoot, eventType, p, modifiers, touchPointId) : + new RawPointerEventArgs(device, Timestamp, _inputRoot, eventType, p, modifiers) + { + RawPointerId = touchPointId + }; + + input.Invoke(args); + } + } + + private IPointerDevice GetPointerDevice(string pointerType) + { + return pointerType switch + { + "touch" => _touchDevice, + "pen" => _penDevice, + _ => MouseDevice + }; + } + + public void RawMouseWheelEvent(Point p, Vector v, RawInputModifiers modifiers) + { + if (_inputRoot is { }) + { + Input?.Invoke(new RawMouseWheelEventArgs(MouseDevice, Timestamp, _inputRoot, p, v, modifiers)); + } + } + + public bool RawKeyboardEvent(RawKeyEventType type, string code, string key, RawInputModifiers modifiers) + { + /*if (Keycodes.KeyCodes.TryGetValue(code, out var avkey)) + { + if (_inputRoot is { }) + { + var args = new RawKeyEventArgs(KeyboardDevice, Timestamp, _inputRoot, type, avkey, modifiers); + + Input?.Invoke(args); + + return args.Handled; + } + } + else if (Keycodes.KeyCodes.TryGetValue(key, out avkey)) + { + if (_inputRoot is { }) + { + var args = new RawKeyEventArgs(KeyboardDevice, Timestamp, _inputRoot, type, avkey, modifiers); + + Input?.Invoke(args); + + return args.Handled; + } + }*/ + + return false; + } + + public void RawTextEvent(string text) + { + if (_inputRoot is { }) + { + Input?.Invoke(new RawTextInputEventArgs(KeyboardDevice, Timestamp, _inputRoot, text)); + } + } + + public void Dispose() + { + + } + + public IRenderer CreateRenderer(IRenderRoot root) + { + var loop = AvaloniaLocator.Current.GetRequiredService(); + return new CompositingRenderer(root, new Compositor(loop, null)); + } + + public void Invalidate(Rect rect) + { + //Console.WriteLine("invalidate rect called"); + } + + public void SetInputRoot(IInputRoot inputRoot) + { + _inputRoot = inputRoot; + } + + public Point PointToClient(PixelPoint point) => new Point(point.X, point.Y); + + public PixelPoint PointToScreen(Point point) => new PixelPoint((int)point.X, (int)point.Y); + + public void SetCursor(ICursorImpl? cursor) + { + /* var val = (cursor as CssCursor)?.Value ?? CssCursor.Default; + if (_currentCursor != val) + { + SetCssCursor?.Invoke(val); + _currentCursor = val; + }*/ + } + + public IPopupImpl? CreatePopup() + { + return null; + } + + public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) + { + + } + + public Size ClientSize => _clientSize; + public Size? FrameSize => null; + public double RenderScaling => _currentSurface?.Scaling ?? 1; + + public IEnumerable Surfaces { get; set; } + + public Action? SetCssCursor { get; set; } + public Action? Input { get; set; } + public Action? Paint { get; set; } + public Action? Resized { get; set; } + public Action? ScalingChanged { get; set; } + public Action? TransparencyLevelChanged { get; set; } + public Action? Closed { get; set; } + public Action? LostFocus { get; set; } + public IMouseDevice MouseDevice { get; } = new MouseDevice(); + + public IKeyboardDevice KeyboardDevice { get; } = BlazorWindowingPlatform.Keyboard; + public WindowTransparencyLevel TransparencyLevel { get; } + public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } + + public ITextInputMethodImpl TextInputMethod => _avaloniaView; + + public INativeControlHostImpl? NativeControlHost => _avaloniaView.GetNativeControlHostImpl(); + public IStorageProvider StorageProvider => _avaloniaView.GetStorageProvider(); + } +} diff --git a/src/Web/Avalonia.Web/WinStubs.cs b/src/Web/Avalonia.Web/WinStubs.cs new file mode 100644 index 0000000000..e41f6ca29c --- /dev/null +++ b/src/Web/Avalonia.Web/WinStubs.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using System.IO; +using Avalonia.Controls; +using Avalonia.Controls.Platform; +using Avalonia.Input; +using Avalonia.Input.Platform; +using Avalonia.Platform; + +#nullable enable + +namespace Avalonia.Web.Blazor +{ + internal class IconLoaderStub : IPlatformIconLoader + { + private class IconStub : IWindowIconImpl + { + public void Save(Stream outputStream) + { + + } + } + + public IWindowIconImpl LoadIcon(string fileName) => new IconStub(); + + public IWindowIconImpl LoadIcon(Stream stream) => new IconStub(); + + public IWindowIconImpl LoadIcon(IBitmapImpl bitmap) => new IconStub(); + } + + internal class ScreenStub : IScreenImpl + { + public int ScreenCount => 1; + + public IReadOnlyList AllScreens { get; } = + new[] { new Screen(96, new PixelRect(0, 0, 4000, 4000), new PixelRect(0, 0, 4000, 4000), true) }; + + public Screen? ScreenFromPoint(PixelPoint point) + { + return ScreenHelper.ScreenFromPoint(point, AllScreens); + } + + public Screen? ScreenFromRect(PixelRect rect) + { + return ScreenHelper.ScreenFromRect(rect, AllScreens); + } + + public Screen? ScreenFromWindow(IWindowBaseImpl window) + { + return ScreenHelper.ScreenFromWindow(window, AllScreens); + } + } +} diff --git a/src/Web/Avalonia.Web/WindowingPlatform.cs b/src/Web/Avalonia.Web/WindowingPlatform.cs new file mode 100644 index 0000000000..4d52f54690 --- /dev/null +++ b/src/Web/Avalonia.Web/WindowingPlatform.cs @@ -0,0 +1,110 @@ +using System; +using System.Threading; +using Avalonia.Controls.Platform; +using Avalonia.Input; +using Avalonia.Input.Platform; +using Avalonia.Platform; +using Avalonia.Rendering; +using Avalonia.Threading; + +#nullable enable + +namespace Avalonia.Web.Blazor +{ + public class BlazorWindowingPlatform : IWindowingPlatform, IPlatformSettings, IPlatformThreadingInterface + { + private bool _signaled; + private static KeyboardDevice? s_keyboard; + + public IWindowImpl CreateWindow() => throw new NotSupportedException(); + + IWindowImpl IWindowingPlatform.CreateEmbeddableWindow() + { + throw new NotImplementedException(); + } + + public ITrayIconImpl? CreateTrayIcon() + { + return null; + } + + public static KeyboardDevice Keyboard => s_keyboard ?? + throw new InvalidOperationException("123 BlazorWindowingPlatform not registered."); + + public static void Register() + { + Console.WriteLine("Registering windowing"); + + var instance = new BlazorWindowingPlatform(); + s_keyboard = new KeyboardDevice(); + AvaloniaLocator.CurrentMutable + //.Bind().ToSingleton() + //.Bind().ToSingleton() + .Bind().ToConstant(s_keyboard) + .Bind().ToConstant(instance) + .Bind().ToConstant(instance) + .Bind().ToConstant(new RenderLoop()) + .Bind().ToConstant(ManualTriggerRenderTimer.Instance) + .Bind().ToConstant(instance) + .Bind().ToSingleton() + .Bind().ToSingleton(); + } + + public Size DoubleClickSize { get; } = new Size(2, 2); + + public TimeSpan DoubleClickTime { get; } = TimeSpan.FromMilliseconds(500); + + public Size TouchDoubleClickSize => new Size(16, 16); + + public TimeSpan TouchDoubleClickTime => DoubleClickTime; + public void RunLoop(CancellationToken cancellationToken) + { + throw new NotSupportedException(); + } + + public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick) + { + return GetRuntimePlatform() + .StartSystemTimer(interval, () => + { + Dispatcher.UIThread.RunJobs(priority); + tick(); + }); + } + + public void Signal(DispatcherPriority priority) + { + if (_signaled) + return; + + _signaled = true; + + IDisposable? disp = null; + + disp = GetRuntimePlatform() + .StartSystemTimer(TimeSpan.FromMilliseconds(1), + () => + { + _signaled = false; + disp?.Dispose(); + + Signaled?.Invoke(null); + }); + } + + public bool CurrentThreadIsLoopThread + { + get + { + return true; // Blazor is single threaded. + } + } + + public event Action? Signaled; + + private static IRuntimePlatform GetRuntimePlatform() + { + return AvaloniaLocator.Current.GetRequiredService(); + } + } +} diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts index 61e7b71797..bb97d0c9b7 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts @@ -11,9 +11,4 @@ export class AvaloniaRuntime { Canvas }); } - - createAvaloniaView(element: HTMLDivElement): void { - const canvas = document.createElement("canvas"); - element.appendChild(canvas); - } } diff --git a/src/Web/Avalonia.Web/wwwroot/avalonia.js b/src/Web/Avalonia.Web/wwwroot/avalonia.js index 896ad807ff..8c2721c600 100644 --- a/src/Web/Avalonia.Web/wwwroot/avalonia.js +++ b/src/Web/Avalonia.Web/wwwroot/avalonia.js @@ -109,10 +109,6 @@ var AvaloniaRuntime = class { Canvas }); } - createAvaloniaView(element) { - const canvas = document.createElement("canvas"); - element.appendChild(canvas); - } }; // modules/avalonia.ts From 94278e5c3a52071b820b0a5d5c2c03d295fe9ffd Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 23 Sep 2022 23:23:58 +0100 Subject: [PATCH 009/169] successful gl canvas init. --- src/Web/Avalonia.Web/AvaloniaView.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Web/Avalonia.Web/AvaloniaView.cs b/src/Web/Avalonia.Web/AvaloniaView.cs index ec462893df..f7af35d2b0 100644 --- a/src/Web/Avalonia.Web/AvaloniaView.cs +++ b/src/Web/Avalonia.Web/AvaloniaView.cs @@ -60,8 +60,6 @@ namespace Avalonia.Web var canvas = AvaloniaRuntime.CreateCanvas(div); canvas.SetProperty("id", "mycanvas"); - AvaloniaRuntime.Foo(canvas); - _topLevelImpl = new RazorViewTopLevelImpl(this); _topLevel = new EmbeddableControlRoot(_topLevelImpl); From 4b5885fab6f9662996fef949af75b2e8ffdd2764 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 23 Sep 2022 22:01:30 -0400 Subject: [PATCH 010/169] Add initial InputHelper --- src/Web/Avalonia.Web.Sample/index.html | 2 +- src/Web/Avalonia.Web/AvaloniaView.cs | 8 +- src/Web/Avalonia.Web/Interop/InputHelper.cs | 46 +++++++ src/Web/Avalonia.Web/Keycodes.cs | 129 ++++++++++++++++++ src/Web/Avalonia.Web/RazorViewTopLevelImpl.cs | 11 +- .../webapp/modules/avalonia/input.ts | 107 +++++++++++++++ .../webapp/modules/avalonia/runtime.ts | 4 +- 7 files changed, 298 insertions(+), 9 deletions(-) create mode 100644 src/Web/Avalonia.Web/Interop/InputHelper.cs create mode 100644 src/Web/Avalonia.Web/Keycodes.cs create mode 100644 src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts diff --git a/src/Web/Avalonia.Web.Sample/index.html b/src/Web/Avalonia.Web.Sample/index.html index 25b92798ad..518abd4a15 100644 --- a/src/Web/Avalonia.Web.Sample/index.html +++ b/src/Web/Avalonia.Web.Sample/index.html @@ -13,7 +13,7 @@ -
+
diff --git a/src/Web/Avalonia.Web/AvaloniaView.cs b/src/Web/Avalonia.Web/AvaloniaView.cs index f7af35d2b0..54652bc7e2 100644 --- a/src/Web/Avalonia.Web/AvaloniaView.cs +++ b/src/Web/Avalonia.Web/AvaloniaView.cs @@ -13,6 +13,8 @@ using Avalonia.Input.TextInput; using Avalonia.Platform.Storage; using Avalonia.Rendering.Composition; using Avalonia.Web.Blazor; +using Avalonia.Web.Interop; + using SkiaSharp; using static Avalonia.Web.AvaloniaRuntime; @@ -72,8 +74,10 @@ namespace Avalonia.Web _topLevel.Renderer.Start(); - - + InputHelper.SubscribeKeyboardEvents( + div, + (code, key, modifier) => _topLevelImpl.RawKeyboardEvent(Input.Raw.RawKeyEventType.KeyDown, code, key, (Input.RawInputModifiers)modifier), + (code, key, modifier) => _topLevelImpl.RawKeyboardEvent(Input.Raw.RawKeyEventType.KeyUp, code, key, (Input.RawInputModifiers)modifier)); //_interop = new SKHtmlCanvasInterop(_avaloniaModule, _htmlCanvas, OnRenderFrame); diff --git a/src/Web/Avalonia.Web/Interop/InputHelper.cs b/src/Web/Avalonia.Web/Interop/InputHelper.cs new file mode 100644 index 0000000000..6856a021f2 --- /dev/null +++ b/src/Web/Avalonia.Web/Interop/InputHelper.cs @@ -0,0 +1,46 @@ +using System; +using System.Runtime.InteropServices.JavaScript; + +namespace Avalonia.Web.Interop; + +internal static partial class InputHelper +{ + //[JSImport("InputHelper.subscribePointerEvents", "avalonia.ts")] + //public static partial void SubscribePointerEvents( + // JSObject htmlElement, + // [JSMarshalAs>] + // Func wheel); + + [JSImport("InputHelper.subscribeKeyboardEvents", "avalonia.ts")] + public static partial void SubscribeKeyboardEvents( + JSObject htmlElement, + [JSMarshalAs>] + Func keyDown, + [JSMarshalAs>] + Func keyUp); + + [JSImport("InputHelper.subscribeInputEvents", "avalonia.ts")] + public static partial void SubscribeInputEvents( + JSObject htmlElement, + [JSMarshalAs>] + Func input); + + + [JSImport("InputHelper.clearInput", "avalonia.ts")] + public static partial void ClearInputElement(JSObject htmlElement); + + [JSImport("InputHelper.isInputElement", "avalonia.ts")] + public static partial void IsInputElement(JSObject htmlElement); + + [JSImport("InputHelper.focusElement", "avalonia.ts")] + public static partial void FocusElement(JSObject htmlElement); + + [JSImport("InputHelper.setCursor", "avalonia.ts")] + public static partial void SetCursor(JSObject htmlElement, string kind); + + [JSImport("InputHelper.hide", "avalonia.ts")] + public static partial void HideElement(JSObject htmlElement); + + [JSImport("InputHelper.show", "avalonia.ts")] + public static partial void ShowElement(JSObject htmlElement); +} diff --git a/src/Web/Avalonia.Web/Keycodes.cs b/src/Web/Avalonia.Web/Keycodes.cs new file mode 100644 index 0000000000..d1185f6e45 --- /dev/null +++ b/src/Web/Avalonia.Web/Keycodes.cs @@ -0,0 +1,129 @@ +using System.Collections.Generic; + +using Avalonia.Input; + +namespace Avalonia.Web +{ + internal static class Keycodes + { + public static Dictionary KeyCodes = new() + { + { "Escape", Key.Escape }, + { "Digit1", Key.D1 }, + { "Digit2", Key.D2 }, + { "Digit3", Key.D3 }, + { "Digit4", Key.D4 }, + { "Digit5", Key.D5 }, + { "Digit6", Key.D6 }, + { "Digit7", Key.D7 }, + { "Digit8", Key.D8 }, + { "Digit9", Key.D9 }, + { "Digit0", Key.D0 }, + { "Minus", Key.OemMinus }, + //{ "Equal" , Key. }, + { "Backspace", Key.Back }, + { "Tab", Key.Tab }, + { "KeyQ", Key.Q }, + { "KeyW", Key.W }, + { "KeyE", Key.E }, + { "KeyR", Key.R }, + { "KeyT", Key.T }, + { "KeyY", Key.Y }, + { "KeyU", Key.U }, + { "KeyI", Key.I }, + { "KeyO", Key.O }, + { "KeyP", Key.P }, + { "BracketLeft", Key.OemOpenBrackets }, + { "BracketRight", Key.OemCloseBrackets }, + { "Enter", Key.Enter }, + { "ControlLeft", Key.LeftCtrl }, + { "KeyA", Key.A }, + { "KeyS", Key.S }, + { "KeyD", Key.D }, + { "KeyF", Key.F }, + { "KeyG", Key.G }, + { "KeyH", Key.H }, + { "KeyJ", Key.J }, + { "KeyK", Key.K }, + { "KeyL", Key.L }, + { "Semicolon", Key.OemSemicolon }, + { "Quote", Key.OemQuotes }, + //{ "Backquote" , Key. }, + { "ShiftLeft", Key.LeftShift }, + { "Backslash", Key.OemBackslash }, + { "KeyZ", Key.Z }, + { "KeyX", Key.X }, + { "KeyC", Key.C }, + { "KeyV", Key.V }, + { "KeyB", Key.B }, + { "KeyN", Key.N }, + { "KeyM", Key.M }, + { "Comma", Key.OemComma }, + { "Period", Key.OemPeriod }, + //{ "Slash" , Key. }, + { "ShiftRight", Key.RightShift }, + { "NumpadMultiply", Key.Multiply }, + { "AltLeft", Key.LeftAlt }, + { "Space", Key.Space }, + { "CapsLock", Key.CapsLock }, + { "F1", Key.F1 }, + { "F2", Key.F2 }, + { "F3", Key.F3 }, + { "F4", Key.F4 }, + { "F5", Key.F5 }, + { "F6", Key.F6 }, + { "F7", Key.F7 }, + { "F8", Key.F8 }, + { "F9", Key.F9 }, + { "F10", Key.F10 }, + { "NumLock", Key.NumLock }, + { "ScrollLock", Key.Scroll }, + { "Numpad7", Key.NumPad7 }, + { "Numpad8", Key.NumPad8 }, + { "Numpad9", Key.NumPad9 }, + { "NumpadSubtract", Key.Subtract }, + { "Numpad4", Key.NumPad4 }, + { "Numpad5", Key.NumPad5 }, + { "Numpad6", Key.NumPad6 }, + { "NumpadAdd", Key.Add }, + { "Numpad1", Key.NumPad1 }, + { "Numpad2", Key.NumPad2 }, + { "Numpad3", Key.NumPad3 }, + { "Numpad0", Key.NumPad0 }, + { "NumpadDecimal", Key.Decimal }, + { "Unidentified", Key.NoName }, + //{ "IntlBackslash" , Key.bac }, + { "F11", Key.F11 }, + { "F12", Key.F12 }, + //{ "IntlRo" , Key.Ro }, + //{ "Unidentified" , Key. }, + { "Convert", Key.ImeConvert }, + { "KanaMode", Key.KanaMode }, + { "NonConvert", Key.ImeNonConvert }, + //{ "Unidentified" , Key. }, + { "NumpadEnter", Key.Enter }, + { "ControlRight", Key.RightCtrl }, + { "NumpadDivide", Key.Divide }, + { "PrintScreen", Key.PrintScreen }, + { "AltRight", Key.RightAlt }, + //{ "Unidentified" , Key. }, + { "Home", Key.Home }, + { "ArrowUp", Key.Up }, + { "PageUp", Key.PageUp }, + { "ArrowLeft", Key.Left }, + { "ArrowRight", Key.Right }, + { "End", Key.End }, + { "ArrowDown", Key.Down }, + { "PageDown", Key.PageDown }, + { "Insert", Key.Insert }, + { "Delete", Key.Delete }, + //{ "Unidentified" , Key. }, + { "AudioVolumeMute", Key.VolumeMute }, + { "AudioVolumeDown", Key.VolumeDown }, + { "AudioVolumeUp", Key.VolumeUp }, + //{ "NumpadEqual" , Key. }, + { "Pause", Key.Pause }, + { "NumpadComma", Key.OemComma } + }; + } +} diff --git a/src/Web/Avalonia.Web/RazorViewTopLevelImpl.cs b/src/Web/Avalonia.Web/RazorViewTopLevelImpl.cs index 82f4064e1a..2bcc36e687 100644 --- a/src/Web/Avalonia.Web/RazorViewTopLevelImpl.cs +++ b/src/Web/Avalonia.Web/RazorViewTopLevelImpl.cs @@ -110,14 +110,15 @@ namespace Avalonia.Web.Blazor public bool RawKeyboardEvent(RawKeyEventType type, string code, string key, RawInputModifiers modifiers) { - /*if (Keycodes.KeyCodes.TryGetValue(code, out var avkey)) + Console.WriteLine($"{type} {code} {key} {modifiers}"); + if (Keycodes.KeyCodes.TryGetValue(code, out var avkey)) { if (_inputRoot is { }) { var args = new RawKeyEventArgs(KeyboardDevice, Timestamp, _inputRoot, type, avkey, modifiers); - + Input?.Invoke(args); - + return args.Handled; } } @@ -126,12 +127,12 @@ namespace Avalonia.Web.Blazor if (_inputRoot is { }) { var args = new RawKeyEventArgs(KeyboardDevice, Timestamp, _inputRoot, type, avkey, modifiers); - + Input?.Invoke(args); return args.Handled; } - }*/ + } return false; } diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts new file mode 100644 index 0000000000..a6734c8ce0 --- /dev/null +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts @@ -0,0 +1,107 @@ +enum RawInputModifiers +{ + None = 0, + Alt = 1, + Control = 2, + Shift = 4, + Meta = 8, + + LeftMouseButton = 16, + RightMouseButton = 32, + MiddleMouseButton = 64, + XButton1MouseButton = 128, + XButton2MouseButton = 256, + KeyboardMask = Alt | Control | Shift | Meta, + + PenInverted = 512, + PenEraser = 1024, + PenBarrelButton = 2048 +} + +export class InputHelper { + public static subscribeKeyboardEvents( + element: HTMLInputElement, + keyDownCallback: (code: string, key: string, modifiers: RawInputModifiers) => boolean, + keyUpCallback: (code: string, key: string, modifiers: RawInputModifiers) => boolean, + ) { + const keyDownHandler = (args: Event) => { + const keyArgs = args; + if (keyDownCallback(keyArgs.code, keyArgs.key, this.getModifiers(keyArgs))) { + args.preventDefault(); + } + }; + element.addEventListener("keydown", keyDownHandler); + + const keyUpHandler = (args: Event) => { + const keyArgs = args; + if (keyUpCallback(keyArgs.code, keyArgs.key, this.getModifiers(keyArgs))) { + args.preventDefault(); + } + }; + element.addEventListener("keyup", keyUpHandler); + + return () => { + element.removeEventListener("keydown", keyDownHandler); + element.removeEventListener("keyup", keyUpHandler); + }; + } + + public static subscribeInputEvents( + element: HTMLInputElement, + inputCallback: (value: string) => boolean + ) { + const inputHandler = (args: Event) => { + if (inputCallback((args as any).value)) { + args.preventDefault(); + } + }; + element.addEventListener("input", inputHandler); + + return () => { + element.removeEventListener("input", inputHandler); + }; + } + + public static clearInput(inputElement: HTMLInputElement) { + inputElement.value = ""; + } + + public static isInputElement(element: HTMLInputElement | HTMLElement): element is HTMLInputElement { + return (element as HTMLInputElement).setSelectionRange !== undefined; + } + + public static focusElement(inputElement: HTMLElement) { + inputElement.focus(); + + if (this.isInputElement(inputElement)) { + (inputElement as HTMLInputElement).setSelectionRange(0, 0); + } + } + + public static setCursor(inputElement: HTMLInputElement, kind: string) { + inputElement.style.cursor = kind; + } + + public static hide(inputElement: HTMLInputElement) { + inputElement.style.display = 'none'; + } + + public static show(inputElement: HTMLInputElement) { + inputElement.style.display = 'block'; + } + + private static getModifiers(args: KeyboardEvent): RawInputModifiers { + var modifiers = RawInputModifiers.None; + + if (args.ctrlKey) + modifiers |= RawInputModifiers.Control; + if (args.altKey) + modifiers |= RawInputModifiers.Alt; + if (args.shiftKey) + modifiers |= RawInputModifiers.Shift; + if (args.metaKey) + modifiers |= RawInputModifiers.Meta; + + return modifiers; + } +} diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts index bb97d0c9b7..2f154aeb3f 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts @@ -1,6 +1,7 @@ import { RuntimeAPI } from "../../types/dotnet"; import { Canvas } from "./canvas"; +import { InputHelper } from "./input"; export class AvaloniaRuntime { constructor( @@ -8,7 +9,8 @@ export class AvaloniaRuntime { api: RuntimeAPI ) { api.setModuleImports("avalonia.ts", { - Canvas + Canvas, + InputHelper }); } } From 8402e6b5f2f22e2bd571b9d9fe782d1bb5cc1373 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 23 Sep 2022 22:01:52 -0400 Subject: [PATCH 011/169] Delete generated files --- src/Web/Avalonia.Web/wwwroot/avalonia.js | 124 ----------------------- src/Web/Avalonia.Web/wwwroot/storage.js | 10 -- 2 files changed, 134 deletions(-) delete mode 100644 src/Web/Avalonia.Web/wwwroot/avalonia.js delete mode 100644 src/Web/Avalonia.Web/wwwroot/storage.js diff --git a/src/Web/Avalonia.Web/wwwroot/avalonia.js b/src/Web/Avalonia.Web/wwwroot/avalonia.js deleted file mode 100644 index 8c2721c600..0000000000 --- a/src/Web/Avalonia.Web/wwwroot/avalonia.js +++ /dev/null @@ -1,124 +0,0 @@ -var __async = (__this, __arguments, generator) => { - return new Promise((resolve, reject) => { - var fulfilled = (value) => { - try { - step(generator.next(value)); - } catch (e) { - reject(e); - } - }; - var rejected = (value) => { - try { - step(generator.throw(value)); - } catch (e) { - reject(e); - } - }; - var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected); - step((generator = generator.apply(__this, __arguments)).next()); - }); -}; - -// modules/avalonia/canvas.ts -var Canvas = class { - constructor(useGL, element) { - this.renderLoopEnabled = false; - this.renderLoopRequest = 0; - if (useGL) { - const ctx = Canvas.createWebGLContext(element); - if (!ctx) { - console.error(`Failed to create WebGL context: err ${ctx}`); - return; - } - var GL = globalThis.AvaloniaGL; - GL.makeContextCurrent(ctx); - var GLctx = GL.currentContext.GLctx; - const fbo = GLctx.getParameter(GLctx.FRAMEBUFFER_BINDING); - this.glInfo = { - context: ctx, - fboId: fbo ? fbo.id : 0, - stencil: GLctx.getParameter(GLctx.STENCIL_BITS), - sample: 0, - depth: GLctx.getParameter(GLctx.DEPTH_BITS) - }; - } - } - static createCanvas(element) { - var canvas = document.createElement("canvas"); - element.appendChild(canvas); - return canvas; - } - static initGL(element, elementId) { - console.log("inside initGL"); - var view = Canvas.init(true, element, elementId); - if (!view || !view.glInfo) - return null; - return view.glInfo; - } - static init(useGL, element, elementId) { - var htmlCanvas = element; - if (!htmlCanvas) { - console.error(`No canvas element was provided.`); - return null; - } - if (!Canvas.elements) - Canvas.elements = /* @__PURE__ */ new Map(); - Canvas.elements.set(elementId, element); - const view = new Canvas(useGL, element); - htmlCanvas.SKHtmlCanvas = view; - return view; - } - static Foo(canvas) { - const ctx = canvas.getContext("2d"); - ctx.fillStyle = "#FF0000"; - ctx.fillRect(0, 0, 150, 75); - } - static createWebGLContext(htmlCanvas) { - const contextAttributes = { - alpha: 1, - depth: 1, - stencil: 8, - antialias: 0, - premultipliedAlpha: 1, - preserveDrawingBuffer: 0, - preferLowPowerToHighPerformance: 0, - failIfMajorPerformanceCaveat: 0, - majorVersion: 2, - minorVersion: 0, - enableExtensionsByDefault: 1, - explicitSwapControl: 0, - renderViaOffscreenBackBuffer: 1 - }; - var GL = globalThis.AvaloniaGL; - let ctx = GL.createContext(htmlCanvas, contextAttributes); - if (!ctx && contextAttributes.majorVersion > 1) { - console.warn("Falling back to WebGL 1.0"); - contextAttributes.majorVersion = 1; - contextAttributes.minorVersion = 0; - ctx = GL.createContext(htmlCanvas, contextAttributes); - } - return ctx; - } -}; - -// modules/avalonia/runtime.ts -var AvaloniaRuntime = class { - constructor(dotnetAssembly, api) { - this.dotnetAssembly = dotnetAssembly; - api.setModuleImports("avalonia.ts", { - Canvas - }); - } -}; - -// modules/avalonia.ts -function createAvaloniaRuntime(api) { - return __async(this, null, function* () { - const dotnetAssembly = yield api.getAssemblyExports("Avalonia.Web.dll"); - return new AvaloniaRuntime(dotnetAssembly, api); - }); -} -export { - createAvaloniaRuntime -}; -//# sourceMappingURL=avalonia.js.map diff --git a/src/Web/Avalonia.Web/wwwroot/storage.js b/src/Web/Avalonia.Web/wwwroot/storage.js deleted file mode 100644 index a2042aaa11..0000000000 --- a/src/Web/Avalonia.Web/wwwroot/storage.js +++ /dev/null @@ -1,10 +0,0 @@ -// modules/storage.ts -var StorageProvider = class { - static isFileApiSupported() { - return globalThis.showOpenFilePicker !== void 0; - } -}; -export { - StorageProvider -}; -//# sourceMappingURL=storage.js.map From 03db5ac8986544c1b070fbbee3971baf52caa38c Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 23 Sep 2022 22:02:26 -0400 Subject: [PATCH 012/169] Ignore wwwroot of web project --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 84faae1806..61a3b53de1 100644 --- a/.gitignore +++ b/.gitignore @@ -215,3 +215,4 @@ src/Web/Avalonia.Web.Blazor/Interop/Typescript/*.js node_modules src/Web/Avalonia.Web.Blazor/webapp/package-lock.json src/Web/Avalonia.Web.Blazor/wwwroot +src/Web/Avalonia.Web/wwwroot From 722c05acc73e46604306c96c4efba5495b4c718d Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 23 Sep 2022 22:16:57 -0400 Subject: [PATCH 013/169] Add initial stream interop --- src/Web/Avalonia.Web/Interop/StreamHelper.cs | 55 +++++++ .../Storage/BlobReadableStream.cs | 100 +++++++++++++ .../Avalonia.Web/Storage/WriteableStream.cs | 136 ++++++++++++++++++ 3 files changed, 291 insertions(+) create mode 100644 src/Web/Avalonia.Web/Interop/StreamHelper.cs create mode 100644 src/Web/Avalonia.Web/Storage/BlobReadableStream.cs create mode 100644 src/Web/Avalonia.Web/Storage/WriteableStream.cs diff --git a/src/Web/Avalonia.Web/Interop/StreamHelper.cs b/src/Web/Avalonia.Web/Interop/StreamHelper.cs new file mode 100644 index 0000000000..c77d87fbe4 --- /dev/null +++ b/src/Web/Avalonia.Web/Interop/StreamHelper.cs @@ -0,0 +1,55 @@ +using System; +using System.Runtime.InteropServices.JavaScript; +using System.Threading.Tasks; + +namespace Avalonia.Web.Storage; + +/// +/// Set of FileSystemWritableFileStream and Blob methods. +/// +internal static partial class StreamHelper +{ + [JSImport("StreamHelper.seek", "avalonia.ts")] + public static partial void Seek(JSObject stream, [JSMarshalAs] long position); + + [JSImport("StreamHelper.truncate", "avalonia.ts")] + public static partial void Truncate(JSObject stream, [JSMarshalAs] long position); + + [JSImport("StreamHelper.write", "avalonia.ts")] + public static partial void Write(JSObject stream, [JSMarshalAs] Span data); + + [JSImport("StreamHelper.write", "avalonia.ts")] + public static partial Task WriteAsync(JSObject stream, [JSMarshalAs] ArraySegment data); + + [JSImport("StreamHelper.close", "avalonia.ts")] + public static partial void Close(JSObject stream); + + [JSImport("StreamHelper.close", "avalonia.ts")] + public static partial Task CloseAsync(JSObject stream); + + [JSImport("StreamHelper.size", "avalonia.ts")] + [return: JSMarshalAs] + public static partial long Size(JSObject stream); + + [JSImport("StreamHelper.byteLength", "avalonia.ts")] + [return: JSMarshalAs] + public static partial long ByteLength(JSObject stream); + + [JSImport("StreamHelper.sliceToArray", "avalonia.ts")] + [return: JSMarshalAs] + public static partial Span Slice(JSObject stream, [JSMarshalAs] long offset, int count); + + public static async Task> SliceAsync(JSObject stream, long offset, int count) + { + using var buffer = await SliceToBufferAsync(stream, offset, count); + return BufferToArray(buffer); + } + + [JSImport("StreamHelper.slice", "avalonia.ts")] + [return: JSMarshalAs>] + private static partial Task SliceToBufferAsync(JSObject stream, [JSMarshalAs] long offset, int count); + + [JSImport("StreamHelper.toArray", "avalonia.ts")] + [return: JSMarshalAs] + private static partial ArraySegment BufferToArray(JSObject stream); +} diff --git a/src/Web/Avalonia.Web/Storage/BlobReadableStream.cs b/src/Web/Avalonia.Web/Storage/BlobReadableStream.cs new file mode 100644 index 0000000000..6afc5e3054 --- /dev/null +++ b/src/Web/Avalonia.Web/Storage/BlobReadableStream.cs @@ -0,0 +1,100 @@ +using System; +using System.IO; +using System.Runtime.InteropServices.JavaScript; +using System.Threading; +using System.Threading.Tasks; + +namespace Avalonia.Web.Storage; + +internal class BlobReadableStream : Stream +{ + private JSObject? _jSReference; + private long _position; + private readonly long _length; + + public BlobReadableStream(JSObject jsStreamReference) + { + _jSReference = jsStreamReference; + _position = 0; + _length = StreamHelper.ByteLength(JSReference); + } + + private JSObject JSReference => _jSReference ?? throw new ObjectDisposedException(nameof(JSWriteableStream)); + + public override bool CanRead => true; + + public override bool CanSeek => false; + + public override bool CanWrite => false; + + public override long Length => _length; + + public override long Position + { + get => _position; + set => throw new NotSupportedException(); + } + + public override void Flush() { } + + public override long Seek(long offset, SeekOrigin origin) + { + return _position = origin switch + { + SeekOrigin.Current => _position + offset, + SeekOrigin.End => _length + offset, + _ => offset + }; + } + + public override void SetLength(long value) + => throw new NotSupportedException(); + + public override void Write(byte[] buffer, int offset, int count) + => throw new NotSupportedException(); + + public override int Read(byte[] buffer, int offset, int count) + => Read(buffer.AsSpan(offset, count)); + + public override int Read(Span buffer) + { + var numBytesToRead = (int)Math.Min(buffer.Length, Length - _position); + var bytesRead = StreamHelper.Slice(JSReference, _position, numBytesToRead); + if (bytesRead.Length != numBytesToRead) + { + throw new EndOfStreamException("Failed to read the requested number of bytes from the stream."); + } + + _position += bytesRead.Length; + bytesRead.CopyTo(buffer); + + return bytesRead.Length; + } + + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + => await ReadAsync(buffer.AsMemory(offset, count), cancellationToken); + + public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + var numBytesToRead = (int)Math.Min(buffer.Length, Length - _position); + var bytesRead = await StreamHelper.SliceAsync(JSReference, _position, numBytesToRead); + if (bytesRead.Count != numBytesToRead) + { + throw new EndOfStreamException("Failed to read the requested number of bytes from the stream."); + } + + _position += bytesRead.Count; + bytesRead.AsMemory().CopyTo(buffer); + + return bytesRead.Count; + } + + protected override void Dispose(bool disposing) + { + if (_jSReference is { } jsReference) + { + _jSReference = null; + jsReference.Dispose(); + } + } +} diff --git a/src/Web/Avalonia.Web/Storage/WriteableStream.cs b/src/Web/Avalonia.Web/Storage/WriteableStream.cs new file mode 100644 index 0000000000..4dce5a4c6c --- /dev/null +++ b/src/Web/Avalonia.Web/Storage/WriteableStream.cs @@ -0,0 +1,136 @@ +using System; +using System.IO; +using System.Runtime.InteropServices.JavaScript; +using System.Threading; +using System.Threading.Tasks; + +namespace Avalonia.Web.Storage; + +// Loose wrapper implementaion of a stream on top of FileAPI FileSystemWritableFileStream +internal sealed class JSWriteableStream : Stream +{ + private JSObject? _jSReference; + + // Unfortunatelly we can't read current length/position, so we need to keep it C#-side only. + private long _length, _position; + + internal JSWriteableStream(JSObject jSReference, long initialLength) + { + _jSReference = jSReference; + _length = initialLength; + } + + private JSObject JSReference => _jSReference ?? throw new ObjectDisposedException(nameof(JSWriteableStream)); + + public override bool CanRead => false; + + public override bool CanSeek => true; + + public override bool CanWrite => true; + + public override long Length => _length; + + public override long Position + { + get => _position; + set => Seek(_position, SeekOrigin.Begin); + } + + public override void Flush() + { + // no-op + } + + public override int Read(byte[] buffer, int offset, int count) + { + throw new NotSupportedException(); + } + + public override long Seek(long offset, SeekOrigin origin) + { + var position = origin switch + { + SeekOrigin.Current => _position + offset, + SeekOrigin.End => _length + offset, + _ => offset + }; + StreamHelper.Seek(JSReference, position); + return position; + } + + public override void SetLength(long value) + { + _length = value; + + // See https://docs.w3cub.com/dom/filesystemwritablefilestream/truncate + // If the offset is smaller than the size, it remains unchanged. If the offset is larger than size, the offset is set to that size + if (_position > _length) + { + _position = _length; + } + + StreamHelper.Truncate(JSReference, value); + } + + public override void Write(byte[] buffer, int offset, int count) + { + StreamHelper.Write(JSReference, buffer.AsSpan(offset, count)); + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + if (offset != 0 || count != buffer.Length) + { + // TODO, we need to pass prepared buffer to the JS + // Can't use ArrayPool as it can return bigger array than requested + // Can't use Span/Memory, as it's not supported by JS interop yet. + // Alternatively we can pass original buffer and offset+count, so it can be trimmed on the JS side (but is it more efficient tho?) + buffer = buffer.AsMemory(offset, count).ToArray(); + } + return WriteAsyncInternal(buffer, cancellationToken); + } + + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + return new ValueTask(WriteAsyncInternal(buffer.ToArray(), cancellationToken)); + } + + private Task WriteAsyncInternal(byte[] buffer, CancellationToken _) + { + _position += buffer.Length; + + return StreamHelper.WriteAsync(JSReference, buffer); + } + + protected override void Dispose(bool disposing) + { + if (_jSReference is { } jsReference) + { + _jSReference = null; + try + { + StreamHelper.Close(JSReference); + } + finally + { + jsReference.Dispose(); + } + } + } + + public override async ValueTask DisposeAsync() + { + if (_jSReference is { } jsReference) + { + _jSReference = null; + try + { + await StreamHelper.CloseAsync(JSReference); + } + finally + { + jsReference.Dispose(); + } + } + } +} From 77f06aa5299ce17f9eeb6ece6b56a898bf280517 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 24 Sep 2022 09:18:56 +0100 Subject: [PATCH 014/169] remove test function. --- src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts index ad39d58a8c..9698e72f82 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts @@ -89,13 +89,6 @@ export class Canvas { } } - - static Foo(canvas: HTMLCanvasElement) { - const ctx = canvas.getContext("2d")!; - ctx.fillStyle = "#FF0000"; - ctx.fillRect(0, 0, 150, 75); - } - static createWebGLContext(htmlCanvas: HTMLCanvasElement): WebGLRenderingContext | WebGL2RenderingContext { const contextAttributes = { alpha: 1, From e1a4609233a81c8739111fa398851214e50032fe Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 24 Sep 2022 10:15:12 +0100 Subject: [PATCH 015/169] render avalonia content. --- samples/ControlCatalog/MainView.xaml | 4 +- src/Web/Avalonia.Web/AvaloniaRuntime.cs | 17 ++- src/Web/Avalonia.Web/AvaloniaView.cs | 61 +++------- .../webapp/modules/avalonia/canvas.ts | 111 +++++++++++++++--- 4 files changed, 129 insertions(+), 64 deletions(-) diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 15cdb93896..55dec57b70 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -4,7 +4,7 @@ xmlns:controls="clr-namespace:ControlSamples;assembly=ControlSamples" xmlns:models="clr-namespace:ControlCatalog.Models" xmlns:pages="clr-namespace:ControlCatalog.Pages"> - - + + diff --git a/src/Web/Avalonia.Web/AvaloniaRuntime.cs b/src/Web/Avalonia.Web/AvaloniaRuntime.cs index 2580f52666..13b53f6cc4 100644 --- a/src/Web/Avalonia.Web/AvaloniaRuntime.cs +++ b/src/Web/Avalonia.Web/AvaloniaRuntime.cs @@ -31,11 +31,11 @@ public partial class AvaloniaRuntime } - public static GLInfo InitialiseGL (JSObject canvas) + public static GLInfo InitialiseGL (JSObject canvas, Action renderFrameCallback) { InterceptGLObject(); - var info = InitGL(canvas, "testCanvas"); + var info = InitGL(canvas, "testCanvas", renderFrameCallback); var glInfo = new GLInfo( @@ -48,14 +48,21 @@ public partial class AvaloniaRuntime return glInfo; } + [JSImport("Canvas.requestAnimationFrame", "avalonia.ts")] + public static partial void RequestAnimationFrame(JSObject canvas, bool renderLoop); + [JSImport("Canvas.createCanvas", "avalonia.ts")] public static partial JSObject CreateCanvas(JSObject container); - [JSImport("Canvas.Foo", "avalonia.ts")] - public static partial void Foo(JSObject canvas); + [JSImport("Canvas.setCanvasSize", "avalonia.ts")] + public static partial void SetCanvasSize(JSObject canvas, int height, int width); [JSImport("Canvas.initGL", "avalonia.ts")] - private static partial JSObject InitGL(JSObject canvas, string canvasId); + private static partial JSObject InitGL( + JSObject canvas, + string canvasId, + [JSMarshalAs] + Action renderFrameCallback); [JSImport("StorageProvider.isFileApiSupported", "storage.ts")] public static partial bool IsFileApiSupported(); diff --git a/src/Web/Avalonia.Web/AvaloniaView.cs b/src/Web/Avalonia.Web/AvaloniaView.cs index 54652bc7e2..2ee4655e40 100644 --- a/src/Web/Avalonia.Web/AvaloniaView.cs +++ b/src/Web/Avalonia.Web/AvaloniaView.cs @@ -1,12 +1,6 @@ using System; -using System.Collections.Generic; -using System.Drawing; -using System.Linq; using System.Runtime.InteropServices.JavaScript; -using System.Text; -using System.Threading.Tasks; using Avalonia.Controls; -using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.Embedding; using Avalonia.Controls.Platform; using Avalonia.Input.TextInput; @@ -14,7 +8,6 @@ using Avalonia.Platform.Storage; using Avalonia.Rendering.Composition; using Avalonia.Web.Blazor; using Avalonia.Web.Interop; - using SkiaSharp; using static Avalonia.Web.AvaloniaRuntime; @@ -55,23 +48,17 @@ namespace Avalonia.Web public AvaloniaView() { - Console.WriteLine("In AvaloniaView"); var div = GetElementById("out"); - Console.WriteLine("got div"); - var canvas = AvaloniaRuntime.CreateCanvas(div); + var canvas = CreateCanvas(div); canvas.SetProperty("id", "mycanvas"); _topLevelImpl = new RazorViewTopLevelImpl(this); _topLevel = new EmbeddableControlRoot(_topLevelImpl); - Console.WriteLine("created toplevel"); - _topLevel.Prepare(); - Console.WriteLine("Prepped"); - _topLevel.Renderer.Start(); InputHelper.SubscribeKeyboardEvents( @@ -87,7 +74,7 @@ namespace Avalonia.Web if (_useGL) { - _jsGlInfo = AvaloniaRuntime.InitialiseGL(canvas); + _jsGlInfo = AvaloniaRuntime.InitialiseGL(canvas, OnRenderFrame); Console.WriteLine("jsglinfo created - init gl"); } else @@ -120,22 +107,26 @@ namespace Avalonia.Web // new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi, _interop.PutImageData); } - // _interop.SetCanvasSize((int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); - - Threading.Dispatcher.UIThread.Post(async () => - { - //_interop.RequestAnimationFrame(true); + AvaloniaRuntime.SetCanvasSize(canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); - // _sizeWatcher = new SizeWatcherInterop(_avaloniaModule, _htmlCanvas, OnSizeChanged); - // _dpiWatcher = new DpiWatcherInterop(_avaloniaModule, OnDpiChanged); + _topLevelImpl.SetClientSize(_canvasSize, _dpi); - //_sizeWatcher.Start(); - }); + RequestAnimationFrame(canvas, true); } - - internal void SetSurface(SKColorType colorType, PixelSize size, double scaling, Action blitCallback) + private void OnRenderFrame() { - //_currentSurface = new BlazorSkiaRasterSurface(colorType, size, scaling, blitCallback); + if (_useGL && (_jsGlInfo == null)) + { + Console.WriteLine("nothing to render"); + return; + } + if (_canvasSize.Width <= 0 || _canvasSize.Height <= 0 || _dpi <= 0) + { + Console.WriteLine("nothing to render"); + return; + } + + ManualTriggerRenderTimer.Instance.RaiseTick(); } public Control? Content @@ -158,22 +149,6 @@ namespace Avalonia.Web //return _storageProvider ?? throw new InvalidOperationException("Blazor View wasn't initialized yet"); } - private void OnRenderFrame() - { - if (_useGL && (_jsGlInfo == null)) - { - Console.WriteLine("nothing to render"); - return; - } - if (_canvasSize.Width <= 0 || _canvasSize.Height <= 0 || _dpi <= 0) - { - Console.WriteLine("nothing to render"); - return; - } - - ManualTriggerRenderTimer.Instance.RaiseTick(); - } - private void ForceBlit() { // Note: this is technically a hack, but it's a kinda unique use case when diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts index 9698e72f82..a6e3c5acb4 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts @@ -8,16 +8,16 @@ type SKGLViewInfo = { depth: number; } -type SKHtmlCanvasElement = { - SKHtmlCanvas: Canvas | undefined +type CanvasElement = { + Canvas: Canvas | undefined } & HTMLCanvasElement export class Canvas { static elements: Map; - //htmlCanvas: HTMLCanvasElement; + htmlCanvas: HTMLCanvasElement; glInfo?: SKGLViewInfo; - //renderFrameCallback: DotNet.DotNetObject; + renderFrameCallback: () => void; renderLoopEnabled: boolean = false; renderLoopRequest: number = 0; newWidth?: number; @@ -31,17 +31,17 @@ export class Canvas { return canvas; } - public static initGL(element: HTMLCanvasElement, elementId: string): SKGLViewInfo | null { + public static initGL(element: HTMLCanvasElement, elementId: string, renderFrameCallback: () => void): SKGLViewInfo | null { console.log("inside initGL"); - var view = Canvas.init(true, element, elementId); + var view = Canvas.init(true, element, elementId, renderFrameCallback); if (!view || !view.glInfo) return null; return view.glInfo; } - static init(useGL: boolean, element: HTMLCanvasElement, elementId: string): Canvas | null { - var htmlCanvas = element as SKHtmlCanvasElement; + static init(useGL: boolean, element: HTMLCanvasElement, elementId: string, renderFrameCallback: () => void): Canvas | null { + var htmlCanvas = element as CanvasElement; if (!htmlCanvas) { console.error(`No canvas element was provided.`); return null; @@ -51,17 +51,16 @@ export class Canvas { Canvas.elements = new Map(); Canvas.elements.set(elementId, element); - const view = new Canvas(useGL, element); + const view = new Canvas(useGL, element, renderFrameCallback); - htmlCanvas.SKHtmlCanvas = view; + htmlCanvas.Canvas = view; return view; } - - public constructor(useGL: boolean, element: HTMLCanvasElement) { - //this.htmlCanvas = element; - //this.renderFrameCallback = callback; + public constructor(useGL: boolean, element: HTMLCanvasElement, renderFrameCallback: () => void) { + this.htmlCanvas = element; + this.renderFrameCallback = renderFrameCallback; if (useGL) { const ctx = Canvas.createWebGLContext(element); @@ -79,6 +78,7 @@ export class Canvas { // read values const fbo = GLctx.getParameter(GLctx.FRAMEBUFFER_BINDING); + this.glInfo = { context: ctx, fboId: fbo ? fbo.id : 0, @@ -89,6 +89,88 @@ export class Canvas { } } + public setEnableRenderLoop(enable: boolean) { + this.renderLoopEnabled = enable; + + // either start the new frame or cancel the existing one + if (enable) { + //console.info(`Enabling render loop with callback ${this.renderFrameCallback._id}...`); + this.requestAnimationFrame(); + } else if (this.renderLoopRequest !== 0) { + window.cancelAnimationFrame(this.renderLoopRequest); + this.renderLoopRequest = 0; + } + } + + public requestAnimationFrame(renderLoop?: boolean) { + // optionally update the render loop + if (renderLoop !== undefined && this.renderLoopEnabled !== renderLoop) + this.setEnableRenderLoop(renderLoop); + + // skip because we have a render loop + if (this.renderLoopRequest !== 0) + return; + + // add the draw to the next frame + this.renderLoopRequest = window.requestAnimationFrame(() => { + if (this.glInfo) { + var GL = (globalThis as any).AvaloniaGL; + // make current + GL.makeContextCurrent(this.glInfo.context); + } + + if (this.htmlCanvas.width != this.newWidth) { + this.htmlCanvas.width = this.newWidth || 0; + } + + if (this.htmlCanvas.height != this.newHeight) { + this.htmlCanvas.height = this.newHeight || 0; + } + + this.renderFrameCallback(); + this.renderLoopRequest = 0; + + // we may want to draw the next frame + if (this.renderLoopEnabled) + this.requestAnimationFrame(); + }); + } + + public setCanvasSize(width: number, height: number) { + this.newWidth = width; + this.newHeight = height; + + if (this.htmlCanvas.width != this.newWidth) { + this.htmlCanvas.width = this.newWidth; + } + + if (this.htmlCanvas.height != this.newHeight) { + this.htmlCanvas.height = this.newHeight; + } + + if (this.glInfo) { + var GL = (globalThis as any).AvaloniaGL; + // make current + GL.makeContextCurrent(this.glInfo.context); + } + } + + public static setCanvasSize(element: HTMLCanvasElement, width: number, height: number) { + const htmlCanvas = element as CanvasElement; + if (!htmlCanvas || !htmlCanvas.Canvas) + return; + + htmlCanvas.Canvas.setCanvasSize(width, height); + } + + public static requestAnimationFrame(element: HTMLCanvasElement, renderLoop?: boolean) { + const htmlCanvas = element as CanvasElement; + if (!htmlCanvas || !htmlCanvas.Canvas) + return; + + htmlCanvas.Canvas.requestAnimationFrame(renderLoop); + } + static createWebGLContext(htmlCanvas: HTMLCanvasElement): WebGLRenderingContext | WebGL2RenderingContext { const contextAttributes = { alpha: 1, @@ -109,6 +191,7 @@ export class Canvas { var GL = (globalThis as any).AvaloniaGL; let ctx: WebGLRenderingContext = GL.createContext(htmlCanvas, contextAttributes); + if (!ctx && contextAttributes.majorVersion > 1) { console.warn('Falling back to WebGL 1.0'); contextAttributes.majorVersion = 1; From 8d6050bf97fbe47044379ca4a7fea1ed96ab66e7 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 24 Sep 2022 11:34:52 +0100 Subject: [PATCH 016/169] implement dpi and size tracking. --- .../Avalonia.Web.Sample.csproj | 1 + src/Web/Avalonia.Web.Sample/css/style.css | 10 ++ src/Web/Avalonia.Web.Sample/index.html | 28 +++-- src/Web/Avalonia.Web/AvaloniaRuntime.cs | 12 ++ src/Web/Avalonia.Web/AvaloniaView.cs | 50 ++++---- src/Web/Avalonia.Web/RazorViewTopLevelImpl.cs | 15 +-- .../webapp/modules/avalonia/canvas.ts | 110 ++++++++++++++++++ .../webapp/modules/avalonia/runtime.ts | 6 +- 8 files changed, 188 insertions(+), 44 deletions(-) create mode 100644 src/Web/Avalonia.Web.Sample/css/style.css diff --git a/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj b/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj index 1d03118993..6446bc2618 100644 --- a/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj +++ b/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj @@ -19,6 +19,7 @@ + diff --git a/src/Web/Avalonia.Web.Sample/css/style.css b/src/Web/Avalonia.Web.Sample/css/style.css new file mode 100644 index 0000000000..4c8c72c29d --- /dev/null +++ b/src/Web/Avalonia.Web.Sample/css/style.css @@ -0,0 +1,10 @@ +.avalonia-canvas { + opacity: 1; + background-color: red; + position: fixed; + width: 100vw; + height: 100vh; + top: 0px; + left: 0px; + z-index: 500; +} diff --git a/src/Web/Avalonia.Web.Sample/index.html b/src/Web/Avalonia.Web.Sample/index.html index 518abd4a15..c67096b3d2 100644 --- a/src/Web/Avalonia.Web.Sample/index.html +++ b/src/Web/Avalonia.Web.Sample/index.html @@ -4,17 +4,29 @@ - Avalonia.Web.Sample - - - - - + Avalonia.Web.Sample + + + + + -
- + +
+ diff --git a/src/Web/Avalonia.Web/AvaloniaRuntime.cs b/src/Web/Avalonia.Web/AvaloniaRuntime.cs index 13b53f6cc4..c7c0f79cab 100644 --- a/src/Web/Avalonia.Web/AvaloniaRuntime.cs +++ b/src/Web/Avalonia.Web/AvaloniaRuntime.cs @@ -64,6 +64,18 @@ public partial class AvaloniaRuntime [JSMarshalAs] Action renderFrameCallback); + [JSImport("SizeWatcher.observe", "avalonia.ts")] + public static partial JSObject ObserveSize( + JSObject canvas, + string canvasId, + [JSMarshalAs>] + Action onSizeChanged); + + [JSImport("DpiWatcher.start", "avalonia.ts")] + public static partial double ObserveDpi( + [JSMarshalAs>] + Action onDpiChanged); + [JSImport("StorageProvider.isFileApiSupported", "storage.ts")] public static partial bool IsFileApiSupported(); diff --git a/src/Web/Avalonia.Web/AvaloniaView.cs b/src/Web/Avalonia.Web/AvaloniaView.cs index 2ee4655e40..de93614162 100644 --- a/src/Web/Avalonia.Web/AvaloniaView.cs +++ b/src/Web/Avalonia.Web/AvaloniaView.cs @@ -36,8 +36,9 @@ namespace Avalonia.Web private ElementReference _inputElement; private ElementReference _containerElement; private ElementReference _nativeControlsContainer;*/ + private JSObject _canvas; private double _dpi = 1; - private SKSize _canvasSize = new(100, 100); + private Size _canvasSize = new(100.0, 100.0); private GRContext? _context; private GRGlInterface? _glInterface; @@ -50,8 +51,8 @@ namespace Avalonia.Web { var div = GetElementById("out"); - var canvas = CreateCanvas(div); - canvas.SetProperty("id", "mycanvas"); + _canvas = CreateCanvas(div); + _canvas.SetProperty("id", "mycanvas"); _topLevelImpl = new RazorViewTopLevelImpl(this); @@ -66,26 +67,17 @@ namespace Avalonia.Web (code, key, modifier) => _topLevelImpl.RawKeyboardEvent(Input.Raw.RawKeyEventType.KeyDown, code, key, (Input.RawInputModifiers)modifier), (code, key, modifier) => _topLevelImpl.RawKeyboardEvent(Input.Raw.RawKeyEventType.KeyUp, code, key, (Input.RawInputModifiers)modifier)); - //_interop = new SKHtmlCanvasInterop(_avaloniaModule, _htmlCanvas, OnRenderFrame); + var skiaOptions = AvaloniaLocator.Current.GetService(); + _dpi = ObserveDpi(OnDpiChanged); - var skiaOptions = AvaloniaLocator.Current.GetService(); - _useGL = skiaOptions?.CustomGpuFactory != null; + Console.WriteLine($"Started observing dpi: {_dpi}"); - if (_useGL) - { - _jsGlInfo = AvaloniaRuntime.InitialiseGL(canvas, OnRenderFrame); - Console.WriteLine("jsglinfo created - init gl"); - } - else - { - throw new NotImplementedException(); - //var rasterInitialized = _interop.InitRaster(); - //Console.WriteLine("raster initialized: {0}", rasterInitialized); - } + _useGL = skiaOptions?.CustomGpuFactory != null; if (_useGL) { + _jsGlInfo = AvaloniaRuntime.InitialiseGL(_canvas, OnRenderFrame); // create the SkiaSharp context if (_context == null) { @@ -93,25 +85,29 @@ namespace Avalonia.Web _glInterface = GRGlInterface.Create(); _context = GRContext.CreateGl(_glInterface); - // bump the default resource cache limit _context.SetResourceCacheLimit(skiaOptions?.MaxGpuResourceSizeBytes ?? 32 * 1024 * 1024); Console.WriteLine("glcontext created and resource limit set"); } - _topLevelImpl.Surfaces = new[] { new BlazorSkiaSurface(_context, _jsGlInfo, ColorType, new PixelSize(100, 100), 1, GRSurfaceOrigin.BottomLeft) }; + _topLevelImpl.Surfaces = new[] { new BlazorSkiaSurface(_context, _jsGlInfo, ColorType, new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi, GRSurfaceOrigin.BottomLeft) }; } else { + //var rasterInitialized = _interop.InitRaster(); + //Console.WriteLine("raster initialized: {0}", rasterInitialized); + //_topLevelImpl.SetSurface(ColorType, - // new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi, _interop.PutImageData); + // new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi, _interop.PutImageData); } - AvaloniaRuntime.SetCanvasSize(canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); + AvaloniaRuntime.SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); _topLevelImpl.SetClientSize(_canvasSize, _dpi); - RequestAnimationFrame(canvas, true); + ObserveSize(_canvas, "mycanvas", OnSizeChanged); + + RequestAnimationFrame(_canvas, true); } private void OnRenderFrame() { @@ -164,13 +160,13 @@ namespace Avalonia.Web } } - private void OnDpiChanged(double newDpi) + private void OnDpiChanged(double oldDpi, double newDpi) { if (Math.Abs(_dpi - newDpi) > 0.0001) { _dpi = newDpi; - //_interop!.SetCanvasSize((int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); + SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); _topLevelImpl.SetClientSize(_canvasSize, _dpi); @@ -178,13 +174,15 @@ namespace Avalonia.Web } } - private void OnSizeChanged(SKSize newSize) + private void OnSizeChanged(int height, int width) { + var newSize = new Size(height, width); + if (_canvasSize != newSize) { _canvasSize = newSize; - //_interop!.SetCanvasSize((int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); + SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); _topLevelImpl.SetClientSize(_canvasSize, _dpi); diff --git a/src/Web/Avalonia.Web/RazorViewTopLevelImpl.cs b/src/Web/Avalonia.Web/RazorViewTopLevelImpl.cs index 2bcc36e687..3417dd18e6 100644 --- a/src/Web/Avalonia.Web/RazorViewTopLevelImpl.cs +++ b/src/Web/Avalonia.Web/RazorViewTopLevelImpl.cs @@ -21,7 +21,6 @@ namespace Avalonia.Web.Blazor internal class RazorViewTopLevelImpl : ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost, ITopLevelImplWithStorageProvider { private Size _clientSize; - private IBlazorSkiaSurface? _currentSurface; private IInputRoot? _inputRoot; private readonly Stopwatch _sw = Stopwatch.StartNew(); private readonly AvaloniaView _avaloniaView; @@ -44,15 +43,13 @@ namespace Avalonia.Web.Blazor - public void SetClientSize(SKSize size, double dpi) + public void SetClientSize(Size newSize, double dpi) { - var newSize = new Size(size.Width, size.Height); - if (Math.Abs(RenderScaling - dpi) > 0.0001) { - if (_currentSurface is { }) + if (Surfaces.FirstOrDefault() is BlazorSkiaSurface surface) { - _currentSurface.Scaling = dpi; + surface.Scaling = dpi; } ScalingChanged?.Invoke(dpi); @@ -62,9 +59,9 @@ namespace Avalonia.Web.Blazor { _clientSize = newSize; - if (_currentSurface is { }) + if (Surfaces.FirstOrDefault() is BlazorSkiaSurface surface) { - _currentSurface.Size = new PixelSize((int)size.Width, (int)size.Height); + surface.Size = new PixelSize((int)newSize.Width, (int)newSize.Height); } Resized?.Invoke(newSize, PlatformResizeReason.User); @@ -192,7 +189,7 @@ namespace Avalonia.Web.Blazor public Size ClientSize => _clientSize; public Size? FrameSize => null; - public double RenderScaling => _currentSurface?.Scaling ?? 1; + public double RenderScaling => (Surfaces.FirstOrDefault() as BlazorSkiaSurface)?.Scaling ?? 1; public IEnumerable Surfaces { get; set; } diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts index a6e3c5acb4..423689cde9 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts @@ -27,6 +27,7 @@ export class Canvas { var canvas = document.createElement("canvas"); element.appendChild(canvas); + canvas.classList.add('avalonia-canvas'); return canvas; } @@ -202,3 +203,112 @@ export class Canvas { return ctx; } } + +type SizeWatcherElement = { + SizeWatcher: SizeWatcherInstance; +} & HTMLElement + +type SizeWatcherInstance = { + callback: (width: number, height: number) => void; +} + +export class SizeWatcher { + static observer: ResizeObserver; + static elements: Map; + + public static observe(element: HTMLElement, elementId: string, callback: (width: number, height: number) => void) { + if (!element || !callback) + return; + + //console.info(`Adding size watcher observation with callback ${callback._id}...`); + + SizeWatcher.init(); + + const watcherElement = element as SizeWatcherElement; + watcherElement.SizeWatcher = { + callback: callback + }; + + SizeWatcher.elements.set(elementId, element); + SizeWatcher.observer.observe(element); + + SizeWatcher.invoke(element); + } + + public static unobserve(elementId: string) { + if (!elementId || !SizeWatcher.observer) + return; + + //console.info('Removing size watcher observation...'); + + const element = SizeWatcher.elements.get(elementId)!; + + SizeWatcher.elements.delete(elementId); + SizeWatcher.observer.unobserve(element); + } + + static init() { + if (SizeWatcher.observer) + return; + + //console.info('Starting size watcher...'); + + SizeWatcher.elements = new Map(); + SizeWatcher.observer = new ResizeObserver((entries) => { + for (let entry of entries) { + SizeWatcher.invoke(entry.target); + } + }); + } + + static invoke(element: Element) { + const watcherElement = element as SizeWatcherElement; + const instance = watcherElement.SizeWatcher; + + if (!instance || !instance.callback) + return; + + return instance.callback(element.clientWidth, element.clientHeight); + } +} + +export class DpiWatcher { + static lastDpi: number; + static timerId: number; + static callback: (old: number, newdpi: number) => void; + + public static getDpi() { + return window.devicePixelRatio; + } + + public static start(callback: (old: number, newdpi: number) => void) : number { + //console.info(`Starting DPI watcher with callback ${callback._id}...`); + + DpiWatcher.lastDpi = window.devicePixelRatio; + DpiWatcher.timerId = window.setInterval(DpiWatcher.update, 1000); + DpiWatcher.callback = callback; + + return DpiWatcher.lastDpi; + } + + public static stop() { + //console.info(`Stopping DPI watcher with callback ${DpiWatcher.callback._id}...`); + + window.clearInterval(DpiWatcher.timerId); + + //DpiWatcher.callback = undefined; + } + + static update() { + if (!DpiWatcher.callback) + return; + + const currentDpi = window.devicePixelRatio; + const lastDpi = DpiWatcher.lastDpi; + DpiWatcher.lastDpi = currentDpi; + + if (Math.abs(lastDpi - currentDpi) > 0.001) { + DpiWatcher.callback(lastDpi, currentDpi); + } + } +} diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts index 2f154aeb3f..d54583b15e 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts @@ -1,5 +1,7 @@ import { RuntimeAPI } from "../../types/dotnet"; +import { SizeWatcher } from "./canvas"; +import { DpiWatcher } from "./canvas"; import { Canvas } from "./canvas"; import { InputHelper } from "./input"; @@ -10,7 +12,9 @@ export class AvaloniaRuntime { ) { api.setModuleImports("avalonia.ts", { Canvas, - InputHelper + InputHelper, + SizeWatcher, + DpiWatcher }); } } From 19ea413e4e15cce34789e1faa4be6e3a34251287 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 24 Sep 2022 11:36:16 +0100 Subject: [PATCH 017/169] Revert "temporarilty trim down control catalog." This reverts commit 2a67b7950b79667e1bbe53ab6988b4de5158da25. # Conflicts: # samples/ControlCatalog/MainView.xaml --- .../ControlCatalog.NetCore.csproj | 2 +- samples/ControlCatalog/MainView.xaml | 217 +++++++++++++++++- samples/ControlCatalog/MainView.xaml.cs | 99 +++++++- src/Avalonia.Base/Properties/AssemblyInfo.cs | 1 + 4 files changed, 314 insertions(+), 5 deletions(-) diff --git a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj index 0667644643..32f199ea8b 100644 --- a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj +++ b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj @@ -2,7 +2,7 @@ WinExe - net6.0 + net7.0 true true diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 55dec57b70..7f5a191519 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -4,7 +4,218 @@ xmlns:controls="clr-namespace:ControlSamples;assembly=ControlSamples" xmlns:models="clr-namespace:ControlCatalog.Models" xmlns:pages="clr-namespace:ControlCatalog.Pages"> - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + None + BorderOnly + Full + + + + + FluentLight + FluentDark + SimpleLight + SimpleDark + + + + + None + Transparent + Blur + AcrylicBlur + Mica + + + + + LeftToRight + RightToLeft + + + + + + + + diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index 52dbd25c8c..7133ddaa6a 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -18,7 +18,104 @@ namespace ControlCatalog { AvaloniaXamlLoader.Load(this); - Console.WriteLine("Main view loaded"); + var sideBar = this.Get("Sidebar"); + + if (AvaloniaLocator.Current?.GetService()?.GetRuntimeInfo().IsDesktop == true) + { + var tabItems = (sideBar.Items as IList); + tabItems?.Add(new TabItem() + { + Header = "Screens", + Content = new ScreenPage() + }); + + } + + var themes = this.Get("Themes"); + themes.SelectionChanged += (sender, e) => + { + if (themes.SelectedItem is CatalogTheme theme) + { + var themeStyle = Application.Current!.Styles[0]; + if (theme == CatalogTheme.FluentLight) + { + if (App.Fluent.Mode != FluentThemeMode.Light) + { + App.Fluent.Mode = FluentThemeMode.Light; + } + Application.Current.Styles[0] = App.Fluent; + Application.Current.Styles[1] = App.ColorPickerFluent; + Application.Current.Styles[2] = App.DataGridFluent; + } + else if (theme == CatalogTheme.FluentDark) + { + + if (App.Fluent.Mode != FluentThemeMode.Dark) + { + App.Fluent.Mode = FluentThemeMode.Dark; + } + Application.Current.Styles[0] = App.Fluent; + Application.Current.Styles[1] = App.ColorPickerFluent; + Application.Current.Styles[2] = App.DataGridFluent; + } + else if (theme == CatalogTheme.SimpleLight) + { + App.Simple.Mode = Avalonia.Themes.Simple.SimpleThemeMode.Light; + Application.Current.Styles[0] = App.SimpleLight; + Application.Current.Styles[1] = App.ColorPickerSimple; + Application.Current.Styles[2] = App.DataGridSimple; + } + else if (theme == CatalogTheme.SimpleDark) + { + App.Simple.Mode = Avalonia.Themes.Simple.SimpleThemeMode.Dark; + Application.Current.Styles[0] = App.SimpleDark; + Application.Current.Styles[1] = App.ColorPickerSimple; + Application.Current.Styles[2] = App.DataGridSimple; + } + } + }; + + var flowDirections = this.Get("FlowDirection"); + flowDirections.SelectionChanged += (sender, e) => + { + if (flowDirections.SelectedItem is FlowDirection flowDirection) + { + this.FlowDirection = flowDirection; + } + }; + + var decorations = this.Get("Decorations"); + decorations.SelectionChanged += (sender, e) => + { + if (VisualRoot is Window window + && decorations.SelectedItem is SystemDecorations systemDecorations) + { + window.SystemDecorations = systemDecorations; + } + }; + + var transparencyLevels = this.Get("TransparencyLevels"); + IDisposable? backgroundSetter = null, paneBackgroundSetter = null; + transparencyLevels.SelectionChanged += (sender, e) => + { + backgroundSetter?.Dispose(); + paneBackgroundSetter?.Dispose(); + if (transparencyLevels.SelectedItem is WindowTransparencyLevel selected + && selected != WindowTransparencyLevel.None) + { + var semiTransparentBrush = new ImmutableSolidColorBrush(Colors.Gray, 0.5); + backgroundSetter = sideBar.SetValue(BackgroundProperty, semiTransparentBrush, Avalonia.Data.BindingPriority.Style); + paneBackgroundSetter = sideBar.SetValue(SplitView.PaneBackgroundProperty, semiTransparentBrush, Avalonia.Data.BindingPriority.Style); + } + }; + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + var decorations = this.Get("Decorations"); + if (VisualRoot is Window window) + decorations.SelectedIndex = (int)window.SystemDecorations; } } } diff --git a/src/Avalonia.Base/Properties/AssemblyInfo.cs b/src/Avalonia.Base/Properties/AssemblyInfo.cs index c8368e6d7a..d5e01087ef 100644 --- a/src/Avalonia.Base/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Base/Properties/AssemblyInfo.cs @@ -30,4 +30,5 @@ using Avalonia.Metadata; [assembly: InternalsVisibleTo("Avalonia.Skia.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("Avalonia.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("Avalonia.Web.Blazor, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] +[assembly: InternalsVisibleTo("Avalonia.Web, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] From 35ca12e9b1156563b15dca9484c917ca07ba8213 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 24 Sep 2022 11:42:17 +0100 Subject: [PATCH 018/169] add missing stubs. --- src/Web/Avalonia.Web/ClipboardImpl.cs | 35 +++++++++ src/Web/Avalonia.Web/Cursor.cs | 95 +++++++++++++++++++++++ src/Web/Avalonia.Web/WindowingPlatform.cs | 4 +- 3 files changed, 132 insertions(+), 2 deletions(-) create mode 100644 src/Web/Avalonia.Web/ClipboardImpl.cs create mode 100644 src/Web/Avalonia.Web/Cursor.cs diff --git a/src/Web/Avalonia.Web/ClipboardImpl.cs b/src/Web/Avalonia.Web/ClipboardImpl.cs new file mode 100644 index 0000000000..c8b2d74c83 --- /dev/null +++ b/src/Web/Avalonia.Web/ClipboardImpl.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Input; +using Avalonia.Input.Platform; + +namespace Avalonia.Web.Blazor +{ + internal class ClipboardImpl : IClipboard + { + public async Task GetTextAsync() + { + throw new NotImplementedException(); + //return await AvaloniaLocator.Current.GetRequiredService(). + // InvokeAsync("navigator.clipboard.readText"); + } + + public async Task SetTextAsync(string text) + { + throw new NotImplementedException(); + //await AvaloniaLocator.Current.GetRequiredService(). + // InvokeAsync("navigator.clipboard.writeText",text); + } + + public async Task ClearAsync() => await SetTextAsync(""); + + public Task SetDataObjectAsync(IDataObject data) => Task.CompletedTask; + + public Task GetFormatsAsync() => Task.FromResult(Array.Empty()); + + public Task GetDataAsync(string format) => Task.FromResult(new()); + } +} diff --git a/src/Web/Avalonia.Web/Cursor.cs b/src/Web/Avalonia.Web/Cursor.cs new file mode 100644 index 0000000000..843932bfde --- /dev/null +++ b/src/Web/Avalonia.Web/Cursor.cs @@ -0,0 +1,95 @@ +using System; +using System.IO; +using Avalonia.Input; +using Avalonia.Platform; + +namespace Avalonia.Web.Blazor +{ + public class CssCursor : ICursorImpl + { + public const string Default = "default"; + public string? Value { get; set; } + + public CssCursor(StandardCursorType type) + { + Value = ToKeyword(type); + } + + /// + /// Create a cursor from base64 image + /// + public CssCursor(string base64, string format, PixelPoint hotspot, StandardCursorType fallback) + { + Value = $"url(\"data:image/{format};base64,{base64}\") {hotspot.X} {hotspot.Y}, {ToKeyword(fallback)}"; + } + + /// + /// Create a cursor from url to *.cur file. + /// + public CssCursor(string url, StandardCursorType fallback) + { + Value = $"url('{url}'), {ToKeyword(fallback)}"; + } + + /// + /// Create a cursor from png/svg and hotspot position + /// + public CssCursor(string url, PixelPoint hotSpot, StandardCursorType fallback) + { + Value = $"url('{url}') {hotSpot.X} {hotSpot.Y}, {ToKeyword(fallback)}"; + } + + private static string ToKeyword(StandardCursorType type) => type switch + { + StandardCursorType.Hand => "pointer", + StandardCursorType.Cross => "crosshair", + StandardCursorType.Help => "help", + StandardCursorType.Ibeam => "text", + StandardCursorType.No => "not-allowed", + StandardCursorType.None => "none", + StandardCursorType.Wait => "progress", + StandardCursorType.AppStarting => "wait", + + StandardCursorType.DragMove => "move", + StandardCursorType.DragCopy => "copy", + StandardCursorType.DragLink => "alias", + + StandardCursorType.UpArrow => "default",/*not found matching one*/ + StandardCursorType.SizeWestEast => "ew-resize", + StandardCursorType.SizeNorthSouth => "ns-resize", + StandardCursorType.SizeAll => "move", + + StandardCursorType.TopSide => "n-resize", + StandardCursorType.BottomSide => "s-resize", + StandardCursorType.LeftSide => "w-resize", + StandardCursorType.RightSide => "e-resize", + StandardCursorType.TopLeftCorner => "nw-resize", + StandardCursorType.TopRightCorner => "ne-resize", + StandardCursorType.BottomLeftCorner => "sw-resize", + StandardCursorType.BottomRightCorner => "se-resize", + + _ => Default, + }; + + public void Dispose() {} + } + + internal class CssCursorFactory : ICursorFactory + { + public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) + { + using var imageStream = new MemoryStream(); + cursor.Save(imageStream); + + //not memory optimized because CryptoStream with ToBase64Transform is not supported in the browser. + var base64String = Convert.ToBase64String(imageStream.ToArray()); + return new CssCursor(base64String, "png", hotSpot, StandardCursorType.Arrow); + } + + ICursorImpl ICursorFactory.GetCursor(StandardCursorType cursorType) + { + return new CssCursor(cursorType); + } + } +} + diff --git a/src/Web/Avalonia.Web/WindowingPlatform.cs b/src/Web/Avalonia.Web/WindowingPlatform.cs index 4d52f54690..f9191926ad 100644 --- a/src/Web/Avalonia.Web/WindowingPlatform.cs +++ b/src/Web/Avalonia.Web/WindowingPlatform.cs @@ -38,8 +38,8 @@ namespace Avalonia.Web.Blazor var instance = new BlazorWindowingPlatform(); s_keyboard = new KeyboardDevice(); AvaloniaLocator.CurrentMutable - //.Bind().ToSingleton() - //.Bind().ToSingleton() + .Bind().ToSingleton() + .Bind().ToSingleton() .Bind().ToConstant(s_keyboard) .Bind().ToConstant(instance) .Bind().ToConstant(instance) From 31090e4989391377f44f04bf86e85e40d8073656 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 24 Sep 2022 13:30:59 +0100 Subject: [PATCH 019/169] implement mouse events. --- src/Web/Avalonia.Web/AvaloniaView.cs | 80 +++++++++++++++++++ src/Web/Avalonia.Web/Interop/InputHelper.cs | 14 ++++ .../webapp/modules/avalonia/input.ts | 40 ++++++++++ 3 files changed, 134 insertions(+) diff --git a/src/Web/Avalonia.Web/AvaloniaView.cs b/src/Web/Avalonia.Web/AvaloniaView.cs index de93614162..15c31ea0fd 100644 --- a/src/Web/Avalonia.Web/AvaloniaView.cs +++ b/src/Web/Avalonia.Web/AvaloniaView.cs @@ -3,6 +3,7 @@ using System.Runtime.InteropServices.JavaScript; using Avalonia.Controls; using Avalonia.Controls.Embedding; using Avalonia.Controls.Platform; +using Avalonia.Input.Raw; using Avalonia.Input.TextInput; using Avalonia.Platform.Storage; using Avalonia.Rendering.Composition; @@ -67,6 +68,85 @@ namespace Avalonia.Web (code, key, modifier) => _topLevelImpl.RawKeyboardEvent(Input.Raw.RawKeyEventType.KeyDown, code, key, (Input.RawInputModifiers)modifier), (code, key, modifier) => _topLevelImpl.RawKeyboardEvent(Input.Raw.RawKeyEventType.KeyUp, code, key, (Input.RawInputModifiers)modifier)); + InputHelper.SubscribePointerEvents(_canvas, args => + { + var type = args.GetPropertyAsString("pointertype"); + + var point = new RawPointerPoint + { + Position = new Point(args.GetPropertyAsDouble("clientX"), args.GetPropertyAsDouble("clientY")), + Pressure = (float)args.GetPropertyAsDouble("pressure"), + XTilt = (float)args.GetPropertyAsDouble("tiltX"), + YTilt = (float)args.GetPropertyAsDouble("tiltY") + // Twist = args.Twist - read from JS code directly when + }; + + _topLevelImpl.RawPointerEvent(Input.Raw.RawPointerEventType.Move, type!, point, Input.RawInputModifiers.None, args.GetPropertyAsInt32("pointerId")); + + return false; + + }, args => { + + var pointerType = args.GetPropertyAsString("pointerType"); + + var type = pointerType switch + { + "touch" => RawPointerEventType.TouchBegin, + _ => args.GetPropertyAsInt32("button") switch + { + 0 => RawPointerEventType.LeftButtonDown, + 1 => RawPointerEventType.MiddleButtonDown, + 2 => RawPointerEventType.RightButtonDown, + 3 => RawPointerEventType.XButton1Down, + 4 => RawPointerEventType.XButton2Down, + // 5 => Pen eraser button, + _ => RawPointerEventType.Move + } + }; + + var point = new RawPointerPoint + { + Position = new Point(args.GetPropertyAsDouble("clientX"), args.GetPropertyAsDouble("clientY")), + Pressure = (float)args.GetPropertyAsDouble("pressure"), + XTilt = (float)args.GetPropertyAsDouble("tiltX"), + YTilt = (float)args.GetPropertyAsDouble("tiltY") + // Twist = args.Twist - read from JS code directly when + }; + + _topLevelImpl.RawPointerEvent(type, pointerType!, point, Input.RawInputModifiers.None, args.GetPropertyAsInt32("pointerId")); + return false; + }, args => { + var pointerType = args.GetPropertyAsString("pointerType"); + + var type = pointerType switch + { + "touch" => RawPointerEventType.TouchEnd, + _ => args.GetPropertyAsInt32("button") switch + { + 0 => RawPointerEventType.LeftButtonUp, + 1 => RawPointerEventType.MiddleButtonUp, + 2 => RawPointerEventType.RightButtonUp, + 3 => RawPointerEventType.XButton1Up, + 4 => RawPointerEventType.XButton2Up, + // 5 => Pen eraser button, + _ => RawPointerEventType.Move + } + }; + + var point = new RawPointerPoint + { + Position = new Point(args.GetPropertyAsDouble("clientX"), args.GetPropertyAsDouble("clientY")), + Pressure = (float)args.GetPropertyAsDouble("pressure"), + XTilt = (float)args.GetPropertyAsDouble("tiltX"), + YTilt = (float)args.GetPropertyAsDouble("tiltY") + // Twist = args.Twist - read from JS code directly when + }; + + _topLevelImpl.RawPointerEvent(type, pointerType!, point, Input.RawInputModifiers.None, args.GetPropertyAsInt32("pointerId")); + return false; + }); + + var skiaOptions = AvaloniaLocator.Current.GetService(); _dpi = ObserveDpi(OnDpiChanged); diff --git a/src/Web/Avalonia.Web/Interop/InputHelper.cs b/src/Web/Avalonia.Web/Interop/InputHelper.cs index 6856a021f2..43cc0b84a2 100644 --- a/src/Web/Avalonia.Web/Interop/InputHelper.cs +++ b/src/Web/Avalonia.Web/Interop/InputHelper.cs @@ -19,6 +19,20 @@ internal static partial class InputHelper [JSMarshalAs>] Func keyUp); + + + + [JSImport("InputHelper.subscribePointerEvents", "avalonia.ts")] + public static partial void SubscribePointerEvents( + JSObject htmlElement, + [JSMarshalAs>] + Func pointerMove, + [JSMarshalAs>] + Func pointerDown, + [JSMarshalAs>] + Func pointerUp); + + [JSImport("InputHelper.subscribeInputEvents", "avalonia.ts")] public static partial void SubscribeInputEvents( JSObject htmlElement, diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts index a6734c8ce0..c27955ce11 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts @@ -45,6 +45,46 @@ export class InputHelper { element.removeEventListener("keyup", keyUpHandler); }; } + + public static subscribePointerEvents( + element: HTMLInputElement, + pointerMoveCallback: (args: PointerEvent) => boolean, + pointerDownCallback: (args: PointerEvent) => boolean, + pointerUpCallback: (args: PointerEvent) => boolean, + ) { + const pointerMoveHandler = (args: PointerEvent) => { + + if (pointerMoveCallback(args)) { + args.preventDefault(); + } + }; + + const pointerDownHandler = (args: PointerEvent) => { + + if (pointerDownCallback(args)) { + args.preventDefault(); + } + }; + + const pointerUpHandler = (args: PointerEvent) => { + + if (pointerUpCallback(args)) { + args.preventDefault(); + } + }; + + element.addEventListener("pointermove", pointerMoveHandler); + element.addEventListener("pointerdown", pointerDownHandler); + element.addEventListener("pointerup", pointerUpHandler); + + + + return () => { + element.removeEventListener("pointerover", pointerMoveHandler); + element.removeEventListener("pointerdown", pointerDownHandler); + element.removeEventListener("pointerup", pointerUpHandler); + }; + } public static subscribeInputEvents( element: HTMLInputElement, From c19797988cf0535e8c210bc868c5a15e6c839e2e Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 24 Sep 2022 19:11:59 +0100 Subject: [PATCH 020/169] allow any div to be turned into a toplevel. --- .../Avalonia.Web.Sample/Avalonia.Web.Sample.csproj | 9 ++++++++- src/Web/Avalonia.Web.Sample/Program.cs | 2 +- src/Web/Avalonia.Web/AvaloniaRuntime.cs | 2 +- src/Web/Avalonia.Web/AvaloniaView.cs | 7 ++++--- src/Web/Avalonia.Web/BlazorSingleViewLifetime.cs | 13 +++---------- 5 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj b/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj index 6446bc2618..68fa9c0003 100644 --- a/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj +++ b/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj @@ -7,10 +7,17 @@ true true true - -sVERBOSE + -sVERBOSE -sERROR_ON_UNDEFINED_SYMBOLS=0 --js-library="C:\Users\User\repos\Avalonia\src\Web\Avalonia.Web.Sample\interop.js" + + true + true + -O3 + -O3 + + diff --git a/src/Web/Avalonia.Web.Sample/Program.cs b/src/Web/Avalonia.Web.Sample/Program.cs index 2a807b8cdf..f101b2e13a 100644 --- a/src/Web/Avalonia.Web.Sample/Program.cs +++ b/src/Web/Avalonia.Web.Sample/Program.cs @@ -18,7 +18,7 @@ internal partial class Program private static void Main(string[] args) { - BuildAvaloniaApp().UseBrowserWasm(); + BuildAvaloniaApp().SetupBrowserApp("out1"); } public static AppBuilder BuildAvaloniaApp() diff --git a/src/Web/Avalonia.Web/AvaloniaRuntime.cs b/src/Web/Avalonia.Web/AvaloniaRuntime.cs index c7c0f79cab..a927b2f2ce 100644 --- a/src/Web/Avalonia.Web/AvaloniaRuntime.cs +++ b/src/Web/Avalonia.Web/AvaloniaRuntime.cs @@ -35,7 +35,7 @@ public partial class AvaloniaRuntime { InterceptGLObject(); - var info = InitGL(canvas, "testCanvas", renderFrameCallback); + var info = InitGL(canvas, canvas.GetPropertyAsString("id")!, renderFrameCallback); var glInfo = new GLInfo( diff --git a/src/Web/Avalonia.Web/AvaloniaView.cs b/src/Web/Avalonia.Web/AvaloniaView.cs index 15c31ea0fd..1aa5a2ef45 100644 --- a/src/Web/Avalonia.Web/AvaloniaView.cs +++ b/src/Web/Avalonia.Web/AvaloniaView.cs @@ -47,13 +47,14 @@ namespace Avalonia.Web private bool _useGL; private bool _inputElementFocused; + private static int _canvasCount; - public AvaloniaView() + public AvaloniaView(string divId) { - var div = GetElementById("out"); + var div = GetElementById(divId); _canvas = CreateCanvas(div); - _canvas.SetProperty("id", "mycanvas"); + _canvas.SetProperty("id", $"avaloniaCanvas{_canvasCount++}"); _topLevelImpl = new RazorViewTopLevelImpl(this); diff --git a/src/Web/Avalonia.Web/BlazorSingleViewLifetime.cs b/src/Web/Avalonia.Web/BlazorSingleViewLifetime.cs index 04c1117510..011413da1d 100644 --- a/src/Web/Avalonia.Web/BlazorSingleViewLifetime.cs +++ b/src/Web/Avalonia.Web/BlazorSingleViewLifetime.cs @@ -19,13 +19,10 @@ namespace Avalonia.Web.Blazor public static partial class WebAppBuilder { - - - public static T UseBrowserWasm( - this T builder) + public static T SetupBrowserApp( + this T builder, string mainDivId) where T : AppBuilderBase, new() { - Console.WriteLine("In UseBrowserWasm"); var lifetime = new BlazorSingleViewLifetime(); return builder @@ -34,13 +31,9 @@ namespace Avalonia.Web.Blazor .With(new SkiaOptions { CustomGpuFactory = () => new BlazorSkiaGpu() }) .AfterSetup(b => { - var view = new AvaloniaView(); - lifetime.View = view; - Console.WriteLine("After setup"); - + lifetime.View = new AvaloniaView(mainDivId); }) .SetupWithLifetime(lifetime); - } } } From c1a3974fb09ce2379291520ad818a60de85a95f8 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 24 Sep 2022 19:23:06 +0100 Subject: [PATCH 021/169] correctly style canvas to fill users div. --- src/Web/Avalonia.Web.Sample/Program.cs | 2 +- src/Web/Avalonia.Web.Sample/index.html | 17 ++++++++--------- src/Web/Avalonia.Web/AvaloniaView.cs | 5 +++++ .../webapp/modules/avalonia/canvas.ts | 2 ++ 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/Web/Avalonia.Web.Sample/Program.cs b/src/Web/Avalonia.Web.Sample/Program.cs index f101b2e13a..496fac2a7c 100644 --- a/src/Web/Avalonia.Web.Sample/Program.cs +++ b/src/Web/Avalonia.Web.Sample/Program.cs @@ -18,7 +18,7 @@ internal partial class Program private static void Main(string[] args) { - BuildAvaloniaApp().SetupBrowserApp("out1"); + BuildAvaloniaApp().SetupBrowserApp("out"); } public static AppBuilder BuildAvaloniaApp() diff --git a/src/Web/Avalonia.Web.Sample/index.html b/src/Web/Avalonia.Web.Sample/index.html index c67096b3d2..7a3c93eb34 100644 --- a/src/Web/Avalonia.Web.Sample/index.html +++ b/src/Web/Avalonia.Web.Sample/index.html @@ -14,15 +14,14 @@
diff --git a/src/Web/Avalonia.Web/AvaloniaView.cs b/src/Web/Avalonia.Web/AvaloniaView.cs index 1aa5a2ef45..14f4337f17 100644 --- a/src/Web/Avalonia.Web/AvaloniaView.cs +++ b/src/Web/Avalonia.Web/AvaloniaView.cs @@ -53,6 +53,11 @@ namespace Avalonia.Web { var div = GetElementById(divId); + if(div == null) + { + throw new Exception($"div with id: {divId}, was not found in the html document."); + } + _canvas = CreateCanvas(div); _canvas.SetProperty("id", $"avaloniaCanvas{_canvasCount++}"); diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts index 423689cde9..43b50294d7 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts @@ -25,6 +25,8 @@ export class Canvas { public static createCanvas(element: HTMLDivElement): HTMLCanvasElement { var canvas = document.createElement("canvas"); + canvas.style.setProperty("width", "100%"); + canvas.style.setProperty("height", "100%"); element.appendChild(canvas); canvas.classList.add('avalonia-canvas'); From 42e0cdce372dcb88bdae1bd560c9bba16165b23e Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 24 Sep 2022 19:26:25 +0100 Subject: [PATCH 022/169] remove logging. --- src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs | 1 - src/Web/Avalonia.Web/AvaloniaView.cs | 4 ---- src/Web/Avalonia.Web/WindowingPlatform.cs | 4 +--- src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts | 1 - 4 files changed, 1 insertion(+), 9 deletions(-) diff --git a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs index 87245a201a..333304895a 100644 --- a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs +++ b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs @@ -287,7 +287,6 @@ namespace Avalonia.Web.Blazor // create the SkiaSharp context if (_context == null) { - Console.WriteLine("create glcontext"); _glInterface = GRGlInterface.Create(); _context = GRContext.CreateGl(_glInterface); diff --git a/src/Web/Avalonia.Web/AvaloniaView.cs b/src/Web/Avalonia.Web/AvaloniaView.cs index 14f4337f17..c502edf203 100644 --- a/src/Web/Avalonia.Web/AvaloniaView.cs +++ b/src/Web/Avalonia.Web/AvaloniaView.cs @@ -157,8 +157,6 @@ namespace Avalonia.Web _dpi = ObserveDpi(OnDpiChanged); - Console.WriteLine($"Started observing dpi: {_dpi}"); - _useGL = skiaOptions?.CustomGpuFactory != null; if (_useGL) @@ -167,13 +165,11 @@ namespace Avalonia.Web // create the SkiaSharp context if (_context == null) { - Console.WriteLine("create glcontext"); _glInterface = GRGlInterface.Create(); _context = GRContext.CreateGl(_glInterface); // bump the default resource cache limit _context.SetResourceCacheLimit(skiaOptions?.MaxGpuResourceSizeBytes ?? 32 * 1024 * 1024); - Console.WriteLine("glcontext created and resource limit set"); } _topLevelImpl.Surfaces = new[] { new BlazorSkiaSurface(_context, _jsGlInfo, ColorType, new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi, GRSurfaceOrigin.BottomLeft) }; diff --git a/src/Web/Avalonia.Web/WindowingPlatform.cs b/src/Web/Avalonia.Web/WindowingPlatform.cs index f9191926ad..4dc200f58c 100644 --- a/src/Web/Avalonia.Web/WindowingPlatform.cs +++ b/src/Web/Avalonia.Web/WindowingPlatform.cs @@ -29,12 +29,10 @@ namespace Avalonia.Web.Blazor } public static KeyboardDevice Keyboard => s_keyboard ?? - throw new InvalidOperationException("123 BlazorWindowingPlatform not registered."); + throw new InvalidOperationException("BlazorWindowingPlatform not registered."); public static void Register() { - Console.WriteLine("Registering windowing"); - var instance = new BlazorWindowingPlatform(); s_keyboard = new KeyboardDevice(); AvaloniaLocator.CurrentMutable diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts index 43b50294d7..fb31eac7b1 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts @@ -35,7 +35,6 @@ export class Canvas { } public static initGL(element: HTMLCanvasElement, elementId: string, renderFrameCallback: () => void): SKGLViewInfo | null { - console.log("inside initGL"); var view = Canvas.init(true, element, elementId, renderFrameCallback); if (!view || !view.glInfo) return null; From 50cc71996c5641baf015ad0b63974e84ff964642 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 25 Sep 2022 02:02:55 -0400 Subject: [PATCH 023/169] Reorganize interop, remove "blazor" terminology and add some missing features --- .../Avalonia.Web.Sample.csproj | 4 +- src/Web/Avalonia.Web.Sample/Program.cs | 24 --- src/Web/Avalonia.Web.Sample/css/style.css | 10 -- src/Web/Avalonia.Web.Sample/index.html | 15 +- src/Web/Avalonia.Web/AvaloniaRuntime.cs | 83 ---------- src/Web/Avalonia.Web/AvaloniaView.cs | 143 ++++++++++++------ .../BlazorSkiaGpuRenderSession.cs | 37 ----- .../Avalonia.Web/BlazorSkiaGpuRenderTarget.cs | 39 ----- ...fetime.cs => BrowserSingleViewLifetime.cs} | 10 +- .../{BlazorSkiaGpu.cs => BrowserSkiaGpu.cs} | 8 +- .../BrowserSkiaGpuRenderSession.cs | 36 +++++ .../BrowserSkiaGpuRenderTarget.cs | 39 +++++ ...Surface.cs => BrowserSkiaRasterSurface.cs} | 6 +- ...orSkiaSurface.cs => BrowserSkiaSurface.cs} | 8 +- ...TopLevelImpl.cs => BrowserTopLevelImpl.cs} | 23 ++- src/Web/Avalonia.Web/ClipboardImpl.cs | 18 +-- src/Web/Avalonia.Web/Cursor.cs | 4 +- ...rSkiaSurface.cs => IBrowserSkiaSurface.cs} | 4 +- src/Web/Avalonia.Web/Interop/CanvasHelper.cs | 42 +++++ src/Web/Avalonia.Web/Interop/DomHelper.cs | 25 +++ src/Web/Avalonia.Web/Interop/InputHelper.cs | 21 +-- .../Avalonia.Web/ManualTriggerRenderTimer.cs | 2 +- src/Web/Avalonia.Web/WinStubs.cs | 6 +- src/Web/Avalonia.Web/WindowingPlatform.cs | 13 +- .../Avalonia.Web/webapp/modules/avalonia.ts | 19 ++- .../webapp/modules/avalonia/canvas.ts | 11 -- .../webapp/modules/avalonia/dom.ts | 51 +++++++ .../webapp/modules/avalonia/input.ts | 20 ++- .../webapp/modules/avalonia/runtime.ts | 20 --- 29 files changed, 373 insertions(+), 368 deletions(-) delete mode 100644 src/Web/Avalonia.Web.Sample/css/style.css delete mode 100644 src/Web/Avalonia.Web/AvaloniaRuntime.cs delete mode 100644 src/Web/Avalonia.Web/BlazorSkiaGpuRenderSession.cs delete mode 100644 src/Web/Avalonia.Web/BlazorSkiaGpuRenderTarget.cs rename src/Web/Avalonia.Web/{BlazorSingleViewLifetime.cs => BrowserSingleViewLifetime.cs} (76%) rename src/Web/Avalonia.Web/{BlazorSkiaGpu.cs => BrowserSkiaGpu.cs} (67%) create mode 100644 src/Web/Avalonia.Web/BrowserSkiaGpuRenderSession.cs create mode 100644 src/Web/Avalonia.Web/BrowserSkiaGpuRenderTarget.cs rename src/Web/Avalonia.Web/{BlazorSkiaRasterSurface.cs => BrowserSkiaRasterSurface.cs} (93%) rename src/Web/Avalonia.Web/{BlazorSkiaSurface.cs => BrowserSkiaSurface.cs} (65%) rename src/Web/Avalonia.Web/{RazorViewTopLevelImpl.cs => BrowserTopLevelImpl.cs} (88%) rename src/Web/Avalonia.Web/{IBlazorSkiaSurface.cs => IBrowserSkiaSurface.cs} (59%) create mode 100644 src/Web/Avalonia.Web/Interop/CanvasHelper.cs create mode 100644 src/Web/Avalonia.Web/Interop/DomHelper.cs create mode 100644 src/Web/Avalonia.Web/webapp/modules/avalonia/dom.ts delete mode 100644 src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts diff --git a/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj b/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj index 68fa9c0003..ca4d29aa21 100644 --- a/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj +++ b/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj @@ -14,6 +14,9 @@ true true + true + true + true -O3 -O3 @@ -26,7 +29,6 @@ - diff --git a/src/Web/Avalonia.Web.Sample/Program.cs b/src/Web/Avalonia.Web.Sample/Program.cs index 496fac2a7c..233f95852f 100644 --- a/src/Web/Avalonia.Web.Sample/Program.cs +++ b/src/Web/Avalonia.Web.Sample/Program.cs @@ -1,21 +1,9 @@ -using System; -using System.Reflection.Metadata; -using System.Runtime.InteropServices; -using System.Runtime.InteropServices.JavaScript; -using System.Threading.Tasks; -using System.Xml.Linq; using Avalonia; using Avalonia.Web; -using Avalonia.Web.Blazor; using ControlCatalog; -//using SkiaSharp; internal partial class Program { - - [JSImport("globalThis.document.getElementById")] - internal static partial JSObject GetElementById(string id); - private static void Main(string[] args) { BuildAvaloniaApp().SetupBrowserApp("out"); @@ -24,15 +12,3 @@ internal partial class Program public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure(); } - -public partial class MyClass -{ - [JSExport] - internal static async Task TestDynamicModule() - { - await JSHost.ImportAsync("storage.ts", "./storage.js"); - var fileApiSupported = AvaloniaRuntime.IsFileApiSupported(); - - Console.WriteLine("DynamicModule result: " + fileApiSupported); - } -} diff --git a/src/Web/Avalonia.Web.Sample/css/style.css b/src/Web/Avalonia.Web.Sample/css/style.css deleted file mode 100644 index 4c8c72c29d..0000000000 --- a/src/Web/Avalonia.Web.Sample/css/style.css +++ /dev/null @@ -1,10 +0,0 @@ -.avalonia-canvas { - opacity: 1; - background-color: red; - position: fixed; - width: 100vw; - height: 100vh; - top: 0px; - left: 0px; - z-index: 500; -} diff --git a/src/Web/Avalonia.Web.Sample/index.html b/src/Web/Avalonia.Web.Sample/index.html index 7a3c93eb34..8f6d402651 100644 --- a/src/Web/Avalonia.Web.Sample/index.html +++ b/src/Web/Avalonia.Web.Sample/index.html @@ -12,19 +12,8 @@ - - -
+ +
diff --git a/src/Web/Avalonia.Web/AvaloniaRuntime.cs b/src/Web/Avalonia.Web/AvaloniaRuntime.cs deleted file mode 100644 index a927b2f2ce..0000000000 --- a/src/Web/Avalonia.Web/AvaloniaRuntime.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using System.Runtime.InteropServices.JavaScript; -using Avalonia.Controls; -using Avalonia.Controls.Platform; -using Avalonia.Input; -using Avalonia.Input.Platform; -using Avalonia.Platform; -using Avalonia.Rendering; -using Avalonia.Threading; -using Avalonia.Web.Blazor; - -namespace Avalonia.Web; - - -public partial class AvaloniaRuntime -{ - - - - public record GLInfo(int ContextId, uint FboId, int Stencils, int Samples, int Depth); - - [DllImport("libSkiaSharp", CallingConvention = CallingConvention.Cdecl)] - static extern JSObject InterceptGLObject(); - - [JSExport] - internal static void StartAvaloniaView(JSObject canvas) - { - - // setup, get gl context... - - } - - public static GLInfo InitialiseGL (JSObject canvas, Action renderFrameCallback) - { - InterceptGLObject(); - - var info = InitGL(canvas, canvas.GetPropertyAsString("id")!, renderFrameCallback); - - - var glInfo = new GLInfo( - info.GetPropertyAsInt32("context"), - (uint)info.GetPropertyAsInt32("fboId"), - info.GetPropertyAsInt32("stencil"), - info.GetPropertyAsInt32("sample"), - info.GetPropertyAsInt32("depth")); - - return glInfo; - } - - [JSImport("Canvas.requestAnimationFrame", "avalonia.ts")] - public static partial void RequestAnimationFrame(JSObject canvas, bool renderLoop); - - [JSImport("Canvas.createCanvas", "avalonia.ts")] - public static partial JSObject CreateCanvas(JSObject container); - - [JSImport("Canvas.setCanvasSize", "avalonia.ts")] - public static partial void SetCanvasSize(JSObject canvas, int height, int width); - - [JSImport("Canvas.initGL", "avalonia.ts")] - private static partial JSObject InitGL( - JSObject canvas, - string canvasId, - [JSMarshalAs] - Action renderFrameCallback); - - [JSImport("SizeWatcher.observe", "avalonia.ts")] - public static partial JSObject ObserveSize( - JSObject canvas, - string canvasId, - [JSMarshalAs>] - Action onSizeChanged); - - [JSImport("DpiWatcher.start", "avalonia.ts")] - public static partial double ObserveDpi( - [JSMarshalAs>] - Action onDpiChanged); - - [JSImport("StorageProvider.isFileApiSupported", "storage.ts")] - public static partial bool IsFileApiSupported(); - -} - diff --git a/src/Web/Avalonia.Web/AvaloniaView.cs b/src/Web/Avalonia.Web/AvaloniaView.cs index c502edf203..7df8cb84e1 100644 --- a/src/Web/Avalonia.Web/AvaloniaView.cs +++ b/src/Web/Avalonia.Web/AvaloniaView.cs @@ -3,41 +3,27 @@ using System.Runtime.InteropServices.JavaScript; using Avalonia.Controls; using Avalonia.Controls.Embedding; using Avalonia.Controls.Platform; +using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Input.TextInput; using Avalonia.Platform.Storage; using Avalonia.Rendering.Composition; -using Avalonia.Web.Blazor; using Avalonia.Web.Interop; using SkiaSharp; -using static Avalonia.Web.AvaloniaRuntime; namespace Avalonia.Web { public partial class AvaloniaView : ITextInputMethodImpl { - [JSImport("globalThis.document.getElementById")] - internal static partial JSObject GetElementById(string id); - - private readonly RazorViewTopLevelImpl _topLevelImpl; + private readonly BrowserTopLevelImpl _topLevelImpl; private EmbeddableControlRoot _topLevel; - // Interop - /*private SKHtmlCanvasInterop? _interop = null; - private SizeWatcherInterop? _sizeWatcher = null; - private DpiWatcherInterop? _dpiWatcher = null;*/ + private readonly JSObject _containerElement; + private readonly JSObject _canvas; + private readonly JSObject _nativeControlsContainer; + private readonly JSObject _inputElement; + private GLInfo? _jsGlInfo = null; - /*private AvaloniaModule? _avaloniaModule = null; - private InputHelperInterop? _inputHelper = null; - private InputHelperInterop? _canvasHelper = null; - private InputHelperInterop? _containerHelper = null; - private NativeControlHostInterop? _nativeControlHost = null; - private StorageProviderInterop? _storageProvider = null; - private ElementReference _htmlCanvas; - private ElementReference _inputElement; - private ElementReference _containerElement; - private ElementReference _nativeControlsContainer;*/ - private JSObject _canvas; private double _dpi = 1; private Size _canvasSize = new(100.0, 100.0); @@ -51,30 +37,48 @@ namespace Avalonia.Web public AvaloniaView(string divId) { - var div = GetElementById(divId); + var host = DomHelper.GetElementById(divId); + if (host == null) + { + throw new Exception($"Element with id {divId} was not found in the html document."); + } - if(div == null) + var hostContent = DomHelper.CreateAvaloniaHost(host); + if (hostContent == null) { - throw new Exception($"div with id: {divId}, was not found in the html document."); + throw new InvalidOperationException("Avalonia WASM host wasn't initialized."); } - _canvas = CreateCanvas(div); + _containerElement = hostContent.GetPropertyAsJSObject("host") + ?? throw new InvalidOperationException("Host cannot be null"); + _canvas = hostContent.GetPropertyAsJSObject("canvas") + ?? throw new InvalidOperationException("Canvas cannot be null"); + _nativeControlsContainer = hostContent.GetPropertyAsJSObject("nativeHost") + ?? throw new InvalidOperationException("NativeHost cannot be null"); + _inputElement = hostContent.GetPropertyAsJSObject("inputElement") + ?? throw new InvalidOperationException("InputElement cannot be null"); + _canvas.SetProperty("id", $"avaloniaCanvas{_canvasCount++}"); - _topLevelImpl = new RazorViewTopLevelImpl(this); + _topLevelImpl = new BrowserTopLevelImpl(this); _topLevel = new EmbeddableControlRoot(_topLevelImpl); + _topLevelImpl.SetCssCursor = (cursor) => + { + InputHelper.SetCursor(_containerElement, cursor); // macOS + InputHelper.SetCursor(_canvas, cursor); // windows + }; _topLevel.Prepare(); _topLevel.Renderer.Start(); InputHelper.SubscribeKeyboardEvents( - div, - (code, key, modifier) => _topLevelImpl.RawKeyboardEvent(Input.Raw.RawKeyEventType.KeyDown, code, key, (Input.RawInputModifiers)modifier), - (code, key, modifier) => _topLevelImpl.RawKeyboardEvent(Input.Raw.RawKeyEventType.KeyUp, code, key, (Input.RawInputModifiers)modifier)); + _containerElement, + (code, key, modifier) => _topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyDown, code, key, (RawInputModifiers)modifier), + (code, key, modifier) => _topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyUp, code, key, (RawInputModifiers)modifier)); - InputHelper.SubscribePointerEvents(_canvas, args => + InputHelper.SubscribePointerEvents(_containerElement, args => { var type = args.GetPropertyAsString("pointertype"); @@ -83,11 +87,11 @@ namespace Avalonia.Web Position = new Point(args.GetPropertyAsDouble("clientX"), args.GetPropertyAsDouble("clientY")), Pressure = (float)args.GetPropertyAsDouble("pressure"), XTilt = (float)args.GetPropertyAsDouble("tiltX"), - YTilt = (float)args.GetPropertyAsDouble("tiltY") - // Twist = args.Twist - read from JS code directly when + YTilt = (float)args.GetPropertyAsDouble("tiltY"), + Twist = (float)args.GetPropertyAsDouble("twist") }; - _topLevelImpl.RawPointerEvent(Input.Raw.RawPointerEventType.Move, type!, point, Input.RawInputModifiers.None, args.GetPropertyAsInt32("pointerId")); + _topLevelImpl.RawPointerEvent(RawPointerEventType.Move, type!, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId")); return false; @@ -115,14 +119,14 @@ namespace Avalonia.Web Position = new Point(args.GetPropertyAsDouble("clientX"), args.GetPropertyAsDouble("clientY")), Pressure = (float)args.GetPropertyAsDouble("pressure"), XTilt = (float)args.GetPropertyAsDouble("tiltX"), - YTilt = (float)args.GetPropertyAsDouble("tiltY") - // Twist = args.Twist - read from JS code directly when + YTilt = (float)args.GetPropertyAsDouble("tiltY"), + Twist = (float)args.GetPropertyAsDouble("twist") }; - _topLevelImpl.RawPointerEvent(type, pointerType!, point, Input.RawInputModifiers.None, args.GetPropertyAsInt32("pointerId")); + _topLevelImpl.RawPointerEvent(type, pointerType!, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId")); return false; }, args => { - var pointerType = args.GetPropertyAsString("pointerType"); + var pointerType = args.GetPropertyAsString("pointerType") ?? "mouse"; var type = pointerType switch { @@ -144,24 +148,29 @@ namespace Avalonia.Web Position = new Point(args.GetPropertyAsDouble("clientX"), args.GetPropertyAsDouble("clientY")), Pressure = (float)args.GetPropertyAsDouble("pressure"), XTilt = (float)args.GetPropertyAsDouble("tiltX"), - YTilt = (float)args.GetPropertyAsDouble("tiltY") - // Twist = args.Twist - read from JS code directly when + YTilt = (float)args.GetPropertyAsDouble("tiltY"), + Twist = (float)args.GetPropertyAsDouble("twist") }; - _topLevelImpl.RawPointerEvent(type, pointerType!, point, Input.RawInputModifiers.None, args.GetPropertyAsInt32("pointerId")); + _topLevelImpl.RawPointerEvent(type, pointerType, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId")); + return false; + }, args => + { + _topLevelImpl.RawMouseWheelEvent(new Point(args.GetPropertyAsDouble("clientX"), args.GetPropertyAsDouble("clientY")), + new Vector(-(args.GetPropertyAsDouble("deltaX") / 50), -(args.GetPropertyAsDouble("deltaY") / 50)), GetModifiers(args)); return false; }); var skiaOptions = AvaloniaLocator.Current.GetService(); - _dpi = ObserveDpi(OnDpiChanged); + _dpi = DomHelper.ObserveDpi(OnDpiChanged); _useGL = skiaOptions?.CustomGpuFactory != null; if (_useGL) { - _jsGlInfo = AvaloniaRuntime.InitialiseGL(_canvas, OnRenderFrame); + _jsGlInfo = CanvasHelper.InitialiseGL(_canvas, OnRenderFrame); // create the SkiaSharp context if (_context == null) { @@ -172,7 +181,7 @@ namespace Avalonia.Web _context.SetResourceCacheLimit(skiaOptions?.MaxGpuResourceSizeBytes ?? 32 * 1024 * 1024); } - _topLevelImpl.Surfaces = new[] { new BlazorSkiaSurface(_context, _jsGlInfo, ColorType, new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi, GRSurfaceOrigin.BottomLeft) }; + _topLevelImpl.Surfaces = new[] { new BrowserSkiaSurface(_context, _jsGlInfo, ColorType, new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi, GRSurfaceOrigin.BottomLeft) }; } else { @@ -183,14 +192,50 @@ namespace Avalonia.Web // new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi, _interop.PutImageData); } - AvaloniaRuntime.SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); + CanvasHelper.SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); _topLevelImpl.SetClientSize(_canvasSize, _dpi); - ObserveSize(_canvas, "mycanvas", OnSizeChanged); + DomHelper.ObserveSize(_canvas, "mycanvas", OnSizeChanged); + + CanvasHelper.RequestAnimationFrame(_canvas, true); + } + + private static RawInputModifiers GetModifiers(JSObject e) + { + var modifiers = RawInputModifiers.None; + + if (e.GetPropertyAsBoolean("ctrlKey")) + modifiers |= RawInputModifiers.Control; + if (e.GetPropertyAsBoolean("altKey")) + modifiers |= RawInputModifiers.Alt; + if (e.GetPropertyAsBoolean("shiftKey")) + modifiers |= RawInputModifiers.Shift; + if (e.GetPropertyAsBoolean("metaKey")) + modifiers |= RawInputModifiers.Meta; + + var buttons = e.GetPropertyAsInt32("buttons"); + if ((buttons & 1L) == 1) + modifiers |= RawInputModifiers.LeftMouseButton; + + if ((buttons & 2L) == 2) + modifiers |= e.GetPropertyAsString("type") == "pen" ? RawInputModifiers.PenBarrelButton : RawInputModifiers.RightMouseButton; + + if ((buttons & 4L) == 4) + modifiers |= RawInputModifiers.MiddleMouseButton; - RequestAnimationFrame(_canvas, true); + if ((buttons & 8L) == 8) + modifiers |= RawInputModifiers.XButton1MouseButton; + + if ((buttons & 16L) == 16) + modifiers |= RawInputModifiers.XButton2MouseButton; + + if ((buttons & 32L) == 32) + modifiers |= RawInputModifiers.PenEraser; + + return modifiers; } + private void OnRenderFrame() { if (_useGL && (_jsGlInfo == null)) @@ -213,8 +258,6 @@ namespace Avalonia.Web set => _topLevel.Content = value; } - public bool KeyPreventDefault { get; set; } - internal INativeControlHostImpl GetNativeControlHostImpl() { throw new NotImplementedException(); @@ -248,7 +291,7 @@ namespace Avalonia.Web { _dpi = newDpi; - SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); + CanvasHelper.SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); _topLevelImpl.SetClientSize(_canvasSize, _dpi); @@ -264,7 +307,7 @@ namespace Avalonia.Web { _canvasSize = newSize; - SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); + CanvasHelper.SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); _topLevelImpl.SetClientSize(_canvasSize, _dpi); diff --git a/src/Web/Avalonia.Web/BlazorSkiaGpuRenderSession.cs b/src/Web/Avalonia.Web/BlazorSkiaGpuRenderSession.cs deleted file mode 100644 index 0c53825131..0000000000 --- a/src/Web/Avalonia.Web/BlazorSkiaGpuRenderSession.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Avalonia.Skia; -using SkiaSharp; - -namespace Avalonia.Web.Blazor -{ - internal class BlazorSkiaGpuRenderSession : ISkiaGpuRenderSession - { - private readonly SKSurface _surface; - - - public BlazorSkiaGpuRenderSession(BlazorSkiaSurface blazorSkiaSurface, GRBackendRenderTarget renderTarget) - { - _surface = SKSurface.Create(blazorSkiaSurface.Context, renderTarget, blazorSkiaSurface.Origin, blazorSkiaSurface.ColorType); - - GrContext = blazorSkiaSurface.Context; - - ScaleFactor = blazorSkiaSurface.Scaling; - - SurfaceOrigin = blazorSkiaSurface.Origin; - } - - public void Dispose() - { - _surface.Flush(); - - _surface.Dispose(); - } - - public GRContext GrContext { get; } - - public SKSurface SkSurface => _surface; - - public double ScaleFactor { get; } - - public GRSurfaceOrigin SurfaceOrigin { get; } - } -} diff --git a/src/Web/Avalonia.Web/BlazorSkiaGpuRenderTarget.cs b/src/Web/Avalonia.Web/BlazorSkiaGpuRenderTarget.cs deleted file mode 100644 index fa6a39f210..0000000000 --- a/src/Web/Avalonia.Web/BlazorSkiaGpuRenderTarget.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Avalonia.Skia; -using SkiaSharp; - -namespace Avalonia.Web.Blazor -{ - internal class BlazorSkiaGpuRenderTarget : ISkiaGpuRenderTarget - { - private readonly GRBackendRenderTarget _renderTarget; - private readonly BlazorSkiaSurface _blazorSkiaSurface; - private readonly PixelSize _size; - - public BlazorSkiaGpuRenderTarget(BlazorSkiaSurface blazorSkiaSurface) - { - _size = blazorSkiaSurface.Size; - - var glFbInfo = new GRGlFramebufferInfo(blazorSkiaSurface.GlInfo.FboId, blazorSkiaSurface.ColorType.ToGlSizedFormat()); - { - _blazorSkiaSurface = blazorSkiaSurface; - _renderTarget = new GRBackendRenderTarget( - (int)(blazorSkiaSurface.Size.Width * blazorSkiaSurface.Scaling), - (int)(blazorSkiaSurface.Size.Height * blazorSkiaSurface.Scaling), - blazorSkiaSurface.GlInfo.Samples, - blazorSkiaSurface.GlInfo.Stencils, glFbInfo); - } - } - - public void Dispose() - { - _renderTarget.Dispose(); - } - - public ISkiaGpuRenderSession BeginRenderingSession() - { - return new BlazorSkiaGpuRenderSession(_blazorSkiaSurface, _renderTarget); - } - - public bool IsCorrupted => _blazorSkiaSurface.Size != _size; - } -} diff --git a/src/Web/Avalonia.Web/BlazorSingleViewLifetime.cs b/src/Web/Avalonia.Web/BrowserSingleViewLifetime.cs similarity index 76% rename from src/Web/Avalonia.Web/BlazorSingleViewLifetime.cs rename to src/Web/Avalonia.Web/BrowserSingleViewLifetime.cs index 011413da1d..e50c11acd6 100644 --- a/src/Web/Avalonia.Web/BlazorSingleViewLifetime.cs +++ b/src/Web/Avalonia.Web/BrowserSingleViewLifetime.cs @@ -4,9 +4,9 @@ using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Media; -namespace Avalonia.Web.Blazor +namespace Avalonia.Web { - public class BlazorSingleViewLifetime : ISingleViewApplicationLifetime + public class BrowserSingleViewLifetime : ISingleViewApplicationLifetime { public AvaloniaView? View; @@ -23,12 +23,12 @@ namespace Avalonia.Web.Blazor this T builder, string mainDivId) where T : AppBuilderBase, new() { - var lifetime = new BlazorSingleViewLifetime(); + var lifetime = new BrowserSingleViewLifetime(); return builder - .UseWindowingSubsystem(BlazorWindowingPlatform.Register) + .UseWindowingSubsystem(BrowserWindowingPlatform.Register) .UseSkia() - .With(new SkiaOptions { CustomGpuFactory = () => new BlazorSkiaGpu() }) + .With(new SkiaOptions { CustomGpuFactory = () => new BrowserSkiaGpu() }) .AfterSetup(b => { lifetime.View = new AvaloniaView(mainDivId); diff --git a/src/Web/Avalonia.Web/BlazorSkiaGpu.cs b/src/Web/Avalonia.Web/BrowserSkiaGpu.cs similarity index 67% rename from src/Web/Avalonia.Web/BlazorSkiaGpu.cs rename to src/Web/Avalonia.Web/BrowserSkiaGpu.cs index 7191a00a66..20765b5449 100644 --- a/src/Web/Avalonia.Web/BlazorSkiaGpu.cs +++ b/src/Web/Avalonia.Web/BrowserSkiaGpu.cs @@ -1,17 +1,17 @@ using System.Collections.Generic; using Avalonia.Skia; -namespace Avalonia.Web.Blazor +namespace Avalonia.Web { - public class BlazorSkiaGpu : ISkiaGpu + public class BrowserSkiaGpu : ISkiaGpu { public ISkiaGpuRenderTarget? TryCreateRenderTarget(IEnumerable surfaces) { foreach (var surface in surfaces) { - if (surface is BlazorSkiaSurface blazorSkiaSurface) + if (surface is BrowserSkiaSurface browserSkiaSurface) { - return new BlazorSkiaGpuRenderTarget(blazorSkiaSurface); + return new BrowserSkiaGpuRenderTarget(browserSkiaSurface); } } diff --git a/src/Web/Avalonia.Web/BrowserSkiaGpuRenderSession.cs b/src/Web/Avalonia.Web/BrowserSkiaGpuRenderSession.cs new file mode 100644 index 0000000000..022ed20022 --- /dev/null +++ b/src/Web/Avalonia.Web/BrowserSkiaGpuRenderSession.cs @@ -0,0 +1,36 @@ +using Avalonia.Skia; +using SkiaSharp; + +namespace Avalonia.Web +{ + internal class BrowserSkiaGpuRenderSession : ISkiaGpuRenderSession + { + private readonly SKSurface _surface; + + public BrowserSkiaGpuRenderSession(BrowserSkiaSurface browserSkiaSurface, GRBackendRenderTarget renderTarget) + { + _surface = SKSurface.Create(browserSkiaSurface.Context, renderTarget, browserSkiaSurface.Origin, browserSkiaSurface.ColorType); + + GrContext = browserSkiaSurface.Context; + + ScaleFactor = browserSkiaSurface.Scaling; + + SurfaceOrigin = browserSkiaSurface.Origin; + } + + public void Dispose() + { + _surface.Flush(); + + _surface.Dispose(); + } + + public GRContext GrContext { get; } + + public SKSurface SkSurface => _surface; + + public double ScaleFactor { get; } + + public GRSurfaceOrigin SurfaceOrigin { get; } + } +} diff --git a/src/Web/Avalonia.Web/BrowserSkiaGpuRenderTarget.cs b/src/Web/Avalonia.Web/BrowserSkiaGpuRenderTarget.cs new file mode 100644 index 0000000000..644ebf19c8 --- /dev/null +++ b/src/Web/Avalonia.Web/BrowserSkiaGpuRenderTarget.cs @@ -0,0 +1,39 @@ +using Avalonia.Skia; +using SkiaSharp; + +namespace Avalonia.Web +{ + internal class BrowserSkiaGpuRenderTarget : ISkiaGpuRenderTarget + { + private readonly GRBackendRenderTarget _renderTarget; + private readonly BrowserSkiaSurface _browserSkiaSurface; + private readonly PixelSize _size; + + public BrowserSkiaGpuRenderTarget(BrowserSkiaSurface browserSkiaSurface) + { + _size = browserSkiaSurface.Size; + + var glFbInfo = new GRGlFramebufferInfo(browserSkiaSurface.GlInfo.FboId, browserSkiaSurface.ColorType.ToGlSizedFormat()); + { + _browserSkiaSurface = browserSkiaSurface; + _renderTarget = new GRBackendRenderTarget( + (int)(browserSkiaSurface.Size.Width * browserSkiaSurface.Scaling), + (int)(browserSkiaSurface.Size.Height * browserSkiaSurface.Scaling), + browserSkiaSurface.GlInfo.Samples, + browserSkiaSurface.GlInfo.Stencils, glFbInfo); + } + } + + public void Dispose() + { + _renderTarget.Dispose(); + } + + public ISkiaGpuRenderSession BeginRenderingSession() + { + return new BrowserSkiaGpuRenderSession(_browserSkiaSurface, _renderTarget); + } + + public bool IsCorrupted => _browserSkiaSurface.Size != _size; + } +} diff --git a/src/Web/Avalonia.Web/BlazorSkiaRasterSurface.cs b/src/Web/Avalonia.Web/BrowserSkiaRasterSurface.cs similarity index 93% rename from src/Web/Avalonia.Web/BlazorSkiaRasterSurface.cs rename to src/Web/Avalonia.Web/BrowserSkiaRasterSurface.cs index a1d58cdda0..0946e72d1d 100644 --- a/src/Web/Avalonia.Web/BlazorSkiaRasterSurface.cs +++ b/src/Web/Avalonia.Web/BrowserSkiaRasterSurface.cs @@ -5,9 +5,9 @@ using Avalonia.Platform; using Avalonia.Skia; using SkiaSharp; -namespace Avalonia.Web.Blazor +namespace Avalonia.Web { - internal class BlazorSkiaRasterSurface : IBlazorSkiaSurface, IFramebufferPlatformSurface, IDisposable + internal class BrowserSkiaRasterSurface : IBrowserSkiaSurface, IFramebufferPlatformSurface, IDisposable { public SKColorType ColorType { get; set; } @@ -19,7 +19,7 @@ namespace Avalonia.Web.Blazor private readonly Action _blitCallback; private readonly Action _onDisposeAction; - public BlazorSkiaRasterSurface( + public BrowserSkiaRasterSurface( SKColorType colorType, PixelSize size, double scaling, Action blitCallback) { ColorType = colorType; diff --git a/src/Web/Avalonia.Web/BlazorSkiaSurface.cs b/src/Web/Avalonia.Web/BrowserSkiaSurface.cs similarity index 65% rename from src/Web/Avalonia.Web/BlazorSkiaSurface.cs rename to src/Web/Avalonia.Web/BrowserSkiaSurface.cs index 30dcddc66a..6970cb1f22 100644 --- a/src/Web/Avalonia.Web/BlazorSkiaSurface.cs +++ b/src/Web/Avalonia.Web/BrowserSkiaSurface.cs @@ -1,11 +1,11 @@ +using Avalonia.Web.Interop; using SkiaSharp; -using static Avalonia.Web.AvaloniaRuntime; -namespace Avalonia.Web.Blazor +namespace Avalonia.Web { - internal class BlazorSkiaSurface : IBlazorSkiaSurface + internal class BrowserSkiaSurface : IBrowserSkiaSurface { - public BlazorSkiaSurface(GRContext context, GLInfo glInfo, SKColorType colorType, PixelSize size, double scaling, GRSurfaceOrigin origin) + public BrowserSkiaSurface(GRContext context, GLInfo glInfo, SKColorType colorType, PixelSize size, double scaling, GRSurfaceOrigin origin) { Context = context; GlInfo = glInfo; diff --git a/src/Web/Avalonia.Web/RazorViewTopLevelImpl.cs b/src/Web/Avalonia.Web/BrowserTopLevelImpl.cs similarity index 88% rename from src/Web/Avalonia.Web/RazorViewTopLevelImpl.cs rename to src/Web/Avalonia.Web/BrowserTopLevelImpl.cs index 3417dd18e6..f8ac2a17c0 100644 --- a/src/Web/Avalonia.Web/RazorViewTopLevelImpl.cs +++ b/src/Web/Avalonia.Web/BrowserTopLevelImpl.cs @@ -11,14 +11,12 @@ using Avalonia.Platform; using Avalonia.Platform.Storage; using Avalonia.Rendering; using Avalonia.Rendering.Composition; -using SkiaSharp; -using static Avalonia.Web.AvaloniaRuntime; #nullable enable -namespace Avalonia.Web.Blazor +namespace Avalonia.Web { - internal class RazorViewTopLevelImpl : ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost, ITopLevelImplWithStorageProvider + internal class BrowserTopLevelImpl : ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost, ITopLevelImplWithStorageProvider { private Size _clientSize; private IInputRoot? _inputRoot; @@ -26,9 +24,9 @@ namespace Avalonia.Web.Blazor private readonly AvaloniaView _avaloniaView; private readonly TouchDevice _touchDevice; private readonly PenDevice _penDevice; - //private string _currentCursor = CssCursor.Default; + private string _currentCursor = CssCursor.Default; - public RazorViewTopLevelImpl(AvaloniaView avaloniaView) + public BrowserTopLevelImpl(AvaloniaView avaloniaView) { Surfaces = Enumerable.Empty(); _avaloniaView = avaloniaView; @@ -47,7 +45,7 @@ namespace Avalonia.Web.Blazor { if (Math.Abs(RenderScaling - dpi) > 0.0001) { - if (Surfaces.FirstOrDefault() is BlazorSkiaSurface surface) + if (Surfaces.FirstOrDefault() is BrowserSkiaSurface surface) { surface.Scaling = dpi; } @@ -59,7 +57,7 @@ namespace Avalonia.Web.Blazor { _clientSize = newSize; - if (Surfaces.FirstOrDefault() is BlazorSkiaSurface surface) + if (Surfaces.FirstOrDefault() is BrowserSkiaSurface surface) { surface.Size = new PixelSize((int)newSize.Width, (int)newSize.Height); } @@ -107,7 +105,6 @@ namespace Avalonia.Web.Blazor public bool RawKeyboardEvent(RawKeyEventType type, string code, string key, RawInputModifiers modifiers) { - Console.WriteLine($"{type} {code} {key} {modifiers}"); if (Keycodes.KeyCodes.TryGetValue(code, out var avkey)) { if (_inputRoot is { }) @@ -169,12 +166,12 @@ namespace Avalonia.Web.Blazor public void SetCursor(ICursorImpl? cursor) { - /* var val = (cursor as CssCursor)?.Value ?? CssCursor.Default; + var val = (cursor as CssCursor)?.Value ?? CssCursor.Default; if (_currentCursor != val) { SetCssCursor?.Invoke(val); _currentCursor = val; - }*/ + } } public IPopupImpl? CreatePopup() @@ -189,7 +186,7 @@ namespace Avalonia.Web.Blazor public Size ClientSize => _clientSize; public Size? FrameSize => null; - public double RenderScaling => (Surfaces.FirstOrDefault() as BlazorSkiaSurface)?.Scaling ?? 1; + public double RenderScaling => (Surfaces.FirstOrDefault() as BrowserSkiaSurface)?.Scaling ?? 1; public IEnumerable Surfaces { get; set; } @@ -203,7 +200,7 @@ namespace Avalonia.Web.Blazor public Action? LostFocus { get; set; } public IMouseDevice MouseDevice { get; } = new MouseDevice(); - public IKeyboardDevice KeyboardDevice { get; } = BlazorWindowingPlatform.Keyboard; + public IKeyboardDevice KeyboardDevice { get; } = BrowserWindowingPlatform.Keyboard; public WindowTransparencyLevel TransparencyLevel { get; } public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } diff --git a/src/Web/Avalonia.Web/ClipboardImpl.cs b/src/Web/Avalonia.Web/ClipboardImpl.cs index c8b2d74c83..793099f55a 100644 --- a/src/Web/Avalonia.Web/ClipboardImpl.cs +++ b/src/Web/Avalonia.Web/ClipboardImpl.cs @@ -1,27 +1,21 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Threading.Tasks; using Avalonia.Input; using Avalonia.Input.Platform; +using Avalonia.Web.Interop; -namespace Avalonia.Web.Blazor +namespace Avalonia.Web { internal class ClipboardImpl : IClipboard { - public async Task GetTextAsync() + public Task GetTextAsync() { - throw new NotImplementedException(); - //return await AvaloniaLocator.Current.GetRequiredService(). - // InvokeAsync("navigator.clipboard.readText"); + return InputHelper.ReadClipboardTextAsync(); } - public async Task SetTextAsync(string text) + public Task SetTextAsync(string text) { - throw new NotImplementedException(); - //await AvaloniaLocator.Current.GetRequiredService(). - // InvokeAsync("navigator.clipboard.writeText",text); + return InputHelper.WriteClipboardTextAsync(text); } public async Task ClearAsync() => await SetTextAsync(""); diff --git a/src/Web/Avalonia.Web/Cursor.cs b/src/Web/Avalonia.Web/Cursor.cs index 843932bfde..5db0bdeda2 100644 --- a/src/Web/Avalonia.Web/Cursor.cs +++ b/src/Web/Avalonia.Web/Cursor.cs @@ -3,7 +3,7 @@ using System.IO; using Avalonia.Input; using Avalonia.Platform; -namespace Avalonia.Web.Blazor +namespace Avalonia.Web { public class CssCursor : ICursorImpl { @@ -80,7 +80,7 @@ namespace Avalonia.Web.Blazor { using var imageStream = new MemoryStream(); cursor.Save(imageStream); - + //not memory optimized because CryptoStream with ToBase64Transform is not supported in the browser. var base64String = Convert.ToBase64String(imageStream.ToArray()); return new CssCursor(base64String, "png", hotSpot, StandardCursorType.Arrow); diff --git a/src/Web/Avalonia.Web/IBlazorSkiaSurface.cs b/src/Web/Avalonia.Web/IBrowserSkiaSurface.cs similarity index 59% rename from src/Web/Avalonia.Web/IBlazorSkiaSurface.cs rename to src/Web/Avalonia.Web/IBrowserSkiaSurface.cs index 5463893e27..4b492f6b17 100644 --- a/src/Web/Avalonia.Web/IBlazorSkiaSurface.cs +++ b/src/Web/Avalonia.Web/IBrowserSkiaSurface.cs @@ -1,6 +1,6 @@ -namespace Avalonia.Web.Blazor +namespace Avalonia.Web { - internal interface IBlazorSkiaSurface + internal interface IBrowserSkiaSurface { public PixelSize Size { get; set; } diff --git a/src/Web/Avalonia.Web/Interop/CanvasHelper.cs b/src/Web/Avalonia.Web/Interop/CanvasHelper.cs new file mode 100644 index 0000000000..1e93d11c34 --- /dev/null +++ b/src/Web/Avalonia.Web/Interop/CanvasHelper.cs @@ -0,0 +1,42 @@ +using System; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.JavaScript; + +namespace Avalonia.Web.Interop; + +internal record GLInfo(int ContextId, uint FboId, int Stencils, int Samples, int Depth); + +internal static partial class CanvasHelper +{ + + [DllImport("libSkiaSharp", CallingConvention = CallingConvention.Cdecl)] + static extern JSObject InterceptGLObject(); + + public static GLInfo InitialiseGL(JSObject canvas, Action renderFrameCallback) + { + InterceptGLObject(); + + var info = InitGL(canvas, canvas.GetPropertyAsString("id")!, renderFrameCallback); + + var glInfo = new GLInfo( + info.GetPropertyAsInt32("context"), + (uint)info.GetPropertyAsInt32("fboId"), + info.GetPropertyAsInt32("stencil"), + info.GetPropertyAsInt32("sample"), + info.GetPropertyAsInt32("depth")); + + return glInfo; + } + + [JSImport("Canvas.requestAnimationFrame", "avalonia.ts")] + public static partial void RequestAnimationFrame(JSObject canvas, bool renderLoop); + + [JSImport("Canvas.setCanvasSize", "avalonia.ts")] + public static partial void SetCanvasSize(JSObject canvas, int height, int width); + + [JSImport("Canvas.initGL", "avalonia.ts")] + private static partial JSObject InitGL( + JSObject canvas, + string canvasId, + [JSMarshalAs] Action renderFrameCallback); +} diff --git a/src/Web/Avalonia.Web/Interop/DomHelper.cs b/src/Web/Avalonia.Web/Interop/DomHelper.cs new file mode 100644 index 0000000000..1167d21a6b --- /dev/null +++ b/src/Web/Avalonia.Web/Interop/DomHelper.cs @@ -0,0 +1,25 @@ +using System; +using System.Runtime.InteropServices.JavaScript; + +namespace Avalonia.Web.Interop; + +internal static partial class DomHelper +{ + [JSImport("globalThis.document.getElementById")] + internal static partial JSObject GetElementById(string id); + + [JSImport("AvaloniaDOM.createAvaloniaHost", "avalonia.ts")] + public static partial JSObject CreateAvaloniaHost(JSObject element); + + [JSImport("SizeWatcher.observe", "avalonia.ts")] + public static partial JSObject ObserveSize( + JSObject canvas, + string canvasId, + [JSMarshalAs>] + Action onSizeChanged); + + [JSImport("DpiWatcher.start", "avalonia.ts")] + public static partial double ObserveDpi( + [JSMarshalAs>] + Action onDpiChanged); +} diff --git a/src/Web/Avalonia.Web/Interop/InputHelper.cs b/src/Web/Avalonia.Web/Interop/InputHelper.cs index 43cc0b84a2..7fb2ba35f4 100644 --- a/src/Web/Avalonia.Web/Interop/InputHelper.cs +++ b/src/Web/Avalonia.Web/Interop/InputHelper.cs @@ -1,16 +1,11 @@ using System; using System.Runtime.InteropServices.JavaScript; +using System.Threading.Tasks; namespace Avalonia.Web.Interop; internal static partial class InputHelper { - //[JSImport("InputHelper.subscribePointerEvents", "avalonia.ts")] - //public static partial void SubscribePointerEvents( - // JSObject htmlElement, - // [JSMarshalAs>] - // Func wheel); - [JSImport("InputHelper.subscribeKeyboardEvents", "avalonia.ts")] public static partial void SubscribeKeyboardEvents( JSObject htmlElement, @@ -19,9 +14,6 @@ internal static partial class InputHelper [JSMarshalAs>] Func keyUp); - - - [JSImport("InputHelper.subscribePointerEvents", "avalonia.ts")] public static partial void SubscribePointerEvents( JSObject htmlElement, @@ -30,7 +22,9 @@ internal static partial class InputHelper [JSMarshalAs>] Func pointerDown, [JSMarshalAs>] - Func pointerUp); + Func pointerUp, + [JSMarshalAs>] + Func wheel); [JSImport("InputHelper.subscribeInputEvents", "avalonia.ts")] @@ -57,4 +51,11 @@ internal static partial class InputHelper [JSImport("InputHelper.show", "avalonia.ts")] public static partial void ShowElement(JSObject htmlElement); + + + [JSImport("navigator.clipboard.readText")] + public static partial Task ReadClipboardTextAsync(); + + [JSImport("navigator.clipboard.writeText")] + public static partial Task WriteClipboardTextAsync(string text); } diff --git a/src/Web/Avalonia.Web/ManualTriggerRenderTimer.cs b/src/Web/Avalonia.Web/ManualTriggerRenderTimer.cs index 505b978a91..774848e19f 100644 --- a/src/Web/Avalonia.Web/ManualTriggerRenderTimer.cs +++ b/src/Web/Avalonia.Web/ManualTriggerRenderTimer.cs @@ -2,7 +2,7 @@ using System; using System.Diagnostics; using Avalonia.Rendering; -namespace Avalonia.Web.Blazor +namespace Avalonia.Web { public class ManualTriggerRenderTimer : IRenderTimer { diff --git a/src/Web/Avalonia.Web/WinStubs.cs b/src/Web/Avalonia.Web/WinStubs.cs index e41f6ca29c..b0961115fe 100644 --- a/src/Web/Avalonia.Web/WinStubs.cs +++ b/src/Web/Avalonia.Web/WinStubs.cs @@ -1,14 +1,10 @@ using System.Collections.Generic; using System.IO; -using Avalonia.Controls; -using Avalonia.Controls.Platform; -using Avalonia.Input; -using Avalonia.Input.Platform; using Avalonia.Platform; #nullable enable -namespace Avalonia.Web.Blazor +namespace Avalonia.Web { internal class IconLoaderStub : IPlatformIconLoader { diff --git a/src/Web/Avalonia.Web/WindowingPlatform.cs b/src/Web/Avalonia.Web/WindowingPlatform.cs index 4dc200f58c..3f14680241 100644 --- a/src/Web/Avalonia.Web/WindowingPlatform.cs +++ b/src/Web/Avalonia.Web/WindowingPlatform.cs @@ -1,17 +1,14 @@ using System; using System.Threading; -using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Threading; -#nullable enable - -namespace Avalonia.Web.Blazor +namespace Avalonia.Web { - public class BlazorWindowingPlatform : IWindowingPlatform, IPlatformSettings, IPlatformThreadingInterface + public class BrowserWindowingPlatform : IWindowingPlatform, IPlatformSettings, IPlatformThreadingInterface { private bool _signaled; private static KeyboardDevice? s_keyboard; @@ -29,11 +26,11 @@ namespace Avalonia.Web.Blazor } public static KeyboardDevice Keyboard => s_keyboard ?? - throw new InvalidOperationException("BlazorWindowingPlatform not registered."); + throw new InvalidOperationException("BrowserWindowingPlatform not registered."); public static void Register() { - var instance = new BlazorWindowingPlatform(); + var instance = new BrowserWindowingPlatform(); s_keyboard = new KeyboardDevice(); AvaloniaLocator.CurrentMutable .Bind().ToSingleton() @@ -94,7 +91,7 @@ namespace Avalonia.Web.Blazor { get { - return true; // Blazor is single threaded. + return true; // Browser is single threaded. } } diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia.ts index bf9ef48772..9e5cec838f 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia.ts @@ -1,7 +1,16 @@ import { RuntimeAPI } from "../types/dotnet"; -import { AvaloniaRuntime } from "./avalonia/runtime"; +import { SizeWatcher } from "./avalonia/canvas"; +import { DpiWatcher } from "./avalonia/canvas"; +import { Canvas } from "./avalonia/canvas"; +import { InputHelper } from "./avalonia/input"; +import { AvaloniaDOM } from "./avalonia/dom"; -export async function createAvaloniaRuntime(api: RuntimeAPI): Promise { - const dotnetAssembly = await api.getAssemblyExports("Avalonia.Web.dll"); - return new AvaloniaRuntime(dotnetAssembly, api); -} \ No newline at end of file +export async function createAvaloniaRuntime(api: RuntimeAPI): Promise { + api.setModuleImports("avalonia.ts", { + Canvas, + InputHelper, + SizeWatcher, + DpiWatcher, + AvaloniaDOM + }); +} diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts index fb31eac7b1..51f851c9c7 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts @@ -23,17 +23,6 @@ export class Canvas { newWidth?: number; newHeight?: number; - public static createCanvas(element: HTMLDivElement): HTMLCanvasElement { - var canvas = document.createElement("canvas"); - canvas.style.setProperty("width", "100%"); - canvas.style.setProperty("height", "100%"); - - element.appendChild(canvas); - canvas.classList.add('avalonia-canvas'); - - return canvas; - } - public static initGL(element: HTMLCanvasElement, elementId: string, renderFrameCallback: () => void): SKGLViewInfo | null { var view = Canvas.init(true, element, elementId, renderFrameCallback); if (!view || !view.glInfo) diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/dom.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/dom.ts new file mode 100644 index 0000000000..fcb66f80be --- /dev/null +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/dom.ts @@ -0,0 +1,51 @@ +export class AvaloniaDOM { + static createAvaloniaHost(host: HTMLElement) { + // Root element + host.classList.add("avalonia-container"); + host.tabIndex = 0; + host.style.touchAction = "none"; + // host.style.position = "relative"; + host.style.position = "fixed"; + host.style.width = "100vw"; + host.style.height = "100vh"; + host.oncontextmenu = function () { return false; }; + + // Rendering target canvas + const canvas = document.createElement("canvas"); + canvas.classList.add('avalonia-canvas'); + canvas.style.backgroundColor = "#ccc"; + canvas.style.width = "100%"; + canvas.style.height = "100%"; + //canvas.style.position = "absolute"; + host.appendChild(canvas); + + // Native controls host + const nativeHost = document.createElement("div"); + nativeHost.classList.add('avalonia-native-host'); + nativeHost.style.width = "100%"; + nativeHost.style.height = "100%"; + nativeHost.style.position = "absolute"; + host.appendChild(nativeHost); + + // IME + const inputElement = document.createElement("input"); + inputElement.autocapitalize = "none"; + inputElement.type = "text"; + inputElement.classList.add('avalonia-input-element'); + inputElement.style.opacity = "0"; + inputElement.style.width = "100%"; + inputElement.style.height = "100%"; + inputElement.style.position = "absolute"; + inputElement.onpaste = function () { return false; }; + inputElement.oncopy = function () { return false; }; + inputElement.oncut = function () { return false; }; + host.appendChild(inputElement); + + return { + host, + canvas, + nativeHost, + inputElement + }; + } +} diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts index c27955ce11..75bdecd71c 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts @@ -24,17 +24,15 @@ export class InputHelper { keyDownCallback: (code: string, key: string, modifiers: RawInputModifiers) => boolean, keyUpCallback: (code: string, key: string, modifiers: RawInputModifiers) => boolean, ) { - const keyDownHandler = (args: Event) => { - const keyArgs = args; - if (keyDownCallback(keyArgs.code, keyArgs.key, this.getModifiers(keyArgs))) { + const keyDownHandler = (args: KeyboardEvent) => { + if (keyDownCallback(args.code, args.key, this.getModifiers(args))) { args.preventDefault(); } }; element.addEventListener("keydown", keyDownHandler); - const keyUpHandler = (args: Event) => { - const keyArgs = args; - if (keyUpCallback(keyArgs.code, keyArgs.key, this.getModifiers(keyArgs))) { + const keyUpHandler = (args: KeyboardEvent) => { + if (keyUpCallback(args.code, args.key, this.getModifiers(args))) { args.preventDefault(); } }; @@ -51,6 +49,7 @@ export class InputHelper { pointerMoveCallback: (args: PointerEvent) => boolean, pointerDownCallback: (args: PointerEvent) => boolean, pointerUpCallback: (args: PointerEvent) => boolean, + wheelCallback: (args: WheelEvent) => boolean, ) { const pointerMoveHandler = (args: PointerEvent) => { @@ -73,9 +72,17 @@ export class InputHelper { } }; + const wheelHandler = (args: WheelEvent) => { + + if (wheelCallback(args)) { + args.preventDefault(); + } + }; + element.addEventListener("pointermove", pointerMoveHandler); element.addEventListener("pointerdown", pointerDownHandler); element.addEventListener("pointerup", pointerUpHandler); + element.addEventListener("wheel", wheelHandler); @@ -83,6 +90,7 @@ export class InputHelper { element.removeEventListener("pointerover", pointerMoveHandler); element.removeEventListener("pointerdown", pointerDownHandler); element.removeEventListener("pointerup", pointerUpHandler); + element.removeEventListener("wheel", wheelHandler); }; } diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts deleted file mode 100644 index d54583b15e..0000000000 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { RuntimeAPI } from "../../types/dotnet"; - -import { SizeWatcher } from "./canvas"; -import { DpiWatcher } from "./canvas"; -import { Canvas } from "./canvas"; -import { InputHelper } from "./input"; - -export class AvaloniaRuntime { - constructor( - private dotnetAssembly: any, - api: RuntimeAPI - ) { - api.setModuleImports("avalonia.ts", { - Canvas, - InputHelper, - SizeWatcher, - DpiWatcher - }); - } -} From 710f015b68077856f8154de7cbd3e36e757ea016 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sun, 25 Sep 2022 11:21:41 +0100 Subject: [PATCH 024/169] refactor subscription of pointer events to share code, and preventdefault when handled. --- src/Web/Avalonia.Web/AvaloniaView.cs | 166 ++++++++++---------- src/Web/Avalonia.Web/BrowserTopLevelImpl.cs | 16 +- 2 files changed, 94 insertions(+), 88 deletions(-) diff --git a/src/Web/Avalonia.Web/AvaloniaView.cs b/src/Web/Avalonia.Web/AvaloniaView.cs index 7df8cb84e1..80ef577368 100644 --- a/src/Web/Avalonia.Web/AvaloniaView.cs +++ b/src/Web/Avalonia.Web/AvaloniaView.cs @@ -78,88 +78,7 @@ namespace Avalonia.Web (code, key, modifier) => _topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyDown, code, key, (RawInputModifiers)modifier), (code, key, modifier) => _topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyUp, code, key, (RawInputModifiers)modifier)); - InputHelper.SubscribePointerEvents(_containerElement, args => - { - var type = args.GetPropertyAsString("pointertype"); - - var point = new RawPointerPoint - { - Position = new Point(args.GetPropertyAsDouble("clientX"), args.GetPropertyAsDouble("clientY")), - Pressure = (float)args.GetPropertyAsDouble("pressure"), - XTilt = (float)args.GetPropertyAsDouble("tiltX"), - YTilt = (float)args.GetPropertyAsDouble("tiltY"), - Twist = (float)args.GetPropertyAsDouble("twist") - }; - - _topLevelImpl.RawPointerEvent(RawPointerEventType.Move, type!, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId")); - - return false; - - }, args => { - - var pointerType = args.GetPropertyAsString("pointerType"); - - var type = pointerType switch - { - "touch" => RawPointerEventType.TouchBegin, - _ => args.GetPropertyAsInt32("button") switch - { - 0 => RawPointerEventType.LeftButtonDown, - 1 => RawPointerEventType.MiddleButtonDown, - 2 => RawPointerEventType.RightButtonDown, - 3 => RawPointerEventType.XButton1Down, - 4 => RawPointerEventType.XButton2Down, - // 5 => Pen eraser button, - _ => RawPointerEventType.Move - } - }; - - var point = new RawPointerPoint - { - Position = new Point(args.GetPropertyAsDouble("clientX"), args.GetPropertyAsDouble("clientY")), - Pressure = (float)args.GetPropertyAsDouble("pressure"), - XTilt = (float)args.GetPropertyAsDouble("tiltX"), - YTilt = (float)args.GetPropertyAsDouble("tiltY"), - Twist = (float)args.GetPropertyAsDouble("twist") - }; - - _topLevelImpl.RawPointerEvent(type, pointerType!, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId")); - return false; - }, args => { - var pointerType = args.GetPropertyAsString("pointerType") ?? "mouse"; - - var type = pointerType switch - { - "touch" => RawPointerEventType.TouchEnd, - _ => args.GetPropertyAsInt32("button") switch - { - 0 => RawPointerEventType.LeftButtonUp, - 1 => RawPointerEventType.MiddleButtonUp, - 2 => RawPointerEventType.RightButtonUp, - 3 => RawPointerEventType.XButton1Up, - 4 => RawPointerEventType.XButton2Up, - // 5 => Pen eraser button, - _ => RawPointerEventType.Move - } - }; - - var point = new RawPointerPoint - { - Position = new Point(args.GetPropertyAsDouble("clientX"), args.GetPropertyAsDouble("clientY")), - Pressure = (float)args.GetPropertyAsDouble("pressure"), - XTilt = (float)args.GetPropertyAsDouble("tiltX"), - YTilt = (float)args.GetPropertyAsDouble("tiltY"), - Twist = (float)args.GetPropertyAsDouble("twist") - }; - - _topLevelImpl.RawPointerEvent(type, pointerType, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId")); - return false; - }, args => - { - _topLevelImpl.RawMouseWheelEvent(new Point(args.GetPropertyAsDouble("clientX"), args.GetPropertyAsDouble("clientY")), - new Vector(-(args.GetPropertyAsDouble("deltaX") / 50), -(args.GetPropertyAsDouble("deltaY") / 50)), GetModifiers(args)); - return false; - }); + InputHelper.SubscribePointerEvents(_containerElement, OnPointerMove, OnPointerDown, OnPointerUp, OnWheel); var skiaOptions = AvaloniaLocator.Current.GetService(); @@ -196,11 +115,88 @@ namespace Avalonia.Web _topLevelImpl.SetClientSize(_canvasSize, _dpi); - DomHelper.ObserveSize(_canvas, "mycanvas", OnSizeChanged); + DomHelper.ObserveSize(_canvas, _canvas.GetPropertyAsString("id")!, OnSizeChanged); CanvasHelper.RequestAnimationFrame(_canvas, true); } + private static RawPointerPoint ExtractRawPointerFromJSArgs(JSObject args) + { + var point = new RawPointerPoint + { + Position = new Point(args.GetPropertyAsDouble("clientX"), args.GetPropertyAsDouble("clientY")), + Pressure = (float)args.GetPropertyAsDouble("pressure"), + XTilt = (float)args.GetPropertyAsDouble("tiltX"), + YTilt = (float)args.GetPropertyAsDouble("tiltY"), + Twist = (float)args.GetPropertyAsDouble("twist") + }; + + return point; + } + + private bool OnPointerMove(JSObject args) + { + var type = args.GetPropertyAsString("pointertype"); + + var point = ExtractRawPointerFromJSArgs(args); + + return _topLevelImpl.RawPointerEvent(RawPointerEventType.Move, type!, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId")); + } + + private bool OnPointerDown(JSObject args) + { + var pointerType = args.GetPropertyAsString("pointerType"); + + var type = pointerType switch + { + "touch" => RawPointerEventType.TouchBegin, + _ => args.GetPropertyAsInt32("button") switch + { + 0 => RawPointerEventType.LeftButtonDown, + 1 => RawPointerEventType.MiddleButtonDown, + 2 => RawPointerEventType.RightButtonDown, + 3 => RawPointerEventType.XButton1Down, + 4 => RawPointerEventType.XButton2Down, + // 5 => Pen eraser button, + _ => RawPointerEventType.Move + } + }; + + var point = ExtractRawPointerFromJSArgs(args); + + return _topLevelImpl.RawPointerEvent(type, pointerType!, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId")); + } + + private bool OnPointerUp(JSObject args) + { + var pointerType = args.GetPropertyAsString("pointerType") ?? "mouse"; + + var type = pointerType switch + { + "touch" => RawPointerEventType.TouchEnd, + _ => args.GetPropertyAsInt32("button") switch + { + 0 => RawPointerEventType.LeftButtonUp, + 1 => RawPointerEventType.MiddleButtonUp, + 2 => RawPointerEventType.RightButtonUp, + 3 => RawPointerEventType.XButton1Up, + 4 => RawPointerEventType.XButton2Up, + // 5 => Pen eraser button, + _ => RawPointerEventType.Move + } + }; + + var point = ExtractRawPointerFromJSArgs(args); + + return _topLevelImpl.RawPointerEvent(type, pointerType, point, GetModifiers(args), args.GetPropertyAsInt32("pointerId")); + } + + private bool OnWheel(JSObject args) + { + return _topLevelImpl.RawMouseWheelEvent(new Point(args.GetPropertyAsDouble("clientX"), args.GetPropertyAsDouble("clientY")), + new Vector(-(args.GetPropertyAsDouble("deltaX") / 50), -(args.GetPropertyAsDouble("deltaY") / 50)), GetModifiers(args)); + } + private static RawInputModifiers GetModifiers(JSObject e) { var modifiers = RawInputModifiers.None; @@ -291,7 +287,7 @@ namespace Avalonia.Web { _dpi = newDpi; - CanvasHelper.SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); + //CanvasHelper.SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); _topLevelImpl.SetClientSize(_canvasSize, _dpi); @@ -307,7 +303,7 @@ namespace Avalonia.Web { _canvasSize = newSize; - CanvasHelper.SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); + //CanvasHelper.SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); _topLevelImpl.SetClientSize(_canvasSize, _dpi); diff --git a/src/Web/Avalonia.Web/BrowserTopLevelImpl.cs b/src/Web/Avalonia.Web/BrowserTopLevelImpl.cs index f8ac2a17c0..f9812c4ae8 100644 --- a/src/Web/Avalonia.Web/BrowserTopLevelImpl.cs +++ b/src/Web/Avalonia.Web/BrowserTopLevelImpl.cs @@ -66,7 +66,7 @@ namespace Avalonia.Web } } - public void RawPointerEvent( + public bool RawPointerEvent( RawPointerEventType eventType, string pointerType, RawPointerPoint p, RawInputModifiers modifiers, long touchPointId) { @@ -82,7 +82,11 @@ namespace Avalonia.Web }; input.Invoke(args); + + return args.Handled; } + + return false; } private IPointerDevice GetPointerDevice(string pointerType) @@ -95,12 +99,18 @@ namespace Avalonia.Web }; } - public void RawMouseWheelEvent(Point p, Vector v, RawInputModifiers modifiers) + public bool RawMouseWheelEvent(Point p, Vector v, RawInputModifiers modifiers) { if (_inputRoot is { }) { - Input?.Invoke(new RawMouseWheelEventArgs(MouseDevice, Timestamp, _inputRoot, p, v, modifiers)); + var args = new RawMouseWheelEventArgs(MouseDevice, Timestamp, _inputRoot, p, v, modifiers); + + Input?.Invoke(args); + + return args.Handled; } + + return false; } public bool RawKeyboardEvent(RawKeyEventType type, string code, string key, RawInputModifiers modifiers) From cd0cbedf10859cb0a3727f350399b60c914cb473 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sun, 25 Sep 2022 11:29:32 +0100 Subject: [PATCH 025/169] set canvas height and width properties to resize backing buffer. --- src/Web/Avalonia.Web/AvaloniaView.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Web/Avalonia.Web/AvaloniaView.cs b/src/Web/Avalonia.Web/AvaloniaView.cs index 80ef577368..08d4505bd6 100644 --- a/src/Web/Avalonia.Web/AvaloniaView.cs +++ b/src/Web/Avalonia.Web/AvaloniaView.cs @@ -277,7 +277,7 @@ namespace Avalonia.Web if (_topLevel.Renderer is CompositingRenderer dr) { - //dr.CompositionTarget.ImmediateUIThreadRender(); + dr.CompositionTarget.ImmediateUIThreadRender(); } } @@ -287,7 +287,7 @@ namespace Avalonia.Web { _dpi = newDpi; - //CanvasHelper.SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); + CanvasHelper.SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); _topLevelImpl.SetClientSize(_canvasSize, _dpi); @@ -303,7 +303,7 @@ namespace Avalonia.Web { _canvasSize = newSize; - //CanvasHelper.SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); + CanvasHelper.SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); _topLevelImpl.SetClientSize(_canvasSize, _dpi); From d48981872da8a93fd34a4634cb460642867145cf Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sun, 25 Sep 2022 15:14:31 +0100 Subject: [PATCH 026/169] ensure the dom is setup so that the avalonia elements tracks the size and position of container div. --- src/Web/Avalonia.Web/AvaloniaView.cs | 4 ++-- src/Web/Avalonia.Web/webapp/modules/avalonia/dom.ts | 10 ++++------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/Web/Avalonia.Web/AvaloniaView.cs b/src/Web/Avalonia.Web/AvaloniaView.cs index 08d4505bd6..e522524ecf 100644 --- a/src/Web/Avalonia.Web/AvaloniaView.cs +++ b/src/Web/Avalonia.Web/AvaloniaView.cs @@ -115,7 +115,7 @@ namespace Avalonia.Web _topLevelImpl.SetClientSize(_canvasSize, _dpi); - DomHelper.ObserveSize(_canvas, _canvas.GetPropertyAsString("id")!, OnSizeChanged); + DomHelper.ObserveSize(host, divId, OnSizeChanged); CanvasHelper.RequestAnimationFrame(_canvas, true); } @@ -124,7 +124,7 @@ namespace Avalonia.Web { var point = new RawPointerPoint { - Position = new Point(args.GetPropertyAsDouble("clientX"), args.GetPropertyAsDouble("clientY")), + Position = new Point(args.GetPropertyAsDouble("offsetX"), args.GetPropertyAsDouble("offsetY")), Pressure = (float)args.GetPropertyAsDouble("pressure"), XTilt = (float)args.GetPropertyAsDouble("tiltX"), YTilt = (float)args.GetPropertyAsDouble("tiltY"), diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/dom.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/dom.ts index fcb66f80be..81ad34acaa 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia/dom.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/dom.ts @@ -3,11 +3,6 @@ export class AvaloniaDOM { // Root element host.classList.add("avalonia-container"); host.tabIndex = 0; - host.style.touchAction = "none"; - // host.style.position = "relative"; - host.style.position = "fixed"; - host.style.width = "100vw"; - host.style.height = "100vh"; host.oncontextmenu = function () { return false; }; // Rendering target canvas @@ -16,12 +11,13 @@ export class AvaloniaDOM { canvas.style.backgroundColor = "#ccc"; canvas.style.width = "100%"; canvas.style.height = "100%"; - //canvas.style.position = "absolute"; host.appendChild(canvas); // Native controls host const nativeHost = document.createElement("div"); nativeHost.classList.add('avalonia-native-host'); + nativeHost.style.left = "0px"; + nativeHost.style.top = "0px"; nativeHost.style.width = "100%"; nativeHost.style.height = "100%"; nativeHost.style.position = "absolute"; @@ -33,6 +29,8 @@ export class AvaloniaDOM { inputElement.type = "text"; inputElement.classList.add('avalonia-input-element'); inputElement.style.opacity = "0"; + inputElement.style.left = "0px"; + inputElement.style.top = "0px"; inputElement.style.width = "100%"; inputElement.style.height = "100%"; inputElement.style.position = "absolute"; From fdf37f57864b6bae073b5374589729a450d3b72e Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sun, 25 Sep 2022 15:26:04 +0100 Subject: [PATCH 027/169] ensure input box is hidden until needed. --- src/Web/Avalonia.Web/AvaloniaView.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Web/Avalonia.Web/AvaloniaView.cs b/src/Web/Avalonia.Web/AvaloniaView.cs index e522524ecf..4e79e5b6d8 100644 --- a/src/Web/Avalonia.Web/AvaloniaView.cs +++ b/src/Web/Avalonia.Web/AvaloniaView.cs @@ -79,6 +79,8 @@ namespace Avalonia.Web (code, key, modifier) => _topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyUp, code, key, (RawInputModifiers)modifier)); InputHelper.SubscribePointerEvents(_containerElement, OnPointerMove, OnPointerDown, OnPointerUp, OnWheel); + + InputHelper.HideElement(_inputElement); var skiaOptions = AvaloniaLocator.Current.GetService(); From 6d610c353369de61cf0b233ad2749ced1ee8a435 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sun, 25 Sep 2022 15:27:42 +0100 Subject: [PATCH 028/169] initialize input hidden. --- src/Web/Avalonia.Web/AvaloniaView.cs | 3 --- src/Web/Avalonia.Web/webapp/modules/avalonia/dom.ts | 1 + 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Web/Avalonia.Web/AvaloniaView.cs b/src/Web/Avalonia.Web/AvaloniaView.cs index 4e79e5b6d8..3fad991ebe 100644 --- a/src/Web/Avalonia.Web/AvaloniaView.cs +++ b/src/Web/Avalonia.Web/AvaloniaView.cs @@ -80,9 +80,6 @@ namespace Avalonia.Web InputHelper.SubscribePointerEvents(_containerElement, OnPointerMove, OnPointerDown, OnPointerUp, OnWheel); - InputHelper.HideElement(_inputElement); - - var skiaOptions = AvaloniaLocator.Current.GetService(); _dpi = DomHelper.ObserveDpi(OnDpiChanged); diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/dom.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/dom.ts index 81ad34acaa..521ecc51de 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia/dom.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/dom.ts @@ -28,6 +28,7 @@ export class AvaloniaDOM { inputElement.autocapitalize = "none"; inputElement.type = "text"; inputElement.classList.add('avalonia-input-element'); + inputElement.style.display = "none"; inputElement.style.opacity = "0"; inputElement.style.left = "0px"; inputElement.style.top = "0px"; From 8a9764a7f67938e6522d5b6ed7901750a2aa966a Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sun, 25 Sep 2022 15:36:09 +0100 Subject: [PATCH 029/169] fix sizing issues. --- src/Web/Avalonia.Web.Sample/index.html | 2 +- src/Web/Avalonia.Web/webapp/modules/avalonia/dom.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Web/Avalonia.Web.Sample/index.html b/src/Web/Avalonia.Web.Sample/index.html index 8f6d402651..a4e017a9aa 100644 --- a/src/Web/Avalonia.Web.Sample/index.html +++ b/src/Web/Avalonia.Web.Sample/index.html @@ -13,7 +13,7 @@ -
+
diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/dom.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/dom.ts index 521ecc51de..0333c3d734 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia/dom.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/dom.ts @@ -11,6 +11,7 @@ export class AvaloniaDOM { canvas.style.backgroundColor = "#ccc"; canvas.style.width = "100%"; canvas.style.height = "100%"; + canvas.style.position = "absolute"; host.appendChild(canvas); // Native controls host From ddc9b33e1aebc972bb15a53f90c51cfb2e4afe0d Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 26 Sep 2022 10:10:46 +0100 Subject: [PATCH 030/169] add input and composition events to input helper. --- src/Web/Avalonia.Web/Interop/InputHelper.cs | 10 +- .../Avalonia.Web/webapp/modules/avalonia.ts | 4 +- .../webapp/modules/avalonia/CaretHelper.ts | 149 ++++++++++++++++++ .../webapp/modules/avalonia/input.ts | 75 +++++++-- 4 files changed, 227 insertions(+), 11 deletions(-) create mode 100644 src/Web/Avalonia.Web/webapp/modules/avalonia/CaretHelper.ts diff --git a/src/Web/Avalonia.Web/Interop/InputHelper.cs b/src/Web/Avalonia.Web/Interop/InputHelper.cs index 7fb2ba35f4..b894aaabfa 100644 --- a/src/Web/Avalonia.Web/Interop/InputHelper.cs +++ b/src/Web/Avalonia.Web/Interop/InputHelper.cs @@ -12,7 +12,15 @@ internal static partial class InputHelper [JSMarshalAs>] Func keyDown, [JSMarshalAs>] - Func keyUp); + Func keyUp, + [JSMarshalAs] + Func onInput, + [JSMarshalAs] + Func onCompositionStart, + [JSMarshalAs] + Func onCompositionUpdate, + [JSMarshalAs] + Func onCompositionEnd) [JSImport("InputHelper.subscribePointerEvents", "avalonia.ts")] public static partial void SubscribePointerEvents( diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia.ts index 9e5cec838f..4b85434656 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia.ts @@ -4,6 +4,7 @@ import { DpiWatcher } from "./avalonia/canvas"; import { Canvas } from "./avalonia/canvas"; import { InputHelper } from "./avalonia/input"; import { AvaloniaDOM } from "./avalonia/dom"; +import { CaretHelper } from "./avalonia/CaretHelper" export async function createAvaloniaRuntime(api: RuntimeAPI): Promise { api.setModuleImports("avalonia.ts", { @@ -11,6 +12,7 @@ export async function createAvaloniaRuntime(api: RuntimeAPI): Promise { InputHelper, SizeWatcher, DpiWatcher, - AvaloniaDOM + AvaloniaDOM, + CaretHelper }); } diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/CaretHelper.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/CaretHelper.ts new file mode 100644 index 0000000000..5709854087 --- /dev/null +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/CaretHelper.ts @@ -0,0 +1,149 @@ +// Based on https://github.com/component/textarea-caret-position/blob/master/index.js +export class CaretHelper { + public static getCaretCoordinates( + element: HTMLInputElement | HTMLTextAreaElement, + position: number, + options?: { debug: boolean } + ) { + if (!isBrowser) { + throw new Error( + "textarea-caret-position#getCaretCoordinates should only be called in a browser" + ); + } + + const debug = (options && options.debug) || false; + if (debug) { + const el = document.querySelector( + "#input-textarea-caret-position-mirror-div" + ); + if (el) el.parentNode?.removeChild(el); + } + + // The mirror div will replicate the textarea's style + const div = document.createElement("div"); + div.id = "input-textarea-caret-position-mirror-div"; + document.body.appendChild(div); + + const style = div.style; + const computed = window.getComputedStyle + ? window.getComputedStyle(element) + : ((element as any)["currentStyle"] as CSSStyleDeclaration); // currentStyle for IE < 9 + const isInput = element.nodeName === "INPUT"; + + // Default textarea styles + style.whiteSpace = "pre-wrap"; + if (!isInput) style.wordWrap = "break-word"; // only for textarea-s + + // Position off-screen + style.position = "absolute"; // required to return coordinates properly + if (!debug) style.visibility = "hidden"; // not 'display: none' because we want rendering + + // Transfer the element's properties to the div + properties.forEach((prop: string) => { + if (isInput && prop === "lineHeight") { + // Special case for s because text is rendered centered and line height may be != height + if (computed.boxSizing === "border-box") { + const height = parseInt(computed.height); + const outerHeight = + parseInt(computed.paddingTop) + + parseInt(computed.paddingBottom) + + parseInt(computed.borderTopWidth) + + parseInt(computed.borderBottomWidth); + const targetHeight = outerHeight + parseInt(computed.lineHeight); + if (height > targetHeight) { + style.lineHeight = height - outerHeight + "px"; + } else if (height === targetHeight) { + style.lineHeight = computed.lineHeight; + } else { + style.lineHeight = "0"; + } + } else { + style.lineHeight = computed.height; + } + } else { + (style as any)[prop] = (computed as any)[prop]; + } + }); + + if (isFirefox) { + // Firefox lies about the overflow property for textareas: https://bugzilla.mozilla.org/show_bug.cgi?id=984275 + if (element.scrollHeight > parseInt(computed.height)) + style.overflowY = "scroll"; + } else { + style.overflow = "hidden"; // for Chrome to not render a scrollbar; IE keeps overflowY = 'scroll' + } + + div.textContent = element.value.substring(0, position); + // The second special handling for input type="text" vs textarea: + // spaces need to be replaced with non-breaking spaces - http://stackoverflow.com/a/13402035/1269037 + if (isInput) div.textContent = div.textContent.replace(/\s/g, "\u00a0"); + + const span = document.createElement("span"); + // Wrapping must be replicated *exactly*, including when a long word gets + // onto the next line, with whitespace at the end of the line before (#7). + // The *only* reliable way to do that is to copy the *entire* rest of the + // textarea's content into the created at the caret position. + // For inputs, just '.' would be enough, but no need to bother. + span.textContent = element.value.substring(position) || "."; // || because a completely empty faux span doesn't render at all + div.appendChild(span); + + const coordinates = { + top: span.offsetTop + parseInt(computed["borderTopWidth"]), + left: span.offsetLeft + parseInt(computed["borderLeftWidth"]), + height: parseInt(computed["lineHeight"]), + }; + + if (debug) { + span.style.backgroundColor = "#aaa"; + } else { + document.body.removeChild(div); + } + + return coordinates; + } +} + + +var properties = [ + "direction", // RTL support + "boxSizing", + "width", // on Chrome and IE, exclude the scrollbar, so the mirror div wraps exactly as the textarea does + "height", + "overflowX", + "overflowY", // copy the scrollbar for IE + + "borderTopWidth", + "borderRightWidth", + "borderBottomWidth", + "borderLeftWidth", + "borderStyle", + + "paddingTop", + "paddingRight", + "paddingBottom", + "paddingLeft", + + // https://developer.mozilla.org/en-US/docs/Web/CSS/font + "fontStyle", + "fontVariant", + "fontWeight", + "fontStretch", + "fontSize", + "fontSizeAdjust", + "lineHeight", + "fontFamily", + + "textAlign", + "textTransform", + "textIndent", + "textDecoration", // might not make a difference, but better be safe + + "letterSpacing", + "wordSpacing", + + "tabSize", + "MozTabSize", +]; + +const isBrowser = typeof window !== "undefined"; +const isFirefox = isBrowser && (window as any).mozInnerScreenX != null; diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts index 75bdecd71c..2aaa25592b 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts @@ -1,3 +1,5 @@ +import { CaretHelper } from "./CaretHelper" + enum RawInputModifiers { None = 0, @@ -23,7 +25,11 @@ export class InputHelper { element: HTMLInputElement, keyDownCallback: (code: string, key: string, modifiers: RawInputModifiers) => boolean, keyUpCallback: (code: string, key: string, modifiers: RawInputModifiers) => boolean, - ) { + inputCallback: (args: InputEvent) => boolean, + compositionStartCallback: (args: CompositionEvent) => boolean, + compositionUpdateCallback: (args: CompositionEvent) => boolean, + compositionEndCallback: (args: CompositionEvent) => boolean) + { const keyDownHandler = (args: KeyboardEvent) => { if (keyDownCallback(args.code, args.key, this.getModifiers(args))) { args.preventDefault(); @@ -38,10 +44,48 @@ export class InputHelper { }; element.addEventListener("keyup", keyUpHandler); + const inputHandler = (args: Event) => { + var inputEvent = args as InputEvent; + + // todo check cast + if (inputCallback(inputEvent)) { + args.preventDefault(); + } + }; + element.addEventListener("input", inputHandler); + + const compositionStartHandler = (args: CompositionEvent) => { + if (compositionStartCallback(args)) { + args.preventDefault(); + } + }; + element.addEventListener("compositionstart", compositionStartHandler); + + const compositionUpdateHandler = (args: CompositionEvent) => { + if (compositionUpdateCallback(args)) { + args.preventDefault(); + } + }; + element.addEventListener("compositionupdate", compositionUpdateHandler); + + const compositionEndHandler = (args: CompositionEvent) => { + if (compositionEndCallback(args)) { + args.preventDefault(); + } + }; + element.addEventListener("compositionend", compositionEndHandler); + return () => { element.removeEventListener("keydown", keyDownHandler); element.removeEventListener("keyup", keyUpHandler); + element.removeEventListener("compositionstart", compositionStartHandler); + element.removeEventListener("compositionupdate", compositionUpdateHandler); + element.removeEventListener("compositionend", compositionEndHandler); }; + + + + } public static subscribePointerEvents( @@ -114,22 +158,24 @@ export class InputHelper { inputElement.value = ""; } - public static isInputElement(element: HTMLInputElement | HTMLElement): element is HTMLInputElement { - return (element as HTMLInputElement).setSelectionRange !== undefined; - } - public static focusElement(inputElement: HTMLElement) { inputElement.focus(); - - if (this.isInputElement(inputElement)) { - (inputElement as HTMLInputElement).setSelectionRange(0, 0); - } } public static setCursor(inputElement: HTMLInputElement, kind: string) { inputElement.style.cursor = kind; } + public static setBounds(inputElement: HTMLInputElement, x: number, y: number, caretWidth: number, caretHeight: number, caret: number) { + inputElement.style.left = (x).toFixed(0) + "px"; + inputElement.style.top = (y).toFixed(0) + "px"; + + let { height, left, top } = CaretHelper.getCaretCoordinates(inputElement, caret); + + inputElement.style.left = (x - left).toFixed(0) + "px"; + inputElement.style.top = (y - top).toFixed(0) + "px"; + } + public static hide(inputElement: HTMLInputElement) { inputElement.style.display = 'none'; } @@ -138,6 +184,17 @@ export class InputHelper { inputElement.style.display = 'block'; } + public static setSurroundingText(inputElement: HTMLInputElement, text: string, start: number, end: number) { + if (!inputElement) { + return; + } + + inputElement.value = text; + inputElement.setSelectionRange(start, end); + inputElement.style.width = "20px"; + inputElement.style.width = inputElement.scrollWidth + "px"; + } + private static getModifiers(args: KeyboardEvent): RawInputModifiers { var modifiers = RawInputModifiers.None; From 01127d6693fae13ece7a7082a5f4591124071b1d Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 26 Sep 2022 10:11:57 +0100 Subject: [PATCH 031/169] style the input element correctly for IME support. --- .../webapp/modules/avalonia/dom.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/dom.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/dom.ts index 0333c3d734..ad6d451343 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia/dom.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/dom.ts @@ -26,16 +26,20 @@ export class AvaloniaDOM { // IME const inputElement = document.createElement("input"); + inputElement.classList.add('avalonia-input-element'); inputElement.autocapitalize = "none"; inputElement.type = "text"; - inputElement.classList.add('avalonia-input-element'); - inputElement.style.display = "none"; - inputElement.style.opacity = "0"; - inputElement.style.left = "0px"; - inputElement.style.top = "0px"; - inputElement.style.width = "100%"; - inputElement.style.height = "100%"; + inputElement.spellcheck = false; + inputElement.style.padding = "0"; + inputElement.style.margin = "0"; inputElement.style.position = "absolute"; + inputElement.style.overflow = "hidden"; + inputElement.style.borderStyle = "hidden"; + inputElement.style.outline = "none"; + inputElement.style.background = "transparent"; + inputElement.style.color = "transparent"; + inputElement.style.display = "none"; + inputElement.style.height = "20px"; inputElement.onpaste = function () { return false; }; inputElement.oncopy = function () { return false; }; inputElement.oncut = function () { return false; }; From 98aba6d4a7e90c2f49e2380eafdafb56b2021d58 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 26 Sep 2022 10:17:40 +0100 Subject: [PATCH 032/169] add basics for ime support. --- src/Web/Avalonia.Web/AvaloniaView.cs | 6 +++++- src/Web/Avalonia.Web/Interop/InputHelper.cs | 10 +++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/Web/Avalonia.Web/AvaloniaView.cs b/src/Web/Avalonia.Web/AvaloniaView.cs index 3fad991ebe..4638709a54 100644 --- a/src/Web/Avalonia.Web/AvaloniaView.cs +++ b/src/Web/Avalonia.Web/AvaloniaView.cs @@ -76,7 +76,11 @@ namespace Avalonia.Web InputHelper.SubscribeKeyboardEvents( _containerElement, (code, key, modifier) => _topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyDown, code, key, (RawInputModifiers)modifier), - (code, key, modifier) => _topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyUp, code, key, (RawInputModifiers)modifier)); + (code, key, modifier) => _topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyUp, code, key, (RawInputModifiers)modifier), + (args) => { return false; }, + (args) => { return false; }, + (args) => { return false; }, + (args) => { return false; }); InputHelper.SubscribePointerEvents(_containerElement, OnPointerMove, OnPointerDown, OnPointerUp, OnWheel); diff --git a/src/Web/Avalonia.Web/Interop/InputHelper.cs b/src/Web/Avalonia.Web/Interop/InputHelper.cs index b894aaabfa..3a86416e6b 100644 --- a/src/Web/Avalonia.Web/Interop/InputHelper.cs +++ b/src/Web/Avalonia.Web/Interop/InputHelper.cs @@ -13,14 +13,14 @@ internal static partial class InputHelper Func keyDown, [JSMarshalAs>] Func keyUp, - [JSMarshalAs] + [JSMarshalAs>] Func onInput, - [JSMarshalAs] + [JSMarshalAs>] Func onCompositionStart, - [JSMarshalAs] + [JSMarshalAs>] Func onCompositionUpdate, - [JSMarshalAs] - Func onCompositionEnd) + [JSMarshalAs>] + Func onCompositionEnd); [JSImport("InputHelper.subscribePointerEvents", "avalonia.ts")] public static partial void SubscribePointerEvents( From b2b706ed20db8fd4a9b5c26ab397076702514cf8 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 26 Sep 2022 19:14:40 +0100 Subject: [PATCH 033/169] working input. --- .../Avalonia.Web.Sample.csproj | 1 + src/Web/Avalonia.Web/AvaloniaView.cs | 128 ++++++++++++++++-- src/Web/Avalonia.Web/BrowserTopLevelImpl.cs | 9 +- src/Web/Avalonia.Web/Interop/InputHelper.cs | 21 ++- .../webapp/modules/avalonia/input.ts | 27 ++-- 5 files changed, 161 insertions(+), 25 deletions(-) diff --git a/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj b/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj index ca4d29aa21..df254df55b 100644 --- a/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj +++ b/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj @@ -14,6 +14,7 @@ true true + full true true true diff --git a/src/Web/Avalonia.Web/AvaloniaView.cs b/src/Web/Avalonia.Web/AvaloniaView.cs index 4638709a54..ad45c6a5a2 100644 --- a/src/Web/Avalonia.Web/AvaloniaView.cs +++ b/src/Web/Avalonia.Web/AvaloniaView.cs @@ -33,6 +33,7 @@ namespace Avalonia.Web private bool _useGL; private bool _inputElementFocused; + private ITextInputMethodClient? _client; private static int _canvasCount; public AvaloniaView(string divId) @@ -73,14 +74,17 @@ namespace Avalonia.Web _topLevel.Renderer.Start(); - InputHelper.SubscribeKeyboardEvents( + InputHelper.SubscribeKeyEvents( _containerElement, - (code, key, modifier) => _topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyDown, code, key, (RawInputModifiers)modifier), - (code, key, modifier) => _topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyUp, code, key, (RawInputModifiers)modifier), - (args) => { return false; }, - (args) => { return false; }, - (args) => { return false; }, - (args) => { return false; }); + OnKeyDown, + OnKeyUp); + + InputHelper.SubscribeTextEvents( + _inputElement, + OnTextInput, + OnCompositionStart, + OnCompositionUpdate, + OnCompositionEnd); InputHelper.SubscribePointerEvents(_containerElement, OnPointerMove, OnPointerDown, OnPointerUp, OnWheel); @@ -235,6 +239,59 @@ namespace Avalonia.Web return modifiers; } + private bool OnKeyDown (string code, string key, int modifier) + { + return _topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyDown, code, key, (RawInputModifiers)modifier); + } + + private bool OnKeyUp(string code, string key, int modifier) + { + return _topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyUp, code, key, (RawInputModifiers)modifier); + } + + private bool OnTextInput (string type, string? data) + { + if(data == null || IsComposing) + { + return false; + } + + return _topLevelImpl.RawTextEvent(data); + } + + private bool OnCompositionStart (JSObject args) + { + if (_client == null) + return false; + + _client.SetPreeditText(null); + IsComposing = true; + + return false; + } + + private bool OnCompositionUpdate(JSObject args) + { + if (_client == null) + return false; + + _client.SetPreeditText(args.GetPropertyAsString("data")); + + return false; + } + + private bool OnCompositionEnd(JSObject args) + { + if (_client == null) + return false; + + IsComposing = false; + _client.SetPreeditText(null); + _topLevelImpl.RawTextEvent(args.GetPropertyAsString("data")!); + + return false; + } + private void OnRenderFrame() { if (_useGL && (_jsGlInfo == null)) @@ -257,6 +314,8 @@ namespace Avalonia.Web set => _topLevel.Content = value; } + public bool IsComposing { get; private set; } + internal INativeControlHostImpl GetNativeControlHostImpl() { throw new NotImplementedException(); @@ -314,13 +373,63 @@ namespace Avalonia.Web } } + private void HideIme() + { + InputHelper.HideElement(_inputElement); + InputHelper.FocusElement(_containerElement); + } + public void SetClient(ITextInputMethodClient? client) { - + Console.WriteLine("Set Client"); + if (_client != null) + { + _client.SurroundingTextChanged -= SurroundingTextChanged; + } + + if (client != null) + { + client.SurroundingTextChanged += SurroundingTextChanged; + } + + InputHelper.ClearInputElement(_inputElement); + + _client = client; + + if (_client != null) + { + InputHelper.ShowElement(_inputElement); + _inputElementFocused = true; + InputHelper.FocusElement(_inputElement); + + var surroundingText = _client.SurroundingText; + + InputHelper.SetSurroundingText(_inputElement, surroundingText.Text, surroundingText.AnchorOffset, surroundingText.CursorOffset); + + Console.WriteLine("Shown, focused and surrounded."); + } + else + { + _inputElementFocused = false; + HideIme(); + } + } + + private void SurroundingTextChanged(object? sender, EventArgs e) + { + if (_client != null) + { + var surroundingText = _client.SurroundingText; + + InputHelper.SetSurroundingText(_inputElement, surroundingText.Text, surroundingText.AnchorOffset, surroundingText.CursorOffset); + } } public void SetCursorRect(Rect rect) { + InputHelper.FocusElement(_inputElement); + InputHelper.SetBounds(_inputElement, (int)rect.X, (int)rect.Y, (int)rect.Width, (int)rect.Height, _client?.SurroundingText.CursorOffset ?? 0); + InputHelper.FocusElement(_inputElement); } public void SetOptions(TextInputOptions options) @@ -329,7 +438,8 @@ namespace Avalonia.Web public void Reset() { - + InputHelper.ClearInputElement(_inputElement); + InputHelper.SetSurroundingText(_inputElement, "", 0, 0); } } } diff --git a/src/Web/Avalonia.Web/BrowserTopLevelImpl.cs b/src/Web/Avalonia.Web/BrowserTopLevelImpl.cs index f9812c4ae8..c514101d53 100644 --- a/src/Web/Avalonia.Web/BrowserTopLevelImpl.cs +++ b/src/Web/Avalonia.Web/BrowserTopLevelImpl.cs @@ -141,12 +141,17 @@ namespace Avalonia.Web return false; } - public void RawTextEvent(string text) + public bool RawTextEvent(string text) { if (_inputRoot is { }) { - Input?.Invoke(new RawTextInputEventArgs(KeyboardDevice, Timestamp, _inputRoot, text)); + var args = new RawTextInputEventArgs(KeyboardDevice, Timestamp, _inputRoot, text); + Input?.Invoke(args); + + return args.Handled; } + + return false; } public void Dispose() diff --git a/src/Web/Avalonia.Web/Interop/InputHelper.cs b/src/Web/Avalonia.Web/Interop/InputHelper.cs index 3a86416e6b..e728acdc52 100644 --- a/src/Web/Avalonia.Web/Interop/InputHelper.cs +++ b/src/Web/Avalonia.Web/Interop/InputHelper.cs @@ -1,20 +1,26 @@ using System; using System.Runtime.InteropServices.JavaScript; using System.Threading.Tasks; +using static System.Net.Mime.MediaTypeNames; +using static System.Runtime.InteropServices.JavaScript.JSType; namespace Avalonia.Web.Interop; internal static partial class InputHelper { - [JSImport("InputHelper.subscribeKeyboardEvents", "avalonia.ts")] - public static partial void SubscribeKeyboardEvents( + [JSImport("InputHelper.subscribeKeyEvents", "avalonia.ts")] + public static partial void SubscribeKeyEvents( JSObject htmlElement, [JSMarshalAs>] Func keyDown, [JSMarshalAs>] - Func keyUp, - [JSMarshalAs>] - Func onInput, + Func keyUp); + + [JSImport("InputHelper.subscribeTextEvents", "avalonia.ts")] + public static partial void SubscribeTextEvents( + JSObject htmlElement, + [JSMarshalAs>] + Func onInput, [JSMarshalAs>] Func onCompositionStart, [JSMarshalAs>] @@ -60,6 +66,11 @@ internal static partial class InputHelper [JSImport("InputHelper.show", "avalonia.ts")] public static partial void ShowElement(JSObject htmlElement); + [JSImport("InputHelper.setSurroundingText", "avalonia.ts")] + public static partial void SetSurroundingText(JSObject htmlElement, string text, int start, int end); + + [JSImport("InputHelper.setBounds", "avalonia.ts")] + public static partial void SetBounds(JSObject htmlElement, int x, int y, int width, int height, int caret); [JSImport("navigator.clipboard.readText")] public static partial Task ReadClipboardTextAsync(); diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts index 2aaa25592b..b5c8a60a88 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts @@ -21,11 +21,11 @@ enum RawInputModifiers } export class InputHelper { - public static subscribeKeyboardEvents( + public static subscribeKeyEvents( element: HTMLInputElement, keyDownCallback: (code: string, key: string, modifiers: RawInputModifiers) => boolean, keyUpCallback: (code: string, key: string, modifiers: RawInputModifiers) => boolean, - inputCallback: (args: InputEvent) => boolean, + inputCallback: (type: string, data: string|null) => boolean, compositionStartCallback: (args: CompositionEvent) => boolean, compositionUpdateCallback: (args: CompositionEvent) => boolean, compositionEndCallback: (args: CompositionEvent) => boolean) @@ -42,13 +42,27 @@ export class InputHelper { args.preventDefault(); } }; + element.addEventListener("keyup", keyUpHandler); + return () => { + element.removeEventListener("keydown", keyDownHandler); + element.removeEventListener("keyup", keyUpHandler); + }; + } + + public static subscribeTextEvents( + element: HTMLInputElement, + inputCallback: (type: string, data: string | null) => boolean, + compositionStartCallback: (args: CompositionEvent) => boolean, + compositionUpdateCallback: (args: CompositionEvent) => boolean, + compositionEndCallback: (args: CompositionEvent) => boolean) { + const inputHandler = (args: Event) => { var inputEvent = args as InputEvent; // todo check cast - if (inputCallback(inputEvent)) { + if (inputCallback(inputEvent.type, inputEvent.data)) { args.preventDefault(); } }; @@ -76,16 +90,11 @@ export class InputHelper { element.addEventListener("compositionend", compositionEndHandler); return () => { - element.removeEventListener("keydown", keyDownHandler); - element.removeEventListener("keyup", keyUpHandler); + element.removeEventListener("input", inputHandler); element.removeEventListener("compositionstart", compositionStartHandler); element.removeEventListener("compositionupdate", compositionUpdateHandler); element.removeEventListener("compositionend", compositionEndHandler); }; - - - - } public static subscribePointerEvents( From 4ca077cd70de7774a4f5a5b7d63b3a4cdff02046 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 26 Sep 2022 21:07:00 +0100 Subject: [PATCH 034/169] non hard coded path for interop.js --- src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj | 4 ++-- src/Web/Avalonia.Web/Avalonia.Web.props | 7 +++++++ src/Web/{Avalonia.Web.Sample => Avalonia.Web}/interop.js | 0 3 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 src/Web/Avalonia.Web/Avalonia.Web.props rename src/Web/{Avalonia.Web.Sample => Avalonia.Web}/interop.js (100%) diff --git a/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj b/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj index df254df55b..cb681930c9 100644 --- a/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj +++ b/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj @@ -8,7 +8,6 @@ true true -sVERBOSE -sERROR_ON_UNDEFINED_SYMBOLS=0 - --js-library="C:\Users\User\repos\Avalonia\src\Web\Avalonia.Web.Sample\interop.js" @@ -34,6 +33,7 @@ - + + diff --git a/src/Web/Avalonia.Web/Avalonia.Web.props b/src/Web/Avalonia.Web/Avalonia.Web.props new file mode 100644 index 0000000000..ae2c19ecc7 --- /dev/null +++ b/src/Web/Avalonia.Web/Avalonia.Web.props @@ -0,0 +1,7 @@ + + + + + $(EmccExtraLDFlags) --js-library="$(MSBuildThisFileDirectory)\interop.js" + + diff --git a/src/Web/Avalonia.Web.Sample/interop.js b/src/Web/Avalonia.Web/interop.js similarity index 100% rename from src/Web/Avalonia.Web.Sample/interop.js rename to src/Web/Avalonia.Web/interop.js From 534a78c04fe2d2d0b79d6c285bb6274592b1b657 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 26 Sep 2022 23:25:03 +0100 Subject: [PATCH 035/169] remove hard coded lib paths. --- src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj | 4 +--- src/Web/Avalonia.Web/Avalonia.Web.targets | 3 ++- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj b/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj index cb681930c9..e1f0d74835 100644 --- a/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj +++ b/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj @@ -24,14 +24,12 @@ + - - - diff --git a/src/Web/Avalonia.Web/Avalonia.Web.targets b/src/Web/Avalonia.Web/Avalonia.Web.targets index 97c8c29ee4..d1bec2aa93 100644 --- a/src/Web/Avalonia.Web/Avalonia.Web.targets +++ b/src/Web/Avalonia.Web/Avalonia.Web.targets @@ -1,6 +1,7 @@ + + - \ No newline at end of file From b2abd961bbbe1885d42b64340b54d52711da6f43 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 26 Sep 2022 23:32:14 +0100 Subject: [PATCH 036/169] update dotnet sdks, and add 7 rc1 sdk. --- azure-pipelines.yml | 41 ++++++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 52fc8db53c..d089073f8e 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -6,7 +6,6 @@ jobs: variables: SolutionDir: '$(Build.SourcesDirectory)' steps: - - task: PowerShell@2 displayName: Get PR Number inputs: @@ -31,14 +30,20 @@ jobs: vmImage: 'ubuntu-20.04' steps: - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 3.1.418' + displayName: 'Use .NET Core SDK 3.1.423' + inputs: + version: 3.1.423 + + - task: UseDotNet@2 + displayName: 'Use .NET Core SDK 6.0.401' inputs: - version: 3.1.418 + version: 6.0.401 - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 6.0.202' + displayName: 'Use .NET Core SDK 7.0.100-rc.1.22431.12' inputs: - version: 6.0.202 + version: 7.0.100-rc.1.22431.12 + - task: CmdLine@2 displayName: 'Run Build' @@ -62,14 +67,19 @@ jobs: vmImage: 'macos-12' steps: - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 3.1.418' + displayName: 'Use .NET Core SDK 3.1.423' inputs: - version: 3.1.418 + version: 3.1.423 - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 6.0.202' + displayName: 'Use .NET Core SDK 6.0.401' inputs: - version: 6.0.202 + version: 6.0.401 + + - task: UseDotNet@2 + displayName: 'Use .NET Core SDK 7.0.100-rc.1.22431.12' + inputs: + version: 7.0.100-rc.1.22431.12 - task: CmdLine@2 displayName: 'Install Mono 5.18' @@ -134,14 +144,19 @@ jobs: SolutionDir: '$(Build.SourcesDirectory)' steps: - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 3.1.418' + displayName: 'Use .NET Core SDK 3.1.423' + inputs: + version: 3.1.423 + + - task: UseDotNet@2 + displayName: 'Use .NET Core SDK 6.0.401' inputs: - version: 3.1.418 + version: 6.0.401 - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 6.0.202' + displayName: 'Use .NET Core SDK 7.0.100-rc.1.22431.12' inputs: - version: 6.0.202 + version: 7.0.100-rc.1.22431.12 - task: CmdLine@2 displayName: 'Install Workloads' From f725b080ecfbd39e2ea90d01d18f26478891dc9b Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 26 Sep 2022 23:38:49 +0100 Subject: [PATCH 037/169] remove not needed usings. tasks. --- src/Web/Avalonia.Web/Avalonia.Web.props | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Web/Avalonia.Web/Avalonia.Web.props b/src/Web/Avalonia.Web/Avalonia.Web.props index ae2c19ecc7..6c975cd284 100644 --- a/src/Web/Avalonia.Web/Avalonia.Web.props +++ b/src/Web/Avalonia.Web/Avalonia.Web.props @@ -1,6 +1,4 @@ - - - + $(EmccExtraLDFlags) --js-library="$(MSBuildThisFileDirectory)\interop.js" From 55bed8805fb9a75878c33d17b0d4b64a0a3f57d8 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 26 Sep 2022 23:44:02 +0100 Subject: [PATCH 038/169] install workloads. --- azure-pipelines.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index d089073f8e..9ae811de9d 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -44,6 +44,11 @@ jobs: inputs: version: 7.0.100-rc.1.22431.12 + - task: CmdLine@2 + displayName: 'Install Workloads' + inputs: + script: | + dotnet workload install android ios wasm-tools wasm-experimental - task: CmdLine@2 displayName: 'Run Build' @@ -80,6 +85,12 @@ jobs: displayName: 'Use .NET Core SDK 7.0.100-rc.1.22431.12' inputs: version: 7.0.100-rc.1.22431.12 + + - task: CmdLine@2 + displayName: 'Install Workloads' + inputs: + script: | + dotnet workload install android ios wasm-tools wasm-experimental - task: CmdLine@2 displayName: 'Install Mono 5.18' @@ -162,7 +173,7 @@ jobs: displayName: 'Install Workloads' inputs: script: | - dotnet workload install android ios + dotnet workload install android ios wasm-tools wasm-experimental - task: CmdLine@2 displayName: 'Install Nuke' From 8053f8d66f91e8c0cb1027eafc87283a61d603d5 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 26 Sep 2022 23:46:07 +0100 Subject: [PATCH 039/169] dont install non compatible workloads on linux. --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 9ae811de9d..5bfda7cd2e 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -48,7 +48,7 @@ jobs: displayName: 'Install Workloads' inputs: script: | - dotnet workload install android ios wasm-tools wasm-experimental + dotnet workload install wasm-tools wasm-experimental - task: CmdLine@2 displayName: 'Run Build' From a34f216b84b77a7eca51fadd2b71124928566c32 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 27 Sep 2022 00:09:37 +0100 Subject: [PATCH 040/169] reference avalonia and not avalonia.base. --- src/Web/Avalonia.Web/Avalonia.Web.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Web/Avalonia.Web/Avalonia.Web.csproj b/src/Web/Avalonia.Web/Avalonia.Web.csproj index 13f311ba53..b8192ecdc4 100644 --- a/src/Web/Avalonia.Web/Avalonia.Web.csproj +++ b/src/Web/Avalonia.Web/Avalonia.Web.csproj @@ -15,7 +15,7 @@ - + From 285a5beca73e7519576bb9aff2d5e57ee8a7b737 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 27 Sep 2022 00:38:19 +0100 Subject: [PATCH 041/169] fix reference. --- src/Web/Avalonia.Web/Avalonia.Web.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Web/Avalonia.Web/Avalonia.Web.csproj b/src/Web/Avalonia.Web/Avalonia.Web.csproj index b8192ecdc4..fd5f976f4b 100644 --- a/src/Web/Avalonia.Web/Avalonia.Web.csproj +++ b/src/Web/Avalonia.Web/Avalonia.Web.csproj @@ -15,7 +15,7 @@ - + From f005a5d0dec993d30bde6a216ab9c6089afc4e94 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 27 Sep 2022 00:38:37 +0100 Subject: [PATCH 042/169] comment out broken code due to net 7 breaking csharp compiler. --- src/iOS/Avalonia.iOS/CombinedSpan3.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/iOS/Avalonia.iOS/CombinedSpan3.cs b/src/iOS/Avalonia.iOS/CombinedSpan3.cs index 29d8eb3955..c73fb8de96 100644 --- a/src/iOS/Avalonia.iOS/CombinedSpan3.cs +++ b/src/iOS/Avalonia.iOS/CombinedSpan3.cs @@ -33,8 +33,8 @@ internal ref struct CombinedSpan3 public void CopyTo(Span to, int offset) { - CopyFromSpan(Span1, ref offset, ref to); - CopyFromSpan(Span2, ref offset, ref to); - CopyFromSpan(Span3, ref offset, ref to); + //CopyFromSpan(Span1, ref offset, ref to); + //CopyFromSpan(Span2, ref offset, ref to); + //CopyFromSpan(Span3, ref offset, ref to); } } From eb4605cf17d0fe3538c202db47f785717ca067d3 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 27 Sep 2022 08:19:26 +0100 Subject: [PATCH 043/169] fix some compile warnings. --- src/Web/Avalonia.Web/AvaloniaView.cs | 6 ++---- src/Web/Avalonia.Web/BrowserSingleViewLifetime.cs | 1 + src/Web/Avalonia.Web/BrowserTopLevelImpl.cs | 1 + src/Web/Avalonia.Web/Interop/CanvasHelper.cs | 1 + src/Web/Avalonia.Web/Storage/WriteableStream.cs | 1 + 5 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Web/Avalonia.Web/AvaloniaView.cs b/src/Web/Avalonia.Web/AvaloniaView.cs index ad45c6a5a2..73da939110 100644 --- a/src/Web/Avalonia.Web/AvaloniaView.cs +++ b/src/Web/Avalonia.Web/AvaloniaView.cs @@ -13,6 +13,7 @@ using SkiaSharp; namespace Avalonia.Web { + [System.Runtime.Versioning.SupportedOSPlatform("browser")] // gets rid of callsite warnings public partial class AvaloniaView : ITextInputMethodImpl { private readonly BrowserTopLevelImpl _topLevelImpl; @@ -31,8 +32,7 @@ namespace Avalonia.Web private GRGlInterface? _glInterface; private const SKColorType ColorType = SKColorType.Rgba8888; - private bool _useGL; - private bool _inputElementFocused; + private bool _useGL; private ITextInputMethodClient? _client; private static int _canvasCount; @@ -399,7 +399,6 @@ namespace Avalonia.Web if (_client != null) { InputHelper.ShowElement(_inputElement); - _inputElementFocused = true; InputHelper.FocusElement(_inputElement); var surroundingText = _client.SurroundingText; @@ -410,7 +409,6 @@ namespace Avalonia.Web } else { - _inputElementFocused = false; HideIme(); } } diff --git a/src/Web/Avalonia.Web/BrowserSingleViewLifetime.cs b/src/Web/Avalonia.Web/BrowserSingleViewLifetime.cs index e50c11acd6..02d9b01319 100644 --- a/src/Web/Avalonia.Web/BrowserSingleViewLifetime.cs +++ b/src/Web/Avalonia.Web/BrowserSingleViewLifetime.cs @@ -6,6 +6,7 @@ using Avalonia.Media; namespace Avalonia.Web { + [System.Runtime.Versioning.SupportedOSPlatform("browser")] // gets rid of callsite warnings public class BrowserSingleViewLifetime : ISingleViewApplicationLifetime { public AvaloniaView? View; diff --git a/src/Web/Avalonia.Web/BrowserTopLevelImpl.cs b/src/Web/Avalonia.Web/BrowserTopLevelImpl.cs index c514101d53..047b462a64 100644 --- a/src/Web/Avalonia.Web/BrowserTopLevelImpl.cs +++ b/src/Web/Avalonia.Web/BrowserTopLevelImpl.cs @@ -16,6 +16,7 @@ using Avalonia.Rendering.Composition; namespace Avalonia.Web { + [System.Runtime.Versioning.SupportedOSPlatform("browser")] // gets rid of callsite warnings internal class BrowserTopLevelImpl : ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost, ITopLevelImplWithStorageProvider { private Size _clientSize; diff --git a/src/Web/Avalonia.Web/Interop/CanvasHelper.cs b/src/Web/Avalonia.Web/Interop/CanvasHelper.cs index 1e93d11c34..66a1d421b7 100644 --- a/src/Web/Avalonia.Web/Interop/CanvasHelper.cs +++ b/src/Web/Avalonia.Web/Interop/CanvasHelper.cs @@ -6,6 +6,7 @@ namespace Avalonia.Web.Interop; internal record GLInfo(int ContextId, uint FboId, int Stencils, int Samples, int Depth); +[System.Runtime.Versioning.SupportedOSPlatform("browser")] // gets rid of callsite warnings internal static partial class CanvasHelper { diff --git a/src/Web/Avalonia.Web/Storage/WriteableStream.cs b/src/Web/Avalonia.Web/Storage/WriteableStream.cs index 4dce5a4c6c..100b7ccfed 100644 --- a/src/Web/Avalonia.Web/Storage/WriteableStream.cs +++ b/src/Web/Avalonia.Web/Storage/WriteableStream.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; namespace Avalonia.Web.Storage; +[System.Runtime.Versioning.SupportedOSPlatform("browser")] // gets rid of callsite warnings // Loose wrapper implementaion of a stream on top of FileAPI FileSystemWritableFileStream internal sealed class JSWriteableStream : Stream { From 3feb132de94c82ce73a8a8977e9ba495008962ae Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 27 Sep 2022 08:34:45 +0100 Subject: [PATCH 044/169] update nuke. --- nukebuild/Build.cs | 6 ++++-- nukebuild/BuildParameters.cs | 4 ++-- nukebuild/DotNetConfigHelper.cs | 4 ++-- nukebuild/_build.csproj | 4 ++-- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index 4bbb667154..b0047b1f37 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -39,6 +39,7 @@ partial class Build : NukeBuild BuildParameters Parameters { get; set; } protected override void OnBuildInitialized() { + Console.WriteLine($"{Solution.Name}"); Parameters = new BuildParameters(this); Information("Building version {0} of Avalonia ({1}) using version {2} of Nuke.", Parameters.Version, @@ -143,6 +144,7 @@ partial class Build : NukeBuild void RunCoreTest(string projectName) { Information($"Running tests from {projectName}"); + Console.WriteLine(Solution == null); var project = Solution.GetProject(projectName).NotNull("project != null"); foreach (var fw in project.GetTargetFrameworks()) @@ -163,7 +165,7 @@ partial class Build : NukeBuild .EnableNoBuild() .EnableNoRestore() .When(Parameters.PublishTestResults, _ => _ - .SetLogger("trx") + .SetLoggers("trx") .SetResultsDirectory(Parameters.TestResultsRoot))); } } @@ -229,7 +231,7 @@ partial class Build : NukeBuild $"{XunitPath.DoubleQuoteIfNeeded()} --propagate-exit-code -- {testAssembly}", timeout: 120_000); } - ControlFlow.ExecuteWithRetry(DoMemoryTest, waitInSeconds: 3); + ControlFlow.ExecuteWithRetry(DoMemoryTest, delay: TimeSpan.FromSeconds(3)); }); Target ZipFiles => _ => _ diff --git a/nukebuild/BuildParameters.cs b/nukebuild/BuildParameters.cs index 1826623674..c5fed21136 100644 --- a/nukebuild/BuildParameters.cs +++ b/nukebuild/BuildParameters.cs @@ -74,11 +74,11 @@ public partial class Build MSBuildSolution = RootDirectory / "dirs.proj"; // PARAMETERS - IsLocalBuild = Host == HostType.Console; + IsLocalBuild = NukeBuild.IsLocalBuild; IsRunningOnUnix = Environment.OSVersion.Platform == PlatformID.Unix || Environment.OSVersion.Platform == PlatformID.MacOSX; IsRunningOnWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - IsRunningOnAzure = Host == HostType.AzurePipelines || + IsRunningOnAzure = NukeBuild.IsServerBuild || Environment.GetEnvironmentVariable("LOGNAME") == "vsts"; if (IsRunningOnAzure) diff --git a/nukebuild/DotNetConfigHelper.cs b/nukebuild/DotNetConfigHelper.cs index 932525288c..eca1e2684d 100644 --- a/nukebuild/DotNetConfigHelper.cs +++ b/nukebuild/DotNetConfigHelper.cs @@ -46,7 +46,7 @@ public class DotNetConfigHelper public DotNetConfigHelper SetVerbosity(DotNetVerbosity verbosity) { Build = Build?.SetVerbosity(verbosity); - Pack = Pack?.SetVerbostiy(verbosity); + Pack = Pack?.SetVerbosity(verbosity); Test = Test?.SetVerbosity(verbosity); return this; } @@ -54,4 +54,4 @@ public class DotNetConfigHelper public static implicit operator DotNetConfigHelper(DotNetBuildSettings s) => new DotNetConfigHelper(s); public static implicit operator DotNetConfigHelper(DotNetPackSettings s) => new DotNetConfigHelper(s); public static implicit operator DotNetConfigHelper(DotNetTestSettings s) => new DotNetConfigHelper(s); -} \ No newline at end of file +} diff --git a/nukebuild/_build.csproj b/nukebuild/_build.csproj index b2c58e2292..73c716cdd3 100644 --- a/nukebuild/_build.csproj +++ b/nukebuild/_build.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp3.1 + net6.0 false False @@ -10,7 +10,7 @@ - + From 1d550168f8d63cfc1c4be82974ca97083a66fb26 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 27 Sep 2022 08:34:55 +0100 Subject: [PATCH 045/169] more warning fixes --- src/Web/Avalonia.Web/Storage/BlobReadableStream.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Web/Avalonia.Web/Storage/BlobReadableStream.cs b/src/Web/Avalonia.Web/Storage/BlobReadableStream.cs index 6afc5e3054..a48f809c06 100644 --- a/src/Web/Avalonia.Web/Storage/BlobReadableStream.cs +++ b/src/Web/Avalonia.Web/Storage/BlobReadableStream.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; namespace Avalonia.Web.Storage; +[System.Runtime.Versioning.SupportedOSPlatform("browser")] // gets rid of callsite warnings internal class BlobReadableStream : Stream { private JSObject? _jSReference; From d66229663dfe22076e91fe55fece74b611e0d821 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 27 Sep 2022 08:38:41 +0100 Subject: [PATCH 046/169] update nuke --- .nuke/build.schema.json | 148 ++++++++++++++++++ .nuke/parameters.json | 4 + .nuke/temp/build-attempt.log | 5 + .nuke/temp/build.2022-09-27_08-36-07.log | 30 ++++ .../temp/build.2022-09-27_08-37-17.log | 0 .nuke/temp/build.log | 0 build.cmd | 7 + build.ps1 | 50 +++--- build.sh | 60 +++++-- nukebuild/_build.csproj | 1 + 10 files changed, 265 insertions(+), 40 deletions(-) create mode 100644 .nuke/build.schema.json create mode 100644 .nuke/parameters.json create mode 100644 .nuke/temp/build-attempt.log create mode 100644 .nuke/temp/build.2022-09-27_08-36-07.log rename .nuke => .nuke/temp/build.2022-09-27_08-37-17.log (100%) create mode 100644 .nuke/temp/build.log create mode 100755 build.cmd diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json new file mode 100644 index 0000000000..5bbc3d6915 --- /dev/null +++ b/.nuke/build.schema.json @@ -0,0 +1,148 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Build Schema", + "$ref": "#/definitions/build", + "definitions": { + "build": { + "type": "object", + "properties": { + "Configuration": { + "type": "string", + "description": "configuration" + }, + "Continue": { + "type": "boolean", + "description": "Indicates to continue a previously failed build attempt" + }, + "ForceNugetVersion": { + "type": "string", + "description": "force-nuget-version" + }, + "Help": { + "type": "boolean", + "description": "Shows the help text for this build assembly" + }, + "Host": { + "type": "string", + "description": "Host for execution. Default is 'automatic'", + "enum": [ + "AppVeyor", + "AzurePipelines", + "Bamboo", + "Bitbucket", + "Bitrise", + "GitHubActions", + "GitLab", + "Jenkins", + "Rider", + "SpaceAutomation", + "TeamCity", + "Terminal", + "TravisCI", + "VisualStudio", + "VSCode" + ] + }, + "NoLogo": { + "type": "boolean", + "description": "Disables displaying the NUKE logo" + }, + "Partition": { + "type": "string", + "description": "Partition to use on CI" + }, + "Plan": { + "type": "boolean", + "description": "Shows the execution plan (HTML)" + }, + "Profile": { + "type": "array", + "description": "Defines the profiles to load", + "items": { + "type": "string" + } + }, + "Root": { + "type": "string", + "description": "Root directory during build execution" + }, + "Skip": { + "type": "array", + "description": "List of targets to be skipped. Empty list skips all dependencies", + "items": { + "type": "string", + "enum": [ + "CiAzureLinux", + "CiAzureOSX", + "CiAzureWindows", + "Clean", + "Compile", + "CompileHtmlPreviewer", + "CompileNative", + "CreateIntermediateNugetPackages", + "CreateNugetPackages", + "GenerateCppHeaders", + "Package", + "RunCoreLibsTests", + "RunDesignerTests", + "RunHtmlPreviewerTests", + "RunLeakTests", + "RunRenderTests", + "RunTests", + "ZipFiles" + ] + } + }, + "SkipPreviewer": { + "type": "boolean", + "description": "skip-previewer" + }, + "SkipTests": { + "type": "boolean", + "description": "skip-tests" + }, + "Solution": { + "type": "string", + "description": "Path to a solution file that is automatically loaded. Default is Avalonia.sln" + }, + "Target": { + "type": "array", + "description": "List of targets to be invoked. Default is '{default_target}'", + "items": { + "type": "string", + "enum": [ + "CiAzureLinux", + "CiAzureOSX", + "CiAzureWindows", + "Clean", + "Compile", + "CompileHtmlPreviewer", + "CompileNative", + "CreateIntermediateNugetPackages", + "CreateNugetPackages", + "GenerateCppHeaders", + "Package", + "RunCoreLibsTests", + "RunDesignerTests", + "RunHtmlPreviewerTests", + "RunLeakTests", + "RunRenderTests", + "RunTests", + "ZipFiles" + ] + } + }, + "Verbosity": { + "type": "string", + "description": "Logging verbosity during build execution. Default is 'Normal'", + "enum": [ + "Minimal", + "Normal", + "Quiet", + "Verbose" + ] + } + } + } + } +} \ No newline at end of file diff --git a/.nuke/parameters.json b/.nuke/parameters.json new file mode 100644 index 0000000000..42521bb7dd --- /dev/null +++ b/.nuke/parameters.json @@ -0,0 +1,4 @@ +{ + "$schema": "./build.schema.json", + "Solution": "" +} \ No newline at end of file diff --git a/.nuke/temp/build-attempt.log b/.nuke/temp/build-attempt.log new file mode 100644 index 0000000000..a926c68865 --- /dev/null +++ b/.nuke/temp/build-attempt.log @@ -0,0 +1,5 @@ +47be5be6de651ed0c6d6996318ce431a +GenerateCppHeaders +Clean +CompileNative +CompileHtmlPreviewer diff --git a/.nuke/temp/build.2022-09-27_08-36-07.log b/.nuke/temp/build.2022-09-27_08-36-07.log new file mode 100644 index 0000000000..2cba08f7bc --- /dev/null +++ b/.nuke/temp/build.2022-09-27_08-36-07.log @@ -0,0 +1,30 @@ +V | | ArgumentsFromParametersFile.OnBuildCreated (150) +V | | Passing value for Build.Solution () +V | | InjectParameterValues.OnBuildCreated (100) +W | | Could not inject value for Build.Solution +System.ArgumentException: Solution 'C:\Users\User\repos\Avalonia\Avalonia.sln' contains duplicated NestedProjects entries: + - {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA} + at Nuke.Common.Assert.True(Boolean condition, String message, String argumentExpression) in /_/source/Nuke.Common/Assert.cs:line 34 + at Nuke.Common.Utilities.Collections.EnumerableExtensions.ToDictionarySafe[T,TKey,TValue](IEnumerable`1 enumerable, Func`2 keySelector, Func`2 valueSelector, String duplicationMessage) in /_/source/Nuke.Common/Utilities/Collections/Enumerable.ToDictionary.cs:line 46 + at Nuke.Common.ProjectModel.SolutionSerializer.GetGlobalSection(String[] lines, String name, String solutionFile) in /_/source/Nuke.Common/ProjectModel/SolutionSerializer.cs:line 92 + at Nuke.Common.ProjectModel.SolutionSerializer.DeserializeFromContent[T](String[] content, String solutionFile) in /_/source/Nuke.Common/ProjectModel/SolutionSerializer.cs:line 63 + at Nuke.Common.ProjectModel.SolutionSerializer.DeserializeFromFile[T](String solutionFile) in /_/source/Nuke.Common/ProjectModel/SolutionSerializer.cs:line 23 +V | | HandleShellCompletion.OnBuildCreated (75) +V | | GenerateBuildServerConfigurations.OnBuildCreated (50) +V | | InvokeBuildServerConfigurationGeneration.OnBuildCreated (45) +V | | UpdateNotification.OnBuildCreated (10) +V | | UnsetVisualStudioEnvironmentVariables.OnBuildCreated (0) +V | | HandleVisualStudioDebugging.OnBuildCreated (0) +V | | HandleSingleFileExecution.OnBuildCreated (-50) +V | | EventInvoker.OnBuildCreated (-3.4028235E+38) +V | | HandleHelpRequests.OnBuildInitialized (5) +V | | Telemetry.OnBuildInitialized (0) +V | | InjectNonParameterValues.OnBuildInitialized (-100) +V | | EventInvoker.OnBuildInitialized (-3.4028235E+38) +V | | Target-unrelated exception was thrown +System.NullReferenceException: Object reference not set to an instance of an object. + at Build.OnBuildInitialized() in C:\Users\User\repos\Avalonia\nukebuild\Build.cs:line 42 + at Nuke.Common.Execution.EventInvoker.OnBuildInitialized(NukeBuild build, IReadOnlyCollection`1 executableTargets, IReadOnlyCollection`1 executionPlan) in /_/source/Nuke.Common/Execution/EventInvoker.cs:line 27 + at Nuke.Common.Utilities.Collections.EnumerableExtensions.ForEach[T](IEnumerable`1 enumerable, Action`1 action) in /_/source/Nuke.Common/Utilities/Collections/Enumerable.ForEach.cs:line 17 + at Nuke.Common.NukeBuild.ExecuteExtension[TExtension](Expression`1 action) + at Nuke.Common.Execution.BuildManager.Execute[T](Expression`1[] defaultTargetExpressions) in /_/source/Nuke.Common/Execution/BuildManager.cs:line 58 diff --git a/.nuke b/.nuke/temp/build.2022-09-27_08-37-17.log similarity index 100% rename from .nuke rename to .nuke/temp/build.2022-09-27_08-37-17.log diff --git a/.nuke/temp/build.log b/.nuke/temp/build.log new file mode 100644 index 0000000000..e69de29bb2 diff --git a/build.cmd b/build.cmd new file mode 100755 index 0000000000..b08cc590f4 --- /dev/null +++ b/build.cmd @@ -0,0 +1,7 @@ +:; set -eo pipefail +:; SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) +:; ${SCRIPT_DIR}/build.sh "$@" +:; exit $? + +@ECHO OFF +powershell -ExecutionPolicy ByPass -NoProfile -File "%~dp0build.ps1" %* diff --git a/build.ps1 b/build.ps1 index 985e8abcee..997e5b423f 100644 --- a/build.ps1 +++ b/build.ps1 @@ -1,13 +1,12 @@ [CmdletBinding()] Param( - #[switch]$CustomParam, [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)] [string[]]$BuildArguments ) -Write-Output "Windows PowerShell $($Host.Version)" +Write-Output "PowerShell $($PSVersionTable.PSEdition) version $($PSVersionTable.PSVersion)" -Set-StrictMode -Version 2.0; $ErrorActionPreference = "Stop"; $ConfirmPreference = "None"; trap { exit 1 } +Set-StrictMode -Version 2.0; $ErrorActionPreference = "Stop"; $ConfirmPreference = "None"; trap { Write-Error $_ -ErrorAction Continue; exit 1 } $PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent ########################################################################### @@ -15,15 +14,15 @@ $PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent ########################################################################### $BuildProjectFile = "$PSScriptRoot\nukebuild\_build.csproj" -$TempDirectory = "$PSScriptRoot\\.tmp" +$TempDirectory = "$PSScriptRoot\\.nuke\temp" $DotNetGlobalFile = "$PSScriptRoot\\global.json" -$DotNetInstallUrl = "https://raw.githubusercontent.com/dotnet/cli/master/scripts/obtain/dotnet-install.ps1" +$DotNetInstallUrl = "https://dot.net/v1/dotnet-install.ps1" $DotNetChannel = "Current" $env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = 1 $env:DOTNET_CLI_TELEMETRY_OPTOUT = 1 -$env:NUGET_XMLDOC_MODE = "skip" +$env:DOTNET_MULTILEVEL_LOOKUP = 0 ########################################################################### # EXECUTION @@ -34,38 +33,37 @@ function ExecSafe([scriptblock] $cmd) { if ($LASTEXITCODE) { exit $LASTEXITCODE } } -# If global.json exists, load expected version -if (Test-Path $DotNetGlobalFile) { - $DotNetGlobal = $(Get-Content $DotNetGlobalFile | Out-String | ConvertFrom-Json) - if ($DotNetGlobal.PSObject.Properties["sdk"] -and $DotNetGlobal.sdk.PSObject.Properties["version"]) { - $DotNetVersion = $DotNetGlobal.sdk.version - } -} - -# If dotnet is installed locally, and expected version is not set or installation matches the expected version +# If dotnet CLI is installed globally and it matches requested version, use for execution if ($null -ne (Get-Command "dotnet" -ErrorAction SilentlyContinue) -and ` - (!(Test-Path variable:DotNetVersion) -or $(& dotnet --version) -eq $DotNetVersion)) { + $(dotnet --version) -and $LASTEXITCODE -eq 0) { $env:DOTNET_EXE = (Get-Command "dotnet").Path } else { - $DotNetDirectory = "$TempDirectory\dotnet-win" - $env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe" - # Download install script $DotNetInstallFile = "$TempDirectory\dotnet-install.ps1" - mkdir -force $TempDirectory > $null + New-Item -ItemType Directory -Path $TempDirectory -Force | Out-Null + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 (New-Object System.Net.WebClient).DownloadFile($DotNetInstallUrl, $DotNetInstallFile) + # If global.json exists, load expected version + if (Test-Path $DotNetGlobalFile) { + $DotNetGlobal = $(Get-Content $DotNetGlobalFile | Out-String | ConvertFrom-Json) + if ($DotNetGlobal.PSObject.Properties["sdk"] -and $DotNetGlobal.sdk.PSObject.Properties["version"]) { + $DotNetVersion = $DotNetGlobal.sdk.version + } + } + # Install by channel or version + $DotNetDirectory = "$TempDirectory\dotnet-win" if (!(Test-Path variable:DotNetVersion)) { - ExecSafe { & $DotNetInstallFile -InstallDir $DotNetDirectory -Channel $DotNetChannel -NoPath } + ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Channel $DotNetChannel -NoPath } } else { - ExecSafe { & $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath } + ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath } } - - $env:PATH="$DotNetDirectory;$env:PATH" + $env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe" } -Write-Output "Microsoft (R) .NET Core SDK version $(& $env:DOTNET_EXE --version)" +Write-Output "Microsoft (R) .NET SDK version $(& $env:DOTNET_EXE --version)" -ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile -- $BuildArguments } +ExecSafe { & $env:DOTNET_EXE build $BuildProjectFile /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet } +ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile --no-build -- $BuildArguments } diff --git a/build.sh b/build.sh index 9532b4fbe0..76919a5351 100755 --- a/build.sh +++ b/build.sh @@ -1,16 +1,6 @@ #!/usr/bin/env bash -echo $(bash --version 2>&1 | head -n 1) - -#CUSTOMPARAM=0 -BUILD_ARGUMENTS=() -for i in "$@"; do - case $(echo $1 | awk '{print tolower($0)}') in - # -custom-param) CUSTOMPARAM=1;; - *) BUILD_ARGUMENTS+=("$1") ;; - esac - shift -done +bash --version 2>&1 | head -n 1 set -eo pipefail SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) @@ -20,11 +10,53 @@ SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) ########################################################################### BUILD_PROJECT_FILE="$SCRIPT_DIR/nukebuild/_build.csproj" +TEMP_DIRECTORY="$SCRIPT_DIR//.nuke/temp" + +DOTNET_GLOBAL_FILE="$SCRIPT_DIR//global.json" +DOTNET_INSTALL_URL="https://dot.net/v1/dotnet-install.sh" +DOTNET_CHANNEL="Current" export DOTNET_CLI_TELEMETRY_OPTOUT=1 export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 -export NUGET_XMLDOC_MODE="skip" +export DOTNET_MULTILEVEL_LOOKUP=0 -dotnet --info +########################################################################### +# EXECUTION +########################################################################### -dotnet run --project "$BUILD_PROJECT_FILE" -- ${BUILD_ARGUMENTS[@]} +function FirstJsonValue { + perl -nle 'print $1 if m{"'"$1"'": "([^"]+)",?}' <<< "${@:2}" +} + +# If dotnet CLI is installed globally and it matches requested version, use for execution +if [ -x "$(command -v dotnet)" ] && dotnet --version &>/dev/null; then + export DOTNET_EXE="$(command -v dotnet)" +else + # Download install script + DOTNET_INSTALL_FILE="$TEMP_DIRECTORY/dotnet-install.sh" + mkdir -p "$TEMP_DIRECTORY" + curl -Lsfo "$DOTNET_INSTALL_FILE" "$DOTNET_INSTALL_URL" + chmod +x "$DOTNET_INSTALL_FILE" + + # If global.json exists, load expected version + if [[ -f "$DOTNET_GLOBAL_FILE" ]]; then + DOTNET_VERSION=$(FirstJsonValue "version" "$(cat "$DOTNET_GLOBAL_FILE")") + if [[ "$DOTNET_VERSION" == "" ]]; then + unset DOTNET_VERSION + fi + fi + + # Install by channel or version + DOTNET_DIRECTORY="$TEMP_DIRECTORY/dotnet-unix" + if [[ -z ${DOTNET_VERSION+x} ]]; then + "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --channel "$DOTNET_CHANNEL" --no-path + else + "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version "$DOTNET_VERSION" --no-path + fi + export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet" +fi + +echo "Microsoft (R) .NET SDK version $("$DOTNET_EXE" --version)" + +"$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet +"$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@" diff --git a/nukebuild/_build.csproj b/nukebuild/_build.csproj index 73c716cdd3..35fb4dbfb6 100644 --- a/nukebuild/_build.csproj +++ b/nukebuild/_build.csproj @@ -7,6 +7,7 @@ False CS0649;CS0169 + 1 From ee11b4dc9afb5e5fd9b6183b7931880f6bb854fd Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 27 Sep 2022 08:38:51 +0100 Subject: [PATCH 047/169] fix sln. --- Avalonia.sln | 1 - 1 file changed, 1 deletion(-) diff --git a/Avalonia.sln b/Avalonia.sln index afc6af0552..c1760cd6b4 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -581,7 +581,6 @@ Global {41B02319-965D-4945-8005-C1A3D1224165} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B} {D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B} = {9B9E3891-2366-4253-A952-D08BCEB71098} {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} - {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B} {351337F5-D66F-461B-A957-4EF60BDB4BA6} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {909A8CBD-7D0E-42FD-B841-022AD8925820} = {8B6A8209-894F-4BA1-B880-965FD453982C} {11BE52AF-E2DD-4CF0-B19A-05285ACAF571} = {9B9E3891-2366-4253-A952-D08BCEB71098} From 87dbbfe8a1be151ef9499971f4f634aef0507800 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 27 Sep 2022 08:58:12 +0100 Subject: [PATCH 048/169] update nuke. --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 5bfda7cd2e..a5dfbc16a5 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -179,7 +179,7 @@ jobs: displayName: 'Install Nuke' inputs: script: | - dotnet tool install --global Nuke.GlobalTool --version 0.24.0 + dotnet tool install --global Nuke.GlobalTool --version 6.2.1 - task: CmdLine@2 displayName: 'Run Nuke' From 947fcd2a57db544b13c823100d36d3dfb4888c72 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 27 Sep 2022 09:21:05 +0100 Subject: [PATCH 049/169] use older netcoreapp for build project, to be compatible with mono.cecil. --- nukebuild/_build.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nukebuild/_build.csproj b/nukebuild/_build.csproj index 35fb4dbfb6..b9bb4f24b8 100644 --- a/nukebuild/_build.csproj +++ b/nukebuild/_build.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + netcoreapp3.1 false False From f1a16db39ee0ea665df8e13c52b844fa69884bb6 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 27 Sep 2022 10:35:54 +0100 Subject: [PATCH 050/169] revert framework upgrade on controlcatalog.netcore. --- samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj index 32f199ea8b..0667644643 100644 --- a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj +++ b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj @@ -2,7 +2,7 @@ WinExe - net7.0 + net6.0 true true From 5a882ce9e9bea280ff8e3ec8f1fbc3523b8d451b Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 27 Sep 2022 13:46:51 +0100 Subject: [PATCH 051/169] add html based splash screen capability. --- src/Web/Avalonia.Web.Sample/index.html | 6 +- src/Web/Avalonia.Web/AvaloniaView.cs | 17 ++++- src/Web/Avalonia.Web/Interop/DomHelper.cs | 2 +- .../Avalonia.Web/WebEmbeddableControlRoot.cs | 68 +++++++++++++++++++ .../webapp/modules/avalonia/dom.ts | 7 +- 5 files changed, 94 insertions(+), 6 deletions(-) create mode 100644 src/Web/Avalonia.Web/WebEmbeddableControlRoot.cs diff --git a/src/Web/Avalonia.Web.Sample/index.html b/src/Web/Avalonia.Web.Sample/index.html index a4e017a9aa..3866e2bfa7 100644 --- a/src/Web/Avalonia.Web.Sample/index.html +++ b/src/Web/Avalonia.Web.Sample/index.html @@ -13,7 +13,11 @@ -
+
+
+ Loading +
+
diff --git a/src/Web/Avalonia.Web/AvaloniaView.cs b/src/Web/Avalonia.Web/AvaloniaView.cs index 73da939110..faba09f66e 100644 --- a/src/Web/Avalonia.Web/AvaloniaView.cs +++ b/src/Web/Avalonia.Web/AvaloniaView.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices.JavaScript; using Avalonia.Controls; using Avalonia.Controls.Embedding; @@ -8,6 +9,7 @@ using Avalonia.Input.Raw; using Avalonia.Input.TextInput; using Avalonia.Platform.Storage; using Avalonia.Rendering.Composition; +using Avalonia.Threading; using Avalonia.Web.Interop; using SkiaSharp; @@ -23,6 +25,7 @@ namespace Avalonia.Web private readonly JSObject _canvas; private readonly JSObject _nativeControlsContainer; private readonly JSObject _inputElement; + private readonly JSObject? _splash; private GLInfo? _jsGlInfo = null; private double _dpi = 1; @@ -59,11 +62,23 @@ namespace Avalonia.Web _inputElement = hostContent.GetPropertyAsJSObject("inputElement") ?? throw new InvalidOperationException("InputElement cannot be null"); + _splash = DomHelper.GetElementById("avalonia-splash"); + _canvas.SetProperty("id", $"avaloniaCanvas{_canvasCount++}"); _topLevelImpl = new BrowserTopLevelImpl(this); - _topLevel = new EmbeddableControlRoot(_topLevelImpl); + _topLevel = new WebEmbeddableControlRoot(_topLevelImpl, () => + { + Dispatcher.UIThread.Post(() => + { + if (_splash != null) + { + InputHelper.HideElement(_splash); + } + }); + }); + _topLevelImpl.SetCssCursor = (cursor) => { InputHelper.SetCursor(_containerElement, cursor); // macOS diff --git a/src/Web/Avalonia.Web/Interop/DomHelper.cs b/src/Web/Avalonia.Web/Interop/DomHelper.cs index 1167d21a6b..f1d98a9d24 100644 --- a/src/Web/Avalonia.Web/Interop/DomHelper.cs +++ b/src/Web/Avalonia.Web/Interop/DomHelper.cs @@ -6,7 +6,7 @@ namespace Avalonia.Web.Interop; internal static partial class DomHelper { [JSImport("globalThis.document.getElementById")] - internal static partial JSObject GetElementById(string id); + internal static partial JSObject? GetElementById(string id); [JSImport("AvaloniaDOM.createAvaloniaHost", "avalonia.ts")] public static partial JSObject CreateAvaloniaHost(JSObject element); diff --git a/src/Web/Avalonia.Web/WebEmbeddableControlRoot.cs b/src/Web/Avalonia.Web/WebEmbeddableControlRoot.cs new file mode 100644 index 0000000000..19f36403ad --- /dev/null +++ b/src/Web/Avalonia.Web/WebEmbeddableControlRoot.cs @@ -0,0 +1,68 @@ +using System; +using Avalonia.Controls.Embedding; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Rendering.SceneGraph; + +namespace Avalonia.Web +{ + internal class WebEmbeddableControlRoot : EmbeddableControlRoot + { + class SplashScreenCloseCustomDrawingOperation : ICustomDrawOperation + { + private bool _hasRendered; + private Action _onFirstRender; + + public SplashScreenCloseCustomDrawingOperation(Action onFirstRender) + { + _onFirstRender = onFirstRender; + } + + public Rect Bounds => Rect.Empty; + + public bool HasRendered => _hasRendered; + + public void Dispose() + { + + } + + public bool Equals(ICustomDrawOperation? other) + { + return false; + } + + public bool HitTest(Point p) + { + return false; + } + + public void Render(IDrawingContextImpl context) + { + _hasRendered = true; + _onFirstRender(); + } + } + + public WebEmbeddableControlRoot(ITopLevelImpl impl, Action onFirstRender) : base(impl) + { + _splashCloseOp = new SplashScreenCloseCustomDrawingOperation(() => + { + _splashCloseOp = null; + onFirstRender(); + }); + } + + private SplashScreenCloseCustomDrawingOperation? _splashCloseOp; + + public override void Render(DrawingContext context) + { + base.Render(context); + + if (_splashCloseOp != null) + { + context.Custom(_splashCloseOp); + } + } + } +} diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/dom.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/dom.ts index ad6d451343..48b6f9c45f 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia/dom.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/dom.ts @@ -12,7 +12,6 @@ export class AvaloniaDOM { canvas.style.width = "100%"; canvas.style.height = "100%"; canvas.style.position = "absolute"; - host.appendChild(canvas); // Native controls host const nativeHost = document.createElement("div"); @@ -22,7 +21,6 @@ export class AvaloniaDOM { nativeHost.style.width = "100%"; nativeHost.style.height = "100%"; nativeHost.style.position = "absolute"; - host.appendChild(nativeHost); // IME const inputElement = document.createElement("input"); @@ -43,7 +41,10 @@ export class AvaloniaDOM { inputElement.onpaste = function () { return false; }; inputElement.oncopy = function () { return false; }; inputElement.oncut = function () { return false; }; - host.appendChild(inputElement); + + host.prepend(inputElement); + host.prepend(nativeHost); + host.prepend(canvas); return { host, From dff3c6a95a61d4d0e0e4ad9808d17f147e951ee4 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 27 Sep 2022 14:48:33 +0100 Subject: [PATCH 052/169] tidy splashscreen. --- .../Avalonia.Web.Sample.csproj | 3 ++ src/Web/Avalonia.Web.Sample/Logo.svg | 5 +++ src/Web/Avalonia.Web.Sample/app.css | 38 ++++++++++++++++++ src/Web/Avalonia.Web.Sample/favicon.ico | Bin 0 -> 176111 bytes src/Web/Avalonia.Web.Sample/index.html | 13 ++++-- src/Web/Avalonia.Web/AvaloniaView.cs | 2 +- src/Web/Avalonia.Web/Interop/DomHelper.cs | 3 ++ .../webapp/modules/avalonia/dom.ts | 5 +++ 8 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 src/Web/Avalonia.Web.Sample/Logo.svg create mode 100644 src/Web/Avalonia.Web.Sample/app.css create mode 100644 src/Web/Avalonia.Web.Sample/favicon.ico diff --git a/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj b/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj index e1f0d74835..ad8d14e527 100644 --- a/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj +++ b/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj @@ -30,6 +30,9 @@ + + + diff --git a/src/Web/Avalonia.Web.Sample/Logo.svg b/src/Web/Avalonia.Web.Sample/Logo.svg new file mode 100644 index 0000000000..9685a23af1 --- /dev/null +++ b/src/Web/Avalonia.Web.Sample/Logo.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/Web/Avalonia.Web.Sample/app.css b/src/Web/Avalonia.Web.Sample/app.css new file mode 100644 index 0000000000..04ea5bee19 --- /dev/null +++ b/src/Web/Avalonia.Web.Sample/app.css @@ -0,0 +1,38 @@ +#out { + height: 100vh; + width: 100vw +} + +#avalonia-splash { + position: absolute; + height: 100%; + width: 100%; + color: whitesmoke; + background: #171C2C; + font-family: 'Nunito', sans-serif; +} + +#avalonia-splash a{ + color: whitesmoke; + text-decoration: none; +} + +.center { + display: flex; + justify-content: center; + height: 250px; +} + +.splash-close { + animation: fadeOut 1s forwards; +} + +@keyframes fadeOut { + from { + opacity: 1; + } + + to { + opacity: 0; + } +} diff --git a/src/Web/Avalonia.Web.Sample/favicon.ico b/src/Web/Avalonia.Web.Sample/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..da8d49ff9b94e52778f5324a1b87dd443a698b57 GIT binary patch literal 176111 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 zds⪻d~AI?xBz^Z$N&azD%i`bv67=(oXHrTmj{nwVMNAhEzCHOseHxL&sUcX=NAare!0~<0z-2WVQ3ehVF)HLE-`4mXn(Av@p6l+tNjk~xtji-(e zPN6*QB6Y-CwMKF5<9<(q_Y)>e7$NJp8A#d~syTIOO2JP;AMFnk@GYhvVoJ-w(6*)~ z5BImC&YPxQyU*k`Y|1hoN?DiaGv-r_^ zU7i!Y{85Y7R zv-910u%)t|tWg@TfQK3+q{eaw7gL}k!00&5YMvl(5F_KEih_^2y8<@$2J*if0MS<6|_rW*im{p>@-kL(z}@NyzzN; zDiX8!Iz9MJ7a*~OJ{t_P`^g@^i?E)5kJr$xV(=ksawzsJ6qqZdrpG6mvXcQfjZ1^m z>`b7$WME43w?6qiH~B~Pbd=-~eUyaZCH6Exkd!W@m+p$Ty^#g%B}TgxH)w0VGx38O zrsYlPB^N0GD;jWK|0ZYDerGUNyU{Rx)r}7%E3B@>X=i8QR-njnPkl^XgbkIQ0~bL* z&j+0518@Eo>hhuW8~bIlH_}JWh9-~aLkemRrt!M1#2}82;zdI}>PE#_wWo*qI@nMq zwOGAqYX0K+e?sF<;~qK(m_tw$qAIu1aI7@(A(Jn@XbkJtn*^}Ch)@jN0lQdUWJ82J0mS+nJhjCxLGBgxG1Ghc8S zjjEmCnT_CTaiv8>KD3>d$Qzq`e<4EP{jKpa?nq<6cEtop%*yC!x3|($?O7G!}g)k3LfgsQ@a=38U+W0u{{S;Ksr3`hXH>@}|mbxiS zw`pc0qao)`G5$MB6I&f>vwGLrLe3{qa8K?~I|gC_;jm6v?pdL(LN<#{p9b^38?A~! z6~C@5bu^lngtN2hVFwx7mQS0DJLyTK8j3M5^8hmFq*wLN;h_cK_QvyGtE}SmYH*+y zQc}QAf2m|OsA&DE2Yige6#>XL+3)37`uU<*Gtz0>_@W%dwlx~C8fj@4xHJ6s)BL=h z7CiQ8>j`!q=6lAvN|-8Yx`;6?$vU4Ik!Obvhu(ar^LVgM`@b0s9cnGJei~vypd6oB z>z?7bS(D-}>TtLKIl+}iWh!e%m-m8rx>7rNP0ZCM`WQiLGXK*PO3>5%skCPHGfGc3 zbdhxn@O=&fb#8bLJUu^;0<&0bWWH36|BGrlM~o5NQ%7xP9fuo-y|c9IDRZ-gKkeS` ztlqRQr>n4bd6Gg3au*bq5r1g(={CQ|wzLa>kyRavuNfRS>N@hGtpE4My&WF z9kFQp6$J-i%es_dJ1SixKlqfg0ZJ+@x%K6kgUtV3^CG7%-o583!4!^D5p}J!thTu% zM*w~W4{)sr{%azA(TK3L&;RJSDmbGuOQuRmS}L7Jl+>vlgjiafJ`S{9)LehFYd4gZ zs|kv(wX=L9kI`&s%hF%Eu6kj4Ct}gifoFgh`3FR*=+P$1?-dQ|%!Y+s^5*=Fa||LH zza2mP+0{H`U5gRQTDylE83AoztVsB#o^DBBb7p@(`1Xy5p&}k#+Uu)ypNHt_k1s84 z0QgbWR<0qh?~c1h=a_R-`M_-JYs@YIxEltH{fMRK#bSO~tXJ#V#n9-l4dr)&Z*#a5 zWfZ;ar4i_+`(+h}j`)iQ>QkS;DzyUe>2}Yr!F=SMg*0 zr@F}iMske>UJfqW+ItP?E+?9K$+_CZVYe7>{Ci4Nt8BDOOC33^3{9-Xm+`f&B)`-cyv2XBX zu$f{z{_15I~xOXCInL;V~64JV2aA{x23*;UpuADLwN z7(S7q$;6uM=tIbB>r@@^Z8c9>W(*?2vi5e;KY;l~u4`6|N<4i(RcRR_I76(e>@iXg z%#*HWKpWDZ_%QdZJ7){9&K(Cbe_)2~8r1J|W(z6;KL$`1|y zf6s4*ZL4ihvUUcYTLQc~RkRPALu~w(Ix$K^g7TLXtyQq~DyKZ6a}&5rbwvU&3R6E_ za97C1QHyh?c{zvWyJDsK@FF}Wtt!_hovTj9#a{B|&Zn!bxQxcLp>Zw%iKvG5sTjlT zrReO7pn+3U%Dbqz-R=ZtJT4Dwd?ZxozVEsM#pMTEfC=EzfYPBj>-LlqEpUhjXFq<6 zWd;7GSxt=4RLF0>HHgyJ=eb$3ThN%hc-xD;@^~>YzI>$8CG+F(pPYyQ`yTpqrp9d~ zyNtWr<0PX{*??kjDDFW(UyE&|&N%wjlIhj4czrTBd$DZXTKrVJ{nCuiQbtDbcbJ7! z3PuX=7y6i@Aa@m9pC*ynL-_oc4{p@iJOYEjHeCPP#>Ma4z>6)oD6@Oom+xhy=v(~b7iixbvsZ5B>Xi|aa%7CS`V9R) z)lUhH$WmT5U+O=D(^Ke3pqV?J3D3W0#l{#l6`d@qnQBXj~Q1c-jJL zOAHcBeTwoUuRp=z6)1+|NVH9cHH2H>d1rMwJ0|xxy;b50dg-|}#(KD=>jL=6VW4!g zlJG%|+^s3TDI&UgA-7Y47{!2|JFF=TB_2g4{nb~u;MB(~ZQOz~DNq#etww&?cZL2` z&&s}9j#%b6RD31vgIKeKU9IZolji;Hq|HgM0ggbzZ58Oh#1ZO+;rnt}Bwy}OU);dr zA=;*@Z%K>ueOjVKi!@WdLcI*0R#e!yFpS*Dp)9mQg#(0v0VmX?33HE0e!e&)l%IFj|YvrG-nGw*drl2Z^+=5$z+v1w!sfE~G0m(MMh|gZ&hJj><(xr7(Jw zxLv;w^b9-_>#-XYqCsa417|8-5d)n;|FcBY%Z&j*Y&!vRqScG8!KMAe$><*YLA+ig z?4T@1bN1Rqg&5I(39IX$mH5#y%#MIPV8~6XLB>vxm&VT8KhLXV)Oz)MmCf?Dh%L8% zA{&a96nf7KWc3Y%K-uCHw&?`NOdw&+8<&=$-`!xo8K7`)2 zxCz5itd!JrmN&AAZyIKFf?#8~SpQ&dY)Taes%`r)KXY5BCXD;;=W7u_E&xO3`%cUo zKJc~Rv@@{_3UC*t@R?Q@ z%+YPku=b`qf_AQny7~5V(&n8Kjorxw-A3wo$p3xo^#=+yN~Q0%Blb#LlHLjQ7^x*) z*hrOCn~;>@gj`niwv>P@PWVY*#;0{1tX*j(*~mV9SprlABElX$9x1(6E>Z57Kv500 z+s9$@%ZSG#z#^a1bZFCL!Im0q&!pkx@-1(kPJ-XV{1<}FlpJQj-kXhPbz^9(dh33DH_)Fb1y^F*5q5xPrJob4wR?Jx@9w)^ zwW)=gX7A@)SVdU6VF1LK%JbXhCLYXUfPgCoQzR#)#TEptT-xbo6^06%LUXRlby;ykz>VhMaAdd!% zdS5j*vG7`7aCU|F-D$Bwkp%-f2pVt$Lx0wZJp3##$A~cB1B(}bh2*go1ta_*1lxym z-`9S+qT6_1MZg2zH2jw< z3S_~q3xuB>q<}p2^-A+LDqi5Wt&#(iazPEHj6j)UfwV!zpfgg`{Jm z!SlmD%n6n<@INoI9Qi|YI;s9UY{(vs$q<23TEaT`uJhM%@D_Vspt~6K>XFY4!n)uH z##J{p>MhBBa9^zWSr=gRwlJ~p@Yqhu;8!`5qP|aGCJs4(umi6i{=45O$H6=FrU94y z(^+cNPCnCbdf@W?5XJs7g$Sg{b}N_sHfZy753v6j0l~G`s-P9LvrKJnTxt13`)8#! zp~-?2NGZ%u>jn|(wGj(C{+>}HAHCH_sFQ9{*Z;I9o;|8zj*mpXyR|!dd}90IzTZqZ zdd6fJ;W?WROz6zQg)2U)ALxQv9n@p9;HS0zJ&+o=?_hl?=A|mv?^?}8)wDOb}VgLcVW_*T1LPEMZUpCk0T7e9EXmPn8gEjUj2acD^B=Ttv1?;I^WNh z{aKOk55sN1m0VU0(qx%AbwACkyzPsWOoEGjakP$B4h^Ad&o6xJp59Mw?bomOx?ll5K|UIQaov0p_+#63rQKdIts8_@`?=|4 zK~?XjgtLlzb+ODjXC$g$-qZk?{gdKZ?5EX{(&~fqw~{C?*qox{dLDCPN+JN)UE^eI z%C<;FT+R&R$IF$|IZbCUS9P<$ijv5M8S^l5z>gG<6Cpm!)oYP&D#h1JS0S3eF8je+ zH2BW`Jz2I)4qV?jLp=Xv{2;L zdc_M+ag-RP$lOkgT2DLTYNHAKhfu-fODl3Z-X0qJ5uWO0G(`!#l9rA1PLP(MvW80d z8&MDB$2>u;nZTR(Y}dhrsz>kY+p?@iOqxSaZ^i@cBwWTrPIMUwv`vk=T0^dU?lO}) z$o1Bm-pTm9*lBezC;fPSNVr>sJxck_3h`Dhz%WP1_sL$r9AAy4P3cA_tA6GZVRlY! zl3i9;>JMPd)NVYb)^m=ivqB=^l5To~Xl_2xHM%9H6)(vcre9n&_olY6H}*>oZ@{&t zy#d3@!oEtEbGnfsF4i8;+5;Njh8gV4cPooxTg z`dvR;i+Phr4vrl!J`#lvEElRYC{bGGE2fGbV@hYvp`_Ug$f;mozc!?m~F3IM@RlTi! zu@|dEGKGjco`6I18zWuRj(s@OK7f=WkR&!(wN6r`a&76VQq+9mJlh#=#M(354 z)PX!gY~$xY4~!IsJAGkkZy@gW$5{A^9J{F4^>i|lhFrv!`q)htqCYJeyesiepO*(d z*E{;Zro?)mpK=0X%K7QdO!j@M$3Q@#bT;c)(3&$woWBVAZxS8yyz7E)T4oGRKg$7HGVDRM z1Fih{kxo}Og$Qwxy-Dx+@bD&vA{Ux2b zhH_U{Md101Sg23{ulJ~ji&~L}?738D5ZoFt;r9I=J?4pl@b};M)?J8}+vY4zywc9m zLmA>sa;o-Qz>4J80U&gnx>ulF5}2IuFMqvFcEV_shUr%>@l(vsTS09hpO(x#g#Q+Y zWtN53ObP_xPO9V9`aA}#yp)O^5Gp$0%){<>@21v+SsJdTSe3>;o2{bp4Y-YZIS^IM zL33=d;aDEDUyEm8^0W4P+h2W|BZYwejKI3Z#WSG=1u`FUz&W$JH73V5dJtBxyR~#g z_o>_>J}^25xF&Fd_i5*BZExT)dvy25U#o#ASDBAjJOpXUpJ;FwUAx(DD1IpB*m*Ne>#<8a=HmI|(Evlq|xxr6}G+_3~ z$=Wrwp)}Wy^=CtuU;2Ffjyl_?)Rc_q{<@v?sy$y+iQ?4v-dT4={<09&$9%5?FQt3P zbqy-Fn}xG?e%)0_3=MdO^=R*r*yW&bq)aO^^chmpq4p#rKNc60boH18@3jH>it&Q& zwsMzI=&9_=4O?~gwy;_~P#OxmeL8fB9y$4ua_PAXP~BfRHU8ybl!J>t%+}(T(lHPr z{O_rT9D7%D2=+vA7NBn-M=N2TZkfrRdB48s^Tz5su*c)tb+_fe%p)f|zAx>3+;_~fTV7s_r@K(Nh3i$3jJ$6U zcVK7Q*;jnvn$RS#6o5$4gjMG2a` z?2uFiedNQ*Jy8;It1A$J3-ZR~0#$4r2kkwh5hr9?F-3!t2$9(3cUGTcAnnDoo2c&` z(3(IvzgG-cju30KkOM;3tnJ_l&!RvpY_XTenk$qnCC}>m-2d$b0QvwEEdtt>oQYX5 zxia}F{z@%CrI~0@dFV+=s(t30Lz|@+z@t7%d3xFCt<(O0-HuDyF&IW6ykD?wa&Qo; z^qn+t;62aCQwfjXV`u0InW6AV>a3pcrc0+sQ4MbG z;8%sn{m$(uFo^^a5-HJmZFC8qAt~*K@+IrU?ivmJu{aH|*AuU%*REhsm%i26!zi* zLUeae>*$myRB&N>KqZ>DnkEGKRgd5t86 zx+KW{O5`94unfma(D8B`Z@bMXiRi-8lr!wsAm`6 z>Ajv<>9m`B{!eOsKZtut#4eV9ge}-5{-7~hZzIraUreVocvUh`!GAgW55&U#2REe0TET=wH%!J*exZ_!abCvQhh+W_=jE!EuOlRvM#7J_^u(Ss((C4PPzkJ`v zQU@nDHON-Q!j+Cb+N}Wuh|TiTC$m=kKAjeU>JtZ~wac(0^@Er!mLitL=XM$Z$(Qs+ z0YqQP3lLNpShl{IV%Mt2rn}%!u7(NOyStP6R1a%a5nc6qGY->pba#)&;xUlg=YyKX zO@nqo8>+rF^{=;q@UOHjDBH-@G|t6sg?)fW;S&Ez`{xVmr#TXn#U4FI5bWWg_Q=%SvzyCTGu z>v){ zy}Lk9K)clyo0-DPXnUgs;={4*-qWk;5qoa;-S{E;%;s?vSRKW=zo#GYZ&AB7*q05h&ArdxktNXSx@&3>3Ui~DKlQbQx)Q;b!XJ>UUEFG%Zlv3kfP3*9Zd#tRdTtE% z%Gq}C)kpsETmf|D7FGT0yHCAXoTI-mW-k01r=Oy!sISkN8#B&5Z=8TuoYBPhEmxKp z^wY`~9XhTfWet&{n>;S)rB&tl{PANnjv2@vrRBI>=dQ2w&Y|s!&^6-QL;g}Wl&h2- zckQXZCU0ARLeZi}rM>xBr-R?2$bEcsa;D>Tm!!E2f}ARcd|fSl^czF{OwPVO3&-Ee z(8xW|D~wZWb8^S(emWiISOe)lTA<7n&SAoBQ%ij!x2MvYek9R{nFC`J7|{PC{+y!M ze(mX9bCj{oKcTpFeYU>jjN;Pw3!INsi*>zumC2u#@}}bQz)!vlJ;9-r zN-xtw2r9`y=fXqFyE$r;jDp62BA}@l!v|8?`d*>Cnf`K_zww998M2`QLfKA$wQ-11 zs~y!p{9&zp28&59(qoA30&XZa!8!9+EIFFdh$2IiMM9CWcn>KipEaX-j~%$T?_)Bv zbt)r_h1~IsTmhgKJY%3`QZ+Tts>`wPPN^`&?V#@qoP0{|V8vmMnlxg}z zVXkoIA>eF^Ib?m4KeB&D=G4fn)kHv*R9aT0;F%YB+Oq!!=I5T1+q`3Ep!piVH5yZE zEvamo^1XNG$c>kndc%aG8U5_;-bkXsQ>AO{>>|c2&&$9!7&CzNR!TCVKvw~1KK+Lq z5}V?k4Lq(mTmp@51>Nmlox5JYV8)H^pD;ucDEJkYE2!TSyja;^hLa~jROwwHGzINL z6HeGv`O!%t_QK?bSbM456-jY0kW~@XJnu!t*YOw&fP)nAHXiU0N=4F&lGGTPmgx$S z4}jaMD)XAf0t>0Db>xS3E*0q9lR@((Vdoa8yw}N$jG6}B@0D|CsHi!@uDL+tmX_8y za+owwlNZjB$Y!aKT2G)|^5uNrm$k7rqChpT6X)nO}Zo zvujmPJR=5gjIc})78lDysBAr|^VHY`=&X3EdIVQQn&WD_D8;2-LE+A|xOhOU`O_)Z zu`(z^1CpbXvAsk(j?zX|@@X~;&%eRP0S5@e&+V%VuobF!bhWlE|J^kV=VnKq>VRuH z=fE+1{r0kpBl`C1=bB%4lkgo^nFD&^D9CXHMy>{YBX)oNKpOEbZYEJ@!4E9}5$ccV zo5T%Bl14EWLlyh8eUEVk$J8qp7Lc zX80zG$6`Lqy8|M`j%UkdAVysGpiN0L5X~RZ+Ryq?HE=6>wbG5GugCu(unnw_JZg-E zqY(4Pi#c;d`Y`-+Wxt0~^DDIqaYL_fzZ* zI1tTY^-zOqY%4R9rB__@3{_EEW=re&Os9G3p8>ZLZ1?;U?fv%sTh%&!SAISEib*)iP$x?U37PVBo=|$aof2iWEKR^YLp`v z{Wa~s-~ZmM@2%()g0dQOP#fo{!OF|y8FW;b{6`VWL1!VFiIZMD<~1{vW%+r*hlc+Y z7fdLP6}cNPpX1d%S~(q_N)YP_-i*xLc|-^=C$R-J!4c&~D`YAtdMC(ezDqpvB2v%~ zUk<36bql_ccn#H~pqBH~$VJq#FbtRh>@?MF6B z^Fsouoy*L&Kt79`K6>F|WVdj7_eJ{}h#B7I>tLd^gC&722Hbap?q54ZfFCjyjeue# zkQa7@mjs>*8G=bzReNS}7kLJ5HHY*eP$y?5S-{`CqC zQwzF7lb?yeUtf6XAe{{D%lvt8VhyPd*)B0yzVaJ?%Zl_Ly#<6#G;*c_i;G-*ie7mc zy#hGn(X>hiDIXJ-xb&_ z&^C!MF0gVRbG9)Na&PrbcdrwN5N-Qy$n}`Zj}-Tb2OPoqhG+Ve(lF;B6}Nw0u}F?C zo8?Qf#U5FBM)ZQf+cHbrK5lFxL5`4cP)<1ld*>0Kp1k(y}mf7$LjXDJ1XRqja`73=gi63Pe;r(=&{9X*Ju^q_#&&Z zzG?Gfv@bFnPaEg*~jb$bCyljwX#!S$SBK$U zS`FV(7A_6lK*H&=^A8YIk&b|S$#X<=3w96XH_FPD3R+pT!fABE76T6U%$>@fpm(EB zKkCsGQ9OvS>gcE~JylcojHQ+g5S8PVx1_{wJKdkoFOQ$pF1m3aFaDjg_2QE!Bv!<~ z?cs!w_0v);ow&oV2d78d_NIX=&pUhwVgGM?{Mn&56p7GZ)p z<`!$d|A)Fdc;+~+s393eKd_Nv7#IP6&r{+Q+4-t|AH(#2+G}14A{LqIgO=x3^k8#7Qr^p*hK_7gP_@K zK0K^dQhEM%#F^0Rn#;}5i6{CRV06e5kas!IMP_k{{3JK0&i)!>o#L^VF~gi21G0~O z8|m;US+kD(3B=CLD~+rB8F^lgnHwAX+o`ePIwk7q^i8)S<&CsK8V9zAEOEWMlF@Tgjw|JQ8|LVm=j(PNVSewS zeQez7(4*ASpYsMu2IF*;g9dnkOLR!HGw?t80^n+~N@Sm>htZ%C$jRK;!<^&g4D;a* zKMUFj{GCd6gcQnRZD#SRYH; zdS(>mW3<<3ChmBNW?nTBqYkRK{jqwz9r(9^PlbvmwYnnnE5srl^sXSZ17_d7tCpYh zY2y2#MuuJRct4*Kmh0UwO<2S}w>Z2>ZCJ!HB*Ed7-WcVmAK~{R)=T|}USeP9)Nro= zT?Y5!_Q2zH@z9}Ds>INbAzv?oSPY_ONugZ!73zjMT?pi7oP1!7sr67h4CsO#&!~`b&5UM3WjK4s{ZMV}Iuf zd7GF6ls#lDCT5WKeN1nEPLEd1v4iO*C!1(LsO8zn>azofJpFa(iGcPBwDgAz%nUDx z5B{h<2tT9!EuK#d;vRc=$RG&uWuKluA(&vs$N*r}6+ZdlE_j)%m zG6jE^-BhY^cQ)NE?*tf~HTCwN)mG&-2p@3_NDB;S;VZk(|2`2SIOc#4#I7!X=XAcJ zlyjv+oirp{^KDziAwb_JOMK1mF|?21A@XX8kxo2{Md(!}$dmyp zHgeq=s#m^nT4IYY`y4{o6+~sp&J``*D?KS*n8B#4F#)M*=NAAeNT!sO3O>s?Sk+|$ z^Cq_NukM3*c{AFzO!Mf(wUt7yg-1S4S%s)PMz9MWz+hv9NP3vhUe95XGySu3Zg=FK zX*ow}fe%Q~Ki+{tFT+8vx(+`EIAwqUpO*0cE3KQ=NF?*M6bKEPL2QtwF}D%)Gjg5E z&fva<_AS(YX3Z*O$@Jmwu4ROZEfn{o4a@!P6|+ zZ3jltPn1Y+^#r{4cFU%`5QRK{CnK8A+T@O<)qWe@ztW*&^m@0+cKhe|o@O@mPeT&O zk3=f1VFf_#uPp#30$fii)CiP?IRq!5jRi=}>SU)PR(N$Dytx^ zE1&`iK@76SZGUN1qcD;nuGQ56_nFY4PX?Tt!g>DcxsGRFH~RjWnl=yks8{k@ApX{Z z>F&~UwpbP|MS=a^m~AaJ7z?2o(>315Gy91M>p9oc(A^KVn+q>r9^wc%z^Oy!HR5{? z$g>O-Du&3%b~~e_Nc_|QxoEdKUfINox`iNO%(qkH$p`HLK{c!ey&9K*3kq@v26R?^ zw_!Pfc2kZe7e2TB~cE2uZIG$rrPIqts-*I4w^z7PRianbX4MH9>q7 za4NT-=f%45D7}L#z;CWupAOu8KHlyuRh7-bHs1O)iW`ztk5Pp5c7@a;CLCJO)edq# zPtc%?I5e5aX$@3LiO*UtexJi&)E!32AIutFQG;MlB|pvA zAI2s$x^}l^jO~+%WC#VQui_ws`!r0U;bXb4?r=aA_}^c{|1u;3q3q*QhE1>B8qbDg z+RjQcq*GUcFJpwMFE>~gfjO@}t3G_8jaFlx&it?x+>rG(@~Dy!%TG~I=TM8RVD@)U z-9f=MuZ9WO_6NZj=RqW-a333XC7?t(%N-Sd?*hRfEE-vAiJkP$qM>da$~5bs(*xs3 z6}=ok-+RlON*ln>!GVb$a(wa4-@K7U#Vlc+B|G^P(Zo4+4dCBG&&WA&xP=`jGeHVg;1p%NY+`P z-wB0@(UI+FCKjY7@OH%RdhKn<#j(b6nj$iPvzK4iE&IEm$XjDFwqkASFtEyL49&2{9v_R%he2(igHkf-o~E}lr4e*s!W47rxf~8fKDlkF9<64MKQN|RCG*r zv96-=lpen#6PWlUH!kqw>szbGjws*Yr-W~pxkQA@UQPf5wqKJu8;B{)1(j-z^yq%O zBZCLQ5aHt-J0i|roUC3MWwUh7iw`bNJ%dvxRwaZ}8KTRwb)ECbb^`G~`8*1Llpz?` z_Z&|}0tM`tOTbHeRhI6JckFlr)sqUx)p!H{M>#iXx&7}3?YEm0CmGKu{-463?P_D! z`6y1>eVbSjUs~8&h8HS7eL#v=11sN&E=qfjbAh0Ye?iXuwh-t(sG&c0x{QumZbx80 z9*q6#lgW|@$ZGB76RF38{}HeRuIFq8Yu>eTBqhb>7YbK4G5u~T^RsstAm?|c3mkKS z+Nozo{kAsFD7K!6IepJW`E)rjKecCnGwnK{i80!c(5Kro zea>5C{m^%WuJ?b>CLW%RI`5IK`7Q4HanQk!zJJs8imi{~+iR`DC!U=(C>bZ+Z(ZK9HGU1S67N6<>`5{~4@^I)zPh_qoNt_ z+)96LPJ+%Qrjlk~KrR*5$BDsCQB_mN#hd$HE%aH^%|1wtK zWPdpnre*JQC#fY84uzX|RgkfH4O+NZZmjCm1Zk+g+~7^a2fG;GI`>c0NW|UE@OXp8CeimVYf^=RIDdu@BGvB#sYL7rj@TkLd8h zcvKzTY?ouO7Lu>wNclK6-XySZFpYkvf<-e3DY2i6Sq&FAQ)J{!1P6n8$cz7O*6-`k;=*fz4C_QHLQEe^qwYuh?! zLa~fJp@hVg-``8g%4~^J* z;^SEmUhr~Wl$RF;mQOfL0=oQIzEb*v4)7qBeX`ZJ9Q-a$@U zf99?Zemr)K86}bWm{9jP6*i3BM+IfU$lL!ECPD3S7w~X72eH7o6{?U06^t#!XM5BW zksB+JvZ)B2IHYrScy-rTPs436ll^R#j*E5N)F!ka1F6A%W(TEwGB%EaTdNINB z^sXkd^7Kzv3DbCQ3DZP3Et?SLSe1&7+tKs7|EtcYy`CsOtXUUizw<}6O&hn?S4w}I zFr2~8utQgVEsSF_$bu#1AWc^yCPbO8oC~plQ3M3(Qu$ViNaXOx-tC>~QvEo?zVUE! zy)Hv4!XB@ZdqO#cuSV1Q9p&f% z-V#2mo#ou>Aca20m0p}oko&Jv_>e+mG52a8V+h^%XzN?Ks4KkSonb?#G<+9_`LoLR zP7A*(ykT#j-XyFNnvdE#m%RR)bKX(GhX^>OY$$)Sbv&&ZpU|hj%mQ`_$W~v+wX3RW zU3V2m>MpYVk9X-tTICp3Hhizz-1*2+yj;)Z|P6H@a8nQJXw>5Fl);en7vkLqQt zoDo6LVM%f9tZc)*6*To?0}y2OphT**^;?GWmOXve<|0uQpr!gxQUoBOnFCPp;h{F{BJLS6`JEQ z`;La6g>(h|GY=M8C!f%*=zi zdj00**~+M(SYRu&9~nF+Abei6u4=;Or2k>Uu5Q0BY-j*K3e-fTd;pezgCj?lYLpms zeR1|~?kuN!>825Hhepv)!LGq0uU41RA{@6rxp91UJ-JIM#w#55u*G(9J@G_X>67uW zT*V>T)ztcqa&Wd!h0!2Y*jsMhFH;I+#2o$>859Kh&_2>6MMd_>Hnb;`o0o|RY09!w)S%H8@SAX>pNRg zhnZ@J1s?Fm`hK{oPgX@B5D*Ov3y^2(p#M)Gb0eXkqDZ`tGGS;@^?_a5y0R%~vk~#( z^-9D<$FAA=-zJk?^v@F`_P?>tD_A;ZEWWy)YLD%0gw?nrVOu;DrQ?E($dY5;6el}4 zIylk_i~R`JVFFFsI@6h>$s*}#;y9%LO&%vX5>(dvO0I=e~R2d%hp* z&w8G=s!>&=dRBMyK(EiO`C~wJcn8Z-KHTAwLJ_;$Il~)$f#gJ45k)@C8%bc;==Aw#oR(A{EGG`)jlz&%j{6e=v$FY#XOwMbshUWa!5~)}~qo3kKD9%Yn zZQX~W!fRzAs<~YgQKc6PwA|-WlI)vJjyP~&CQ7E0r@29f__!@E^D1iiTu+`@5k0xOsIEMSc)E_!MXH5Ix#LYOtc*2#|d1Fi0;uRMG6KC%HJ!A9Jp6sPyBhuLW z)`j18?}t!GAiEyg4A%?SZlQ{@lBMVR4hHXzKFa@=Q8_frx1Zj7rKp?SFK{#!8L@w&9+Kby%V6_JmvVBT2jCMV-p z?pAs^=JtAmGxTs1tU5tH9^3rlctb2iAIE%l`fN8bp|NVnSqgyxTzXtjTKOg>1uR;Z z=X|W|)rNvsbkddO(C+6Wocw9&ruqI^WajHQ_u1zlLooUrogU}4lW1~hQ!QV$!7+F~ zW;s0)>94OnEflbMaOBsy1G;*PJAPi8?!-YPcGPwdnkZ&6UB8u_a%YCMt+U-V33A7#;2oM*Ac<9&0!n+%o_O+H8fgF$rYlgmFL}ug9~TPC8&J+g2pS|2pTVKbD9~|=rnyG3^vtwRo zkO6XR;!aD+ox%7&-7GyH(g}E0cLF=&Gf)64 z7baPL*=H9L0o%r=S)MJjMj&Ov!%OJE{%uyxD!ee;Onkh!2&Y(gi;yoj3%a^=B)sr# zmLa(?9??ZBNTdjUeuECG1`rn@sU-JO*ioTPf2Bh-x)#OO3!pp2W$I4Nd+l3mCt$ij zfy9;0?-)~9g6PaE$j50-E3fZj2{lf52fbVS3JSO~;&2Bwy0@olD``nw8K*$P>d`#HSEp&m&GidlMzrb;G`s1s^Qo!^zCK|Y~bniU~ z^sO7{NJ?3D&A4n<1UP$?cVy}bXg5JGI-?84ENjr?H#jGu%R%J%Hy?M;JI9|)4xSn$ zPk%QQ-afBABmS{^#LJEp2W-tQToCdJSKO>_$mQ}EdhRt7Em80(hivSdX610Joipyr zIuaYJt$Nzh9f8^)(BzRJ(4N<2!Qe31la_Y$g`(>&{YGTaqBqJ9l{~19kAnpQz$1je zC6er)_fD)QGe$;0ftMsJE)wFoFql_h4(z|%GDn5Dz=b?{9IW;NKSY$S3hTMaWx%%t zUhcxzpWimOg#rTy6W?G!fn)4^(8BogNmiGsscwM4Hk)hF3jZe*$8pH$QPSamjS!JUc?FIR1T0?`twqU+F14&@U#jaCd&eGHkM4 z;eRh~G6Z_l8Z}~tH%^dMiX_m_3ZUjACY3|i z_>4-H?IX1Hhy^vFu00--Zk@0Q@?1SH1miCz_Xj@yjAv~Z4(yAM76(iR*$p9rd}=d% z__-(cj1r^oy1mC5oQCl*7OlQyyVE0Yc3!9l{_x&E)iA_f)IX)Ut3=a zeBjXjann)}wh~X=oBnbuJ%pAUrdlD(D9iBo$G%rLpmP{!e^KXzQw|s%EH*g>WgCQ9?tv%b(HW z-3pwVRL}w)_Fw|q;qc>Q>77%qqm<6_ohRD*;L90$wqX5k*sB0+R|kPAdqa7;q7TY3 z*~qC>NcN2rW4h+m$P<$HU(hYj_cEXx_z zdg1uM?^h9CkAwWBFFIpt;2c*+YysMMsJ-G%D{mrz%1FHm*<1M!nKAi~BtHwGrQb5D zin8CpS0?}k=lV@_R<&3S1PEOlzX!*_@UQ-x5q`4y!z3MtI**6G0YM*k(bh^p5@p)J zod*##Fbg$=tUdsNMM=nS-5AQXNH6~$=b|5~?+`+L6veTFAXn?T&ISH+P1hZGySVWK zOWuzcpTW8FqvlSvxgT+QbeJ1idF^m7S2t)P{nBXopaYyg8hM}sGvKK&apLd%Xg7=m zvec~=-`nyO8dbT?_S4V$?sgEznq0M`e(-rJIUfY&Q0e$AE)vih8u;-0=IlyoHB6N` zvz^&Pq4WE4NTAQ0=h@|x@2|kGQ`rf;(5xPtJ75&;Ud*vYpx@3% zEMyG`AYYuIH8>wjSMV7jH)_A%5rzi3lW$J?c31B;1pXGT{hF@Fs zL+cGDS~F-sXFcx1*K6P52b9r#%32y)zL5mU@TZfB!!GL3@+8gj;qUZX0%sBj^t@Y- zYPIK^SIN!JuW|`PfGGYD$`1yE=W(VM(;G}b%8`8u$tcp7t&rPZrnMirYa|tRIh!#2JAG~P)D@W!GkYTcBEuD20*ICar8H4+tVw&aUb@XP@VZQ2b1*x}dlkmy@W){iR1 zZeHTWGRX$Q*T|woP1q7apAU$oXV|@cnGUM3WEHg58adO%!9O#~4+Kos)TU*@^l01F z>!54b`6tTmx~F3sqV0Pchd@YChs~-e{D3smc>2K~i6u`u@BJ($ zPfGYON`>CzRpKH|Lj8uyZ*j$>Qs_+_(W(47gs#h9h^%9s^Xb#w`a+nMd?9*nx2=P> z&i0!dXaz>*@LVu|k@kAD9A7_OyXN$}qx9TtlfG8c_asJiVLN&HTX?&Cx)zuBI3*5W zFa174ATDovz7XaJ!Kf;iQ34G}rR-Lu;P|chT#>9#zprqFNNvuCMVBN)$-EuxpL3`g zp1a$`D3V3ctMRoBPxyKG32&J@ct0?GGdHNvqfVoQlv?DVVWm(#h9x~>Pa4^QDp z&J$Oblrz}^?I`lg&%_~Kmk~;)4}#-AX!*Nx$jIXED|!Dj3GY*I63>i7Q~KoaP>dwokXOSG$sHu9mQ_|R|`ctBUcY63pmb_Tw)cJ=VASjnN>ZUenm zTfAF4=fM}Q_PZzG`J7kX`p0v;zxXR3ZmD4{POo0O{zTXYptRC~6u)})F8tae-d_wP z6!XD4wE&P(3U!^A>q1;%Bz~NMZKbtnor;;w%_jC5YevKH~IFP;i= zIUHYjbo5B5Fu1^4Vzg%?{wKO|X%32_ZPac1+@U>p%bKQ0>~j6d42G_O4fT%Y1) zzk6~X!2DZ)@=83y5P|6slI3Y{On11f4{T~~QQN;1&161lSGp~x2T;7hm5TN!u1uHR zh`NMqsysnZTE9B&AbEMLb0m5AT{W8;PH}4={KJFNd})h1>(}oM|mEGuXt{T zr<`|+OY;kD7bfg0A+W8Fd~JYnK|g?>6g(E*sRN`cdEUl5%QHcyYP?26Mk`>i$D+O56)83j^>_d1SbLS_Sl+8Zg07qxMM$7_(fog zIm@G{D^(Q3nFLL7XqRs?dIS3z&hc(2FeL8`g6NKa)GdCmtkgPsE?$sh`&9PZ`|g;g z@%Hz>vDDV%w>Tg%S3N1*C*Pb3_VCfXI1^nddRe@8AKN^2KUiVY)WTs0Vh-09l;~nw zzu|B(_6;0pBW#%zDk{LLl%t=NNp>9Q&*A5I?PoC)27=M!;27i)8Z@9+J?IyM0~TXO zrJQ;gD9L$fylqBWJbEpa>6SD zksFl|@In(Q^U+C_BSlbd6Vz8<~^$$AOMV*gYGD%R~FF-${-b^=!^9_EfjGl`sesnBX zA{Sqz92**JHjSmB4_}S#lUA9x9rAj@$U0!9Xc2Ib#RHDWf}kvXsTB3)Zt8K@&j^} zRcVtwl4GiPI0fQz2e>xV1MPFmnFIX7=k0%?P$4+x{%I*#ri0c!Zyp5F?IdsQ9H)}E z;`S-2T$*(g)D$7Cj8gG)6|u4Fz3I81aPb9$&v#5wXm6IvBxSs;^}}PfbGvf!JT1O!=M46rNn6FBv#wZL+R;@y zqXmiFq&f7mX4^Mt-Ve~0rb=t-7MfUo*ozGAwK{XMU($IocwCfs>aS!L`1bLi@d>Cv z3o^6`KrpgnHF9p05r4wB#m*238>YwA6MtikDBnK`QF!!4x20*d^CJ1{U_}jf1h*6X z=e9AqMDR`zU?4417Y@Kc-#a-eY*gsLW*V?4_;&syG&&_Y&F)VXf-EKcCvs59mLOO4 zx>;L>9?x>p{;&2)x^O}DxqmZ(CttOi^4}IQwAPg-=1tK}Gl>(_BdFK`@)Gs4iq)Q$ z`YwD1SJHC|t3%{QJA4e1uh7KPqsI{k@JNv(jq+TJ-!OzU&L*lzEoe(JyX1>7B!7}) zTO&1s__i@i=rjOR`u}T+2sZ8R>j|Rt=*z&s-v*Bz$R&KrcK*UiKN5#SLPnu=I6KQf zesh)!p196T=Tr;`{zqyDjG)vaaAW}<0X(O8S)M)}2B<

r@R8DskCg$QY%d=x6zD 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

!LxQh!NmF zk`(0Tj<ZW5E8&DpXim*G3_g%p~m+WPP3v>mv`TF{tA#CZlAV1iI z$sbTihG%;$dY@ID{z$-{<*^l96ez;5-yPlm9<#0Vp(DmRMjt5}mO&)*|Ao}9w1NT!U!3v{BR>pG3m4dvAv-jR< zR##iwb^`%3Z8MMRLF*iNu zmF$BJptdopP98FT%?meHALC3)l*?Eak7wgYuQrXJ_Eu;ZwiG;`^w{1|Op42fT9g|_ zsshz40(mPpNJVTIg;l?C(@J!tO!HQn8OY;fg^<T3@U|Sc83Nq zOA#FoV9fdJ-PZT9?Dwwq6Kp+yW$6+j$E+2gEKV)%rCetTnm49eyPU7DZk#q%+pW~y z|B0KsLwG6D4WtEQsJaB!E_;~4mYqb}Yk`PDRW*g$2-eD;kqA^yXCsWBh2TB7O9xEH zMx{eH8;>r~9Zc${$noP^Mu!$ThYQfe*2=(d(b)gw;QQqxDX3?It`b{AekTLUF7Gb?yx`*nAz;?yh5 z7*w$JXKH%7VCZ3i~H`= zlE&Bef$)~x%WH6)Z!*-LrE7XzE52Q^$yk%qEh{E$wz}DjokiB|S1m0hvNysa2*7yx zRs-~^|HTL(s$@{Nh>l8Kiq&75AW8<|9L$KoUWACkuW*yoq&mH?Kuqg+SJ6RO&dhv<{j z_$#!^w$(5>*r|^qC%a+CrN&>={f#9E(8 zJq;|$^KYMHGhb#$GV`L9DA&Tu9WVS=`^R-~QI^QPnguaC>2Sx$j= z-DutV&bp-LooA|saXYm0#D?5tVq=*c1p_JM)&sN!Dh&h8$0s8+pI&UM5-VKtLR^|f zR6s3K=#)Hww>x}V{+W1FY5O9Ni&J-Te0)w}mk>YIY2Ed=li}W)<=Itt!TYdl7=Sh z;#qSU=Y^){>z%OJwY9FtNm9bWx!lVEY)KUXd6US~c+`>?LTC%RKVj5XescPDCNiH? zhV{psT50&XMGX!8$9>GU06DboPbf-;-0P_;zwX6JY=spxmQ`z`k{a_CoKYvuhcgq% zgGHcIBXvS5q}ECb@4U`Uv3cE^d^1{?t!u$Wa;s?YK3?q!GiIHHU~SSePBrQbcrT5l z8M7Uyd3zr$y^wcP^4z>MVgQId{@6WNWId7%zhfB)DWt2*V6E}%sYf?CA#7BqKby!E zU`eWB^{=NNul)2nr$iTYbH8GLlSZZt!UO%d;Gt)uXYZ?PO5s?9?A(C0%~LOn+LFYj zHJ8#<=lC|m+5=%H0=Ljrao{)uL_UwRPr~}iSWP>_`@!$uN^%{oWBg@5H}L$#;Jgdf z!IXj`;4o57A;S-*j_r!|EKOdBhcG&#e_(%em3P_vDnCUDx8|3VoIqSZfy-a4*ZSFi z#>Vwd(~o)X^;%H3HdsLQ)%uy?KJ$Ks4%*DVf(g|Of%ZgWvHhXL5>dX|b=*{QpTuS< zY+o-T7n6<7X)^uUH98tYoijiGlpxU8=iLh%+h`TXTx8j@M=NbKq{zu}{EMHE&bJ&f zAvqz1V6+C09m3dYl2P1^)rViuhQ9oUch_HpDv(~XwWJ(y(Lw-#dmXkHB}mJ|u;={x zv)>iJy8ehjP`Ptfx=}ZLJrfVz4?8rtG}uDPDR%?+|^7l}C~k1Va1A4ABL*j4-5YPgh@0zLBW4rw7M~5mML5 zU!bcsd?0e)a*n=uTDm(cjd0oa)goDH2@JvjqT{!l9z5|MwH$j-HfjtOJPEe6xw&HA zDzIn5mD83mi{T8({+5!(lVr1+%ufiGfG;>$l57u3ymgDvWU zPfwnwk6F#JaGP5ya|5z-+dBNVSr)5JzkXO7?QI@E&vE$e3${65=*1sZ1fr=ziku@< zjp59%EX{DohXgIT^5uS*`h-g$`#Ch7w?%%4F6?c$ta`y)eHe%+QEVtHdycoF>h z>`C=EU!DYpHK9;x@0ss#bP)NseFhK8=&e;J!@F*eTlpZWJDr&>T+)`zhv~xpkd@?P zQ5EB<#nIV{q$a?8i@cpKmgTSwTpjDk!xJBx{Xo}3#vHxLOR>eD{GF$r|E^dK2=taUu+Z1BeUQ&{hSd$r1wazZe!Q@+T17Tv1}>tf2fd$2Nyn?U$JsM zyB!4eW0HQ#l)ln>Wt^!eESx#I=t1!u;(-OR zDi4rxJ1lGCK7(2>UCrQt9fSKQKgXhyV{00YtH5c*kiA?>!o#Y21M6-c>1(GS&vb?F z9yE4|k_CZ*+RTjnz24dQR$62HNpv3@#JNtx=XZhbXU{9Al6fbRd8hr&ge*AL^!A^9 z^U@3Zd)NFe-@uL9Ourm{4U}GIi@g{AN(4zuk}-o1#ck|)%75<}_}9?Q^QNBbQCu&D~>g|fXS&?W#NlX+5tOBn*Rw|xJT!O2)0z3GsE5^92gs8eQHcPOZ6*%RHF^Y6E_UexI^;&$1e65lU8 zzURAt@cfJ0&H~>Rci8bHc)f|H>;S7Splj*yt|YG z`VqJOyMSMgSBmLAC8MjFaf*umWyrGe)rW~+atV$GQ%a2SPii-~BUrA!*VhR?7FL`F zkiI!iUflzM>FUO%`#ASQPuj8XbDU}d<=UNWq%{tXb7F8~VgGVO4K5mhK}0gNTIuVv zbu}2c{L<(GpYN3|^iIlX*9mQd9UZw#X0FREN$XWSt}9&F9ur`kTp}?G0|&AFIObV;}q{3H7ifApmbTo~QAxrwPNsggCKC z%C*}lG-p1?$%34*^c7n7g_zzyPqGA^oWo}amf!`cU~q{Z?|mWzKzMR{I`2Bcef2*k zxF4R>En2%3i^?wLg2~Z{PVHi@}_fmO8#q58n^EZvC zaZGjXuko-eXOFYtQr9!JH92}?8jo^=?2Lwdc^`6cuoE^aT;)rJ3`V|%A-c8Bhn~(C zo-D>^p@?^gq64cD*4jHdGQavS4W?FEo_S-Lui3k^l&Wr_QUGNEe9PeO`;m`1If9`p zR~sKiEWA_qGtY{<*dVIh1)KfnS_Tq1u(iy}Dq<>!jAFkbLdhr-atXfsP5Eclsj!Rh z8|yNiG00v9i|rp@Ixc#0UZ%M=43Mp}#k1XnluVb#$j?kB7?)(1Zr#scqFzJ>;_vug zNTm^g{^aY#2)Lv+x%3)OiJ1ihh^?V(`|)~88X0WDitL_fdOaO3PK?<>K`2g)OXRF} z?yB##-p;0_70X&NTb0T+e4sUy=2sZ|@I0(e9RGZY5mGF;NzOK(^v-Cyxa%F950!K< zXS=z07oU8?QasUbu)6ZpEVK_=11R%L9!%(2PclE#2b6Z9uDD%ams-~BqD5>D6~fGH#FaxfGEwGY)El-(V?X`)dIfhir*9wFvi!M>Hd=|o zZR41~CGOd!&1y}aT=DJx+s2iu3-);X78#0PcPdkg9pokYOz{Kq*UV_+#8yYrmsTI zh{m(w`ao`s0^ek=1|crEcTPYn7kahJ4(OQ-v%^NmMG)0iou zi+A?|zsN`Y;6F{vO}W{3;57*Ww-%S8i{ZzOaK4a$lHxfWEkwRoyMLM1`ek`2ZjC{A zvN-f8)c`{;SK>zW$nR@pEwgz`s3OSY)9bdpr&<4tqZDbnA-6aL^sEtQplCDIk3;0s=4C zSZ!%Be>8dw->q-2iUW0s=WSIC)nGV-NZIE$iXud120g7GJyK8n?&JPHXRbY~8s-vz z3+}%p6wnG`w%I?P>=`{uExEOr$XyU!Fy0Hq(=quGX3a*J|FzFbTVy(MH{MLILsLY2 zAa{&!F?Y07%Iouf{w@n5wqfT89~yAFck4ae3Q;o#ypX~}wL-adz+({02ZUyt`{vt6~~LQ6sfJb|+Ap4Jx3b)0?@``W9(A4h%AknA+m4TvJ@d6JMrj=!H{KyB4lYol(CCs$yl-*OAoS>wMcd; z@=ovjFTB@vu5*4p=f3aj+@H@m_vfz9Yc|Gbo9tYDNzd*ERxWShzDYjGrzN&;m^1Ip z9@P)QcQbdi@!d)O^3P_B1?VT6ZmMSh)E~#OL$8^xduQYc?~OIQ`Fkb(%p< z)FXMnc=dddZD%ix=jBXF=iVB+fH4}ZoHm@VZLm=)URkHeRazxApF&k@=Yp&2`capl zJq*!Gu@@NB#zQjhxBm>JWl`$t5TUp}w8#)N#?qq0`K0g*Z?V>Xq9PQJdUthN}xe#%^jYy6N5K@^*a zf13LBH#zJb-5IqubNR9#`^CG$d`WKh>>P8SZ+y zGF_OG;hkX@U-b=rFEiiC7-w@mj`{msUwTWL%xUEG_nn9U?jJ|>*8!=#Yiw{ZFh|U7 zZ(f{@^d@TK#&42?G>5tV8ZHUJ@P<+!m~YBHD_IQ{gcAO(@8tu#Az@J2>|TOZUpE)# z0)H)jOzM?G3`h~;k^ZkDFZk8i!Nv;N+NmnN*}3!`Ce{(XlJRLsLW%Xo9v>Y+7OAb^ zgxebXY!45DF8#7Kem4v$b90c;_cm^TE4-1ue#XJm#n>SO%RR|C`I^n>z+WwizJ&Q6 zYDVHxX8IZ%c7M_=rj{cd>Cw2mxp6CMwBrQFxEjXHWDhp1G_sc$D<$6NS7va(VZO;K zQ$AF+N>jb;dd(AP1$bRb`Jy`sx7w+(Njy50R9D%Q9ty@8o@$20;h9)irTN5_N&N|) znV%Yjd>bbn{H&V2;odb187WnRhG4M`(c2X{MK+wm&r9mmmi!I3>zrbpE_C+2RsUX0 z9$J>tGT5|ygY0qGo4hmN{b=%KO5>8c@Y7*tqsSVzMfJy58lI^5_!drmsml>qkI;R; z|KHSyRvVqYS`sop&k%_+o#tb&?QOngM5RvygtoH+UfvT{ZMv-BD20Q1a&`jiD>Kvf zjZ6|Bqawbe!xI=nAt)sZL{glgtBn<0XiyXl7C8@7l4k%-BrtbAV^y4%#A)horoIq5 zH>PI;Cg`A}0VFWAt%!jHq&Gr-Y&kMPEUg>{ON)^VNN42=07izvU~wQydQh!mcz&)h z`m0iPi>wgVBs|A9K=Fti!fhgSL*rz)V?TKm@=<0(5ycKd10JxuQKWy4;f!WT(~1)9 znu`{+Gc12E;hLsYW@a{a4Keu=Ng$4ZrvOY#l119B;&BrY{JG%JOoMUTdky=S1(YM& z*PJw<8hpv{b#v}X>R@_>al@|#CTh)2*?*A=_b9j>puWS%x5SROG{GPRM@)`^kzr2X zZlztAG&S+BF{!sKXMR*NnVM1=7z-f^Ub(?uN2?@MR>dMSC**5jH6l0MB*Z1SZWN7P z5)nO5L7x*zl;L;Lv&uxUr0QE)tK;!lCI}v@&4DyWa+vkn+SHiSvc%84mU)4emO*gH z$RH4Oc#0^M7+e8iEdd6?sEOFfXeoe_27pwV?(IcA7z)$1UX~YlstnaEq3jBRMvfQrDbyL{vA;O|7sRvqy@88NY)8)r+6HVnUi*oRBMU6cBdIv*6h! zS?zcgJW?`L;Rxp3d5tzM#y(Y6w1>TZ32o@BwN#Mc?YPvrEYdhP=ErB`^^; z&Vi^jgnWF5EXz5pF`37rR~pvAc_R|@B)N@ms>pzgqMDcpd7^XHEwf{;@9^MzyuPm1 zank9;g=4Qj_KQA=#Q=Vx_0bg|YD31np_B*Hr>Tuwg?d(}e4Q6ez)7ytjD{SqzN%e+ z+&%ggAfLjj*V2{IF zf0V&0i0NLcpS)HMpcO0Lk``KbvZ6}y4U-fwd)3PR6+;*cAyXM=huqyi|8ZN+p{~8wLc>HfH|>i9p~F!B3>AL-QaHRQt0d|VSP2u6|W6p>ITK3rQ)EE zhvjj)cus9Mrc@Gjr7-xbfA<^L7X`_=?ItsS=skkk&iloos&DSaQG&DLT zOI{z1Zj+@%pWx#tM|eGAJiCI3+gEUhQeL)X?7UcU8@L z+#q=60Mf6 zT1pdtf`IhqRHv9bcEk|OC0=YaqEfO+h(IN8SC}!Jv^?)1IRtl&xL-OR;ZJVN2nlv~ zzh;s<$hIZG`MZ`*$v(dgDarre%yM@`=x__0TA&BK^hlV5>WJmQ(>DCAK)jz{^hRNHj_HWX4zN^0(Ci^qt_IOHLwE!ZgPy?GEn{)>N(5xCD0KrXJxI}6F zM)}xiL88)b@o0I7=Y+uR2R^wICq0fc5u5kk%RQ^J&%5j8ux7l{{M`=4droTKmPtaPfGI|6j`F z6T!vd*UKki^>*Q*5fkKDQ-Zp1Skl03k&wpH{$=K=!Tm>vANw5{dE zFGXJL^NRGI5L4#?s*dXD*e1gs7*ea5LTPED*hbKWDKVj&Bg}xlGJ9Hx3r-bL~Gy) z)(cjuf?Po7%>I)vF<0B>e(^+1PqEpklwSNDo4a_CF18wPc^Bt2S07$Y8%W+h + -
-
- Loading +
+
+
+

Powered by

+ + Avalonia Logo + Avalonia + +
diff --git a/src/Web/Avalonia.Web/AvaloniaView.cs b/src/Web/Avalonia.Web/AvaloniaView.cs index faba09f66e..6d7ddeb022 100644 --- a/src/Web/Avalonia.Web/AvaloniaView.cs +++ b/src/Web/Avalonia.Web/AvaloniaView.cs @@ -74,7 +74,7 @@ namespace Avalonia.Web { if (_splash != null) { - InputHelper.HideElement(_splash); + DomHelper.AddCssClass(_splash, "splash-close"); } }); }); diff --git a/src/Web/Avalonia.Web/Interop/DomHelper.cs b/src/Web/Avalonia.Web/Interop/DomHelper.cs index f1d98a9d24..c02bc4dae3 100644 --- a/src/Web/Avalonia.Web/Interop/DomHelper.cs +++ b/src/Web/Avalonia.Web/Interop/DomHelper.cs @@ -11,6 +11,9 @@ internal static partial class DomHelper [JSImport("AvaloniaDOM.createAvaloniaHost", "avalonia.ts")] public static partial JSObject CreateAvaloniaHost(JSObject element); + [JSImport("AvaloniaDOM.addClass", "avalonia.ts")] + public static partial void AddCssClass(JSObject element, string className); + [JSImport("SizeWatcher.observe", "avalonia.ts")] public static partial JSObject ObserveSize( JSObject canvas, diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/dom.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/dom.ts index 48b6f9c45f..068fe08620 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia/dom.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/dom.ts @@ -1,4 +1,9 @@ export class AvaloniaDOM { + + public static addClass(element: HTMLElement, className: string) { + element.classList.add(className); + } + static createAvaloniaHost(host: HTMLElement) { // Root element host.classList.add("avalonia-container"); From 203cd8f036c8955557cbc013215a68720f5c2f34 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 27 Sep 2022 15:13:57 +0100 Subject: [PATCH 053/169] downgrade netcore 3.1 sdk --- azure-pipelines.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index a5dfbc16a5..2610134f02 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -30,9 +30,9 @@ jobs: vmImage: 'ubuntu-20.04' steps: - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 3.1.423' + displayName: 'Use .NET Core SDK 3.1.418' inputs: - version: 3.1.423 + version: 3.1.418 - task: UseDotNet@2 displayName: 'Use .NET Core SDK 6.0.401' @@ -72,9 +72,9 @@ jobs: vmImage: 'macos-12' steps: - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 3.1.423' + displayName: 'Use .NET Core SDK 3.1.418' inputs: - version: 3.1.423 + version: 3.1.418 - task: UseDotNet@2 displayName: 'Use .NET Core SDK 6.0.401' @@ -155,9 +155,9 @@ jobs: SolutionDir: '$(Build.SourcesDirectory)' steps: - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 3.1.423' + displayName: 'Use .NET Core SDK 3.1.418' inputs: - version: 3.1.423 + version: 3.1.418 - task: UseDotNet@2 displayName: 'Use .NET Core SDK 6.0.401' From 52f196e20e2800c9b14c9b40740a1eae4492d8dd Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 27 Sep 2022 22:43:41 +0300 Subject: [PATCH 054/169] Build? --- nukebuild/BuildTasksPatcher.cs | 12 +++++++----- nukebuild/_build.csproj | 1 + 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/nukebuild/BuildTasksPatcher.cs b/nukebuild/BuildTasksPatcher.cs index e3766ae23f..5fd331035a 100644 --- a/nukebuild/BuildTasksPatcher.cs +++ b/nukebuild/BuildTasksPatcher.cs @@ -17,8 +17,12 @@ public class BuildTasksPatcher { if (entry.Name == "Avalonia.Build.Tasks.dll") { - var temp = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".dll"); + var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempDir); + var temp = Path.Combine(tempDir, Guid.NewGuid() + ".dll"); var output = temp + ".output"; + File.Copy(typeof(Microsoft.Build.Framework.ITask).Assembly.GetModules()[0].FullyQualifiedName, + Path.Combine(tempDir, "Microsoft.Build.Framework.dll")); var patched = new MemoryStream(); try { @@ -57,10 +61,8 @@ public class BuildTasksPatcher { try { - if (File.Exists(temp)) - File.Delete(temp); - if (File.Exists(output)) - File.Delete(output); + if(Directory.Exists(tempDir)) + Directory.Delete(tempDir, true); } catch { diff --git a/nukebuild/_build.csproj b/nukebuild/_build.csproj index b9bb4f24b8..9100509ec4 100644 --- a/nukebuild/_build.csproj +++ b/nukebuild/_build.csproj @@ -18,6 +18,7 @@ + From 9814d63f071ba407dd3464c23e37c6597b84d9bb Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 27 Sep 2022 20:51:12 +0100 Subject: [PATCH 055/169] Revert "update nuke." This reverts commit 3feb132de94c82ce73a8a8977e9ba495008962ae. --- nukebuild/Build.cs | 6 ++---- nukebuild/BuildParameters.cs | 4 ++-- nukebuild/DotNetConfigHelper.cs | 4 ++-- nukebuild/_build.csproj | 2 +- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index b0047b1f37..4bbb667154 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -39,7 +39,6 @@ partial class Build : NukeBuild BuildParameters Parameters { get; set; } protected override void OnBuildInitialized() { - Console.WriteLine($"{Solution.Name}"); Parameters = new BuildParameters(this); Information("Building version {0} of Avalonia ({1}) using version {2} of Nuke.", Parameters.Version, @@ -144,7 +143,6 @@ partial class Build : NukeBuild void RunCoreTest(string projectName) { Information($"Running tests from {projectName}"); - Console.WriteLine(Solution == null); var project = Solution.GetProject(projectName).NotNull("project != null"); foreach (var fw in project.GetTargetFrameworks()) @@ -165,7 +163,7 @@ partial class Build : NukeBuild .EnableNoBuild() .EnableNoRestore() .When(Parameters.PublishTestResults, _ => _ - .SetLoggers("trx") + .SetLogger("trx") .SetResultsDirectory(Parameters.TestResultsRoot))); } } @@ -231,7 +229,7 @@ partial class Build : NukeBuild $"{XunitPath.DoubleQuoteIfNeeded()} --propagate-exit-code -- {testAssembly}", timeout: 120_000); } - ControlFlow.ExecuteWithRetry(DoMemoryTest, delay: TimeSpan.FromSeconds(3)); + ControlFlow.ExecuteWithRetry(DoMemoryTest, waitInSeconds: 3); }); Target ZipFiles => _ => _ diff --git a/nukebuild/BuildParameters.cs b/nukebuild/BuildParameters.cs index c5fed21136..1826623674 100644 --- a/nukebuild/BuildParameters.cs +++ b/nukebuild/BuildParameters.cs @@ -74,11 +74,11 @@ public partial class Build MSBuildSolution = RootDirectory / "dirs.proj"; // PARAMETERS - IsLocalBuild = NukeBuild.IsLocalBuild; + IsLocalBuild = Host == HostType.Console; IsRunningOnUnix = Environment.OSVersion.Platform == PlatformID.Unix || Environment.OSVersion.Platform == PlatformID.MacOSX; IsRunningOnWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - IsRunningOnAzure = NukeBuild.IsServerBuild || + IsRunningOnAzure = Host == HostType.AzurePipelines || Environment.GetEnvironmentVariable("LOGNAME") == "vsts"; if (IsRunningOnAzure) diff --git a/nukebuild/DotNetConfigHelper.cs b/nukebuild/DotNetConfigHelper.cs index eca1e2684d..932525288c 100644 --- a/nukebuild/DotNetConfigHelper.cs +++ b/nukebuild/DotNetConfigHelper.cs @@ -46,7 +46,7 @@ public class DotNetConfigHelper public DotNetConfigHelper SetVerbosity(DotNetVerbosity verbosity) { Build = Build?.SetVerbosity(verbosity); - Pack = Pack?.SetVerbosity(verbosity); + Pack = Pack?.SetVerbostiy(verbosity); Test = Test?.SetVerbosity(verbosity); return this; } @@ -54,4 +54,4 @@ public class DotNetConfigHelper public static implicit operator DotNetConfigHelper(DotNetBuildSettings s) => new DotNetConfigHelper(s); public static implicit operator DotNetConfigHelper(DotNetPackSettings s) => new DotNetConfigHelper(s); public static implicit operator DotNetConfigHelper(DotNetTestSettings s) => new DotNetConfigHelper(s); -} +} \ No newline at end of file diff --git a/nukebuild/_build.csproj b/nukebuild/_build.csproj index b9bb4f24b8..e4ad76de56 100644 --- a/nukebuild/_build.csproj +++ b/nukebuild/_build.csproj @@ -11,7 +11,7 @@ - + From 89b55d8e7e276110c86013549ea4cd48c1d95f2c Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 27 Sep 2022 20:51:27 +0100 Subject: [PATCH 056/169] Revert "update nuke" This reverts commit d66229663dfe22076e91fe55fece74b611e0d821. --- .../build.2022-09-27_08-37-17.log => .nuke | 0 .nuke/build.schema.json | 148 ------------------ .nuke/parameters.json | 4 - .nuke/temp/build-attempt.log | 5 - .nuke/temp/build.2022-09-27_08-36-07.log | 30 ---- .nuke/temp/build.log | 0 build.cmd | 7 - build.ps1 | 50 +++--- build.sh | 60 ++----- nukebuild/_build.csproj | 1 - 10 files changed, 40 insertions(+), 265 deletions(-) rename .nuke/temp/build.2022-09-27_08-37-17.log => .nuke (100%) delete mode 100644 .nuke/build.schema.json delete mode 100644 .nuke/parameters.json delete mode 100644 .nuke/temp/build-attempt.log delete mode 100644 .nuke/temp/build.2022-09-27_08-36-07.log delete mode 100644 .nuke/temp/build.log delete mode 100755 build.cmd diff --git a/.nuke/temp/build.2022-09-27_08-37-17.log b/.nuke similarity index 100% rename from .nuke/temp/build.2022-09-27_08-37-17.log rename to .nuke diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json deleted file mode 100644 index 5bbc3d6915..0000000000 --- a/.nuke/build.schema.json +++ /dev/null @@ -1,148 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - "title": "Build Schema", - "$ref": "#/definitions/build", - "definitions": { - "build": { - "type": "object", - "properties": { - "Configuration": { - "type": "string", - "description": "configuration" - }, - "Continue": { - "type": "boolean", - "description": "Indicates to continue a previously failed build attempt" - }, - "ForceNugetVersion": { - "type": "string", - "description": "force-nuget-version" - }, - "Help": { - "type": "boolean", - "description": "Shows the help text for this build assembly" - }, - "Host": { - "type": "string", - "description": "Host for execution. Default is 'automatic'", - "enum": [ - "AppVeyor", - "AzurePipelines", - "Bamboo", - "Bitbucket", - "Bitrise", - "GitHubActions", - "GitLab", - "Jenkins", - "Rider", - "SpaceAutomation", - "TeamCity", - "Terminal", - "TravisCI", - "VisualStudio", - "VSCode" - ] - }, - "NoLogo": { - "type": "boolean", - "description": "Disables displaying the NUKE logo" - }, - "Partition": { - "type": "string", - "description": "Partition to use on CI" - }, - "Plan": { - "type": "boolean", - "description": "Shows the execution plan (HTML)" - }, - "Profile": { - "type": "array", - "description": "Defines the profiles to load", - "items": { - "type": "string" - } - }, - "Root": { - "type": "string", - "description": "Root directory during build execution" - }, - "Skip": { - "type": "array", - "description": "List of targets to be skipped. Empty list skips all dependencies", - "items": { - "type": "string", - "enum": [ - "CiAzureLinux", - "CiAzureOSX", - "CiAzureWindows", - "Clean", - "Compile", - "CompileHtmlPreviewer", - "CompileNative", - "CreateIntermediateNugetPackages", - "CreateNugetPackages", - "GenerateCppHeaders", - "Package", - "RunCoreLibsTests", - "RunDesignerTests", - "RunHtmlPreviewerTests", - "RunLeakTests", - "RunRenderTests", - "RunTests", - "ZipFiles" - ] - } - }, - "SkipPreviewer": { - "type": "boolean", - "description": "skip-previewer" - }, - "SkipTests": { - "type": "boolean", - "description": "skip-tests" - }, - "Solution": { - "type": "string", - "description": "Path to a solution file that is automatically loaded. Default is Avalonia.sln" - }, - "Target": { - "type": "array", - "description": "List of targets to be invoked. Default is '{default_target}'", - "items": { - "type": "string", - "enum": [ - "CiAzureLinux", - "CiAzureOSX", - "CiAzureWindows", - "Clean", - "Compile", - "CompileHtmlPreviewer", - "CompileNative", - "CreateIntermediateNugetPackages", - "CreateNugetPackages", - "GenerateCppHeaders", - "Package", - "RunCoreLibsTests", - "RunDesignerTests", - "RunHtmlPreviewerTests", - "RunLeakTests", - "RunRenderTests", - "RunTests", - "ZipFiles" - ] - } - }, - "Verbosity": { - "type": "string", - "description": "Logging verbosity during build execution. Default is 'Normal'", - "enum": [ - "Minimal", - "Normal", - "Quiet", - "Verbose" - ] - } - } - } - } -} \ No newline at end of file diff --git a/.nuke/parameters.json b/.nuke/parameters.json deleted file mode 100644 index 42521bb7dd..0000000000 --- a/.nuke/parameters.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "./build.schema.json", - "Solution": "" -} \ No newline at end of file diff --git a/.nuke/temp/build-attempt.log b/.nuke/temp/build-attempt.log deleted file mode 100644 index a926c68865..0000000000 --- a/.nuke/temp/build-attempt.log +++ /dev/null @@ -1,5 +0,0 @@ -47be5be6de651ed0c6d6996318ce431a -GenerateCppHeaders -Clean -CompileNative -CompileHtmlPreviewer diff --git a/.nuke/temp/build.2022-09-27_08-36-07.log b/.nuke/temp/build.2022-09-27_08-36-07.log deleted file mode 100644 index 2cba08f7bc..0000000000 --- a/.nuke/temp/build.2022-09-27_08-36-07.log +++ /dev/null @@ -1,30 +0,0 @@ -V | | ArgumentsFromParametersFile.OnBuildCreated (150) -V | | Passing value for Build.Solution () -V | | InjectParameterValues.OnBuildCreated (100) -W | | Could not inject value for Build.Solution -System.ArgumentException: Solution 'C:\Users\User\repos\Avalonia\Avalonia.sln' contains duplicated NestedProjects entries: - - {4D36CEC8-53F2-40A5-9A37-79AAE356E2DA} - at Nuke.Common.Assert.True(Boolean condition, String message, String argumentExpression) in /_/source/Nuke.Common/Assert.cs:line 34 - at Nuke.Common.Utilities.Collections.EnumerableExtensions.ToDictionarySafe[T,TKey,TValue](IEnumerable`1 enumerable, Func`2 keySelector, Func`2 valueSelector, String duplicationMessage) in /_/source/Nuke.Common/Utilities/Collections/Enumerable.ToDictionary.cs:line 46 - at Nuke.Common.ProjectModel.SolutionSerializer.GetGlobalSection(String[] lines, String name, String solutionFile) in /_/source/Nuke.Common/ProjectModel/SolutionSerializer.cs:line 92 - at Nuke.Common.ProjectModel.SolutionSerializer.DeserializeFromContent[T](String[] content, String solutionFile) in /_/source/Nuke.Common/ProjectModel/SolutionSerializer.cs:line 63 - at Nuke.Common.ProjectModel.SolutionSerializer.DeserializeFromFile[T](String solutionFile) in /_/source/Nuke.Common/ProjectModel/SolutionSerializer.cs:line 23 -V | | HandleShellCompletion.OnBuildCreated (75) -V | | GenerateBuildServerConfigurations.OnBuildCreated (50) -V | | InvokeBuildServerConfigurationGeneration.OnBuildCreated (45) -V | | UpdateNotification.OnBuildCreated (10) -V | | UnsetVisualStudioEnvironmentVariables.OnBuildCreated (0) -V | | HandleVisualStudioDebugging.OnBuildCreated (0) -V | | HandleSingleFileExecution.OnBuildCreated (-50) -V | | EventInvoker.OnBuildCreated (-3.4028235E+38) -V | | HandleHelpRequests.OnBuildInitialized (5) -V | | Telemetry.OnBuildInitialized (0) -V | | InjectNonParameterValues.OnBuildInitialized (-100) -V | | EventInvoker.OnBuildInitialized (-3.4028235E+38) -V | | Target-unrelated exception was thrown -System.NullReferenceException: Object reference not set to an instance of an object. - at Build.OnBuildInitialized() in C:\Users\User\repos\Avalonia\nukebuild\Build.cs:line 42 - at Nuke.Common.Execution.EventInvoker.OnBuildInitialized(NukeBuild build, IReadOnlyCollection`1 executableTargets, IReadOnlyCollection`1 executionPlan) in /_/source/Nuke.Common/Execution/EventInvoker.cs:line 27 - at Nuke.Common.Utilities.Collections.EnumerableExtensions.ForEach[T](IEnumerable`1 enumerable, Action`1 action) in /_/source/Nuke.Common/Utilities/Collections/Enumerable.ForEach.cs:line 17 - at Nuke.Common.NukeBuild.ExecuteExtension[TExtension](Expression`1 action) - at Nuke.Common.Execution.BuildManager.Execute[T](Expression`1[] defaultTargetExpressions) in /_/source/Nuke.Common/Execution/BuildManager.cs:line 58 diff --git a/.nuke/temp/build.log b/.nuke/temp/build.log deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/build.cmd b/build.cmd deleted file mode 100755 index b08cc590f4..0000000000 --- a/build.cmd +++ /dev/null @@ -1,7 +0,0 @@ -:; set -eo pipefail -:; SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) -:; ${SCRIPT_DIR}/build.sh "$@" -:; exit $? - -@ECHO OFF -powershell -ExecutionPolicy ByPass -NoProfile -File "%~dp0build.ps1" %* diff --git a/build.ps1 b/build.ps1 index 997e5b423f..985e8abcee 100644 --- a/build.ps1 +++ b/build.ps1 @@ -1,12 +1,13 @@ [CmdletBinding()] Param( + #[switch]$CustomParam, [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)] [string[]]$BuildArguments ) -Write-Output "PowerShell $($PSVersionTable.PSEdition) version $($PSVersionTable.PSVersion)" +Write-Output "Windows PowerShell $($Host.Version)" -Set-StrictMode -Version 2.0; $ErrorActionPreference = "Stop"; $ConfirmPreference = "None"; trap { Write-Error $_ -ErrorAction Continue; exit 1 } +Set-StrictMode -Version 2.0; $ErrorActionPreference = "Stop"; $ConfirmPreference = "None"; trap { exit 1 } $PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent ########################################################################### @@ -14,15 +15,15 @@ $PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent ########################################################################### $BuildProjectFile = "$PSScriptRoot\nukebuild\_build.csproj" -$TempDirectory = "$PSScriptRoot\\.nuke\temp" +$TempDirectory = "$PSScriptRoot\\.tmp" $DotNetGlobalFile = "$PSScriptRoot\\global.json" -$DotNetInstallUrl = "https://dot.net/v1/dotnet-install.ps1" +$DotNetInstallUrl = "https://raw.githubusercontent.com/dotnet/cli/master/scripts/obtain/dotnet-install.ps1" $DotNetChannel = "Current" $env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = 1 $env:DOTNET_CLI_TELEMETRY_OPTOUT = 1 -$env:DOTNET_MULTILEVEL_LOOKUP = 0 +$env:NUGET_XMLDOC_MODE = "skip" ########################################################################### # EXECUTION @@ -33,37 +34,38 @@ function ExecSafe([scriptblock] $cmd) { if ($LASTEXITCODE) { exit $LASTEXITCODE } } -# If dotnet CLI is installed globally and it matches requested version, use for execution +# If global.json exists, load expected version +if (Test-Path $DotNetGlobalFile) { + $DotNetGlobal = $(Get-Content $DotNetGlobalFile | Out-String | ConvertFrom-Json) + if ($DotNetGlobal.PSObject.Properties["sdk"] -and $DotNetGlobal.sdk.PSObject.Properties["version"]) { + $DotNetVersion = $DotNetGlobal.sdk.version + } +} + +# If dotnet is installed locally, and expected version is not set or installation matches the expected version if ($null -ne (Get-Command "dotnet" -ErrorAction SilentlyContinue) -and ` - $(dotnet --version) -and $LASTEXITCODE -eq 0) { + (!(Test-Path variable:DotNetVersion) -or $(& dotnet --version) -eq $DotNetVersion)) { $env:DOTNET_EXE = (Get-Command "dotnet").Path } else { + $DotNetDirectory = "$TempDirectory\dotnet-win" + $env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe" + # Download install script $DotNetInstallFile = "$TempDirectory\dotnet-install.ps1" - New-Item -ItemType Directory -Path $TempDirectory -Force | Out-Null - [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + mkdir -force $TempDirectory > $null (New-Object System.Net.WebClient).DownloadFile($DotNetInstallUrl, $DotNetInstallFile) - # If global.json exists, load expected version - if (Test-Path $DotNetGlobalFile) { - $DotNetGlobal = $(Get-Content $DotNetGlobalFile | Out-String | ConvertFrom-Json) - if ($DotNetGlobal.PSObject.Properties["sdk"] -and $DotNetGlobal.sdk.PSObject.Properties["version"]) { - $DotNetVersion = $DotNetGlobal.sdk.version - } - } - # Install by channel or version - $DotNetDirectory = "$TempDirectory\dotnet-win" if (!(Test-Path variable:DotNetVersion)) { - ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Channel $DotNetChannel -NoPath } + ExecSafe { & $DotNetInstallFile -InstallDir $DotNetDirectory -Channel $DotNetChannel -NoPath } } else { - ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath } + ExecSafe { & $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath } } - $env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe" + + $env:PATH="$DotNetDirectory;$env:PATH" } -Write-Output "Microsoft (R) .NET SDK version $(& $env:DOTNET_EXE --version)" +Write-Output "Microsoft (R) .NET Core SDK version $(& $env:DOTNET_EXE --version)" -ExecSafe { & $env:DOTNET_EXE build $BuildProjectFile /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet } -ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile --no-build -- $BuildArguments } +ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile -- $BuildArguments } diff --git a/build.sh b/build.sh index 76919a5351..9532b4fbe0 100755 --- a/build.sh +++ b/build.sh @@ -1,6 +1,16 @@ #!/usr/bin/env bash -bash --version 2>&1 | head -n 1 +echo $(bash --version 2>&1 | head -n 1) + +#CUSTOMPARAM=0 +BUILD_ARGUMENTS=() +for i in "$@"; do + case $(echo $1 | awk '{print tolower($0)}') in + # -custom-param) CUSTOMPARAM=1;; + *) BUILD_ARGUMENTS+=("$1") ;; + esac + shift +done set -eo pipefail SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) @@ -10,53 +20,11 @@ SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) ########################################################################### BUILD_PROJECT_FILE="$SCRIPT_DIR/nukebuild/_build.csproj" -TEMP_DIRECTORY="$SCRIPT_DIR//.nuke/temp" - -DOTNET_GLOBAL_FILE="$SCRIPT_DIR//global.json" -DOTNET_INSTALL_URL="https://dot.net/v1/dotnet-install.sh" -DOTNET_CHANNEL="Current" export DOTNET_CLI_TELEMETRY_OPTOUT=1 export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 -export DOTNET_MULTILEVEL_LOOKUP=0 +export NUGET_XMLDOC_MODE="skip" -########################################################################### -# EXECUTION -########################################################################### +dotnet --info -function FirstJsonValue { - perl -nle 'print $1 if m{"'"$1"'": "([^"]+)",?}' <<< "${@:2}" -} - -# If dotnet CLI is installed globally and it matches requested version, use for execution -if [ -x "$(command -v dotnet)" ] && dotnet --version &>/dev/null; then - export DOTNET_EXE="$(command -v dotnet)" -else - # Download install script - DOTNET_INSTALL_FILE="$TEMP_DIRECTORY/dotnet-install.sh" - mkdir -p "$TEMP_DIRECTORY" - curl -Lsfo "$DOTNET_INSTALL_FILE" "$DOTNET_INSTALL_URL" - chmod +x "$DOTNET_INSTALL_FILE" - - # If global.json exists, load expected version - if [[ -f "$DOTNET_GLOBAL_FILE" ]]; then - DOTNET_VERSION=$(FirstJsonValue "version" "$(cat "$DOTNET_GLOBAL_FILE")") - if [[ "$DOTNET_VERSION" == "" ]]; then - unset DOTNET_VERSION - fi - fi - - # Install by channel or version - DOTNET_DIRECTORY="$TEMP_DIRECTORY/dotnet-unix" - if [[ -z ${DOTNET_VERSION+x} ]]; then - "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --channel "$DOTNET_CHANNEL" --no-path - else - "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version "$DOTNET_VERSION" --no-path - fi - export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet" -fi - -echo "Microsoft (R) .NET SDK version $("$DOTNET_EXE" --version)" - -"$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet -"$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@" +dotnet run --project "$BUILD_PROJECT_FILE" -- ${BUILD_ARGUMENTS[@]} diff --git a/nukebuild/_build.csproj b/nukebuild/_build.csproj index e4ad76de56..b2c58e2292 100644 --- a/nukebuild/_build.csproj +++ b/nukebuild/_build.csproj @@ -7,7 +7,6 @@ False CS0649;CS0169 - 1 From 8f3582bc576683ac2d69bf5c415221bee91852a7 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 27 Sep 2022 20:51:36 +0100 Subject: [PATCH 057/169] Revert "update nuke." This reverts commit 87dbbfe8a1be151ef9499971f4f634aef0507800. --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 2610134f02..8dbe304d7e 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -179,7 +179,7 @@ jobs: displayName: 'Install Nuke' inputs: script: | - dotnet tool install --global Nuke.GlobalTool --version 6.2.1 + dotnet tool install --global Nuke.GlobalTool --version 0.24.0 - task: CmdLine@2 displayName: 'Run Nuke' From 185dd96a17f9b0eed107ad02ab384bb0b62949e6 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 27 Sep 2022 16:45:24 -0400 Subject: [PATCH 058/169] Add ESLint to the project --- src/Web/Avalonia.Web/webapp/.eslintrc.json | 47 + src/Web/Avalonia.Web/webapp/build.js | 8 +- .../Avalonia.Web/webapp/modules/avalonia.ts | 11 +- .../webapp/modules/avalonia/canvas.ts | 137 +- .../{CaretHelper.ts => caretHelper.ts} | 22 +- .../webapp/modules/avalonia/dom.ts | 11 +- .../webapp/modules/avalonia/input.ts | 51 +- .../Avalonia.Web/webapp/modules/storage.ts | 4 +- src/Web/Avalonia.Web/webapp/package-lock.json | 2330 ++++++++++++++--- src/Web/Avalonia.Web/webapp/package.json | 15 +- 10 files changed, 2135 insertions(+), 501 deletions(-) create mode 100644 src/Web/Avalonia.Web/webapp/.eslintrc.json rename src/Web/Avalonia.Web/webapp/modules/avalonia/{CaretHelper.ts => caretHelper.ts} (90%) diff --git a/src/Web/Avalonia.Web/webapp/.eslintrc.json b/src/Web/Avalonia.Web/webapp/.eslintrc.json new file mode 100644 index 0000000000..4b7e24987f --- /dev/null +++ b/src/Web/Avalonia.Web/webapp/.eslintrc.json @@ -0,0 +1,47 @@ +{ + "env": { + "browser": true, + "es6": true + }, + "extends": "standard-with-typescript", + "overrides": [], + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module", + "project": [ + "tsconfig.json" + ] + }, + "rules": { + "indent": [ + "warn", + 4 + ], + "@typescript-eslint/indent": [ + "warn", + 4 + ], + "quotes": ["warn", "double"], + "semi": ["error", "always"], + "@typescript-eslint/quotes": ["warn", "double"], + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/no-extraneous-class": "off", + "@typescript-eslint/strict-boolean-expressions": "off", + "@typescript-eslint/space-before-function-paren": "off", + "@typescript-eslint/semi": ["error", "always"], + "@typescript-eslint/member-delimiter-style": [ + "error", + { + "multiline": { + "delimiter": "semi", + "requireLast": true + }, + "singleline": { + "delimiter": "semi", + "requireLast": false + } + } + ] + }, + "ignorePatterns": ["types/*"] +} diff --git a/src/Web/Avalonia.Web/webapp/build.js b/src/Web/Avalonia.Web/webapp/build.js index 32f79b0709..6b6df4c300 100644 --- a/src/Web/Avalonia.Web/webapp/build.js +++ b/src/Web/Avalonia.Web/webapp/build.js @@ -10,7 +10,7 @@ require("esbuild").build({ target: "es2016", platform: "browser", sourcemap: "linked", - loader: {".ts": "ts"} - }) - .then(() => console.log("⚡ Done")) - .catch(() => process.exit(1)); \ No newline at end of file + loader: { ".ts": "ts" } +}) + .then(() => console.log("⚡ Done")) + .catch(() => process.exit(1)); diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia.ts index 4b85434656..542d48cef5 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia.ts @@ -1,10 +1,8 @@ -import { RuntimeAPI } from "../types/dotnet"; -import { SizeWatcher } from "./avalonia/canvas"; -import { DpiWatcher } from "./avalonia/canvas"; -import { Canvas } from "./avalonia/canvas"; +import { RuntimeAPI } from "../types/dotnet"; +import { SizeWatcher, DpiWatcher, Canvas } from "./avalonia/canvas"; + import { InputHelper } from "./avalonia/input"; import { AvaloniaDOM } from "./avalonia/dom"; -import { CaretHelper } from "./avalonia/CaretHelper" export async function createAvaloniaRuntime(api: RuntimeAPI): Promise { api.setModuleImports("avalonia.ts", { @@ -12,7 +10,6 @@ export async function createAvaloniaRuntime(api: RuntimeAPI): Promise { InputHelper, SizeWatcher, DpiWatcher, - AvaloniaDOM, - CaretHelper + AvaloniaDOM }); } diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts index 51f851c9c7..017ca58b8f 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts @@ -1,6 +1,4 @@ -declare let Module: EmscriptenModule; - -type SKGLViewInfo = { +interface SKGLViewInfo { context: WebGLRenderingContext | WebGL2RenderingContext | undefined; fboId: number; stencil: number; @@ -9,8 +7,8 @@ type SKGLViewInfo = { } type CanvasElement = { - Canvas: Canvas | undefined -} & HTMLCanvasElement + Canvas: Canvas | undefined; +} & HTMLCanvasElement; export class Canvas { static elements: Map; @@ -24,22 +22,24 @@ export class Canvas { newHeight?: number; public static initGL(element: HTMLCanvasElement, elementId: string, renderFrameCallback: () => void): SKGLViewInfo | null { - var view = Canvas.init(true, element, elementId, renderFrameCallback); - if (!view || !view.glInfo) + const view = Canvas.init(true, element, elementId, renderFrameCallback); + if (!view || !view.glInfo) { return null; + } return view.glInfo; } static init(useGL: boolean, element: HTMLCanvasElement, elementId: string, renderFrameCallback: () => void): Canvas | null { - var htmlCanvas = element as CanvasElement; + const htmlCanvas = element as CanvasElement; if (!htmlCanvas) { - console.error(`No canvas element was provided.`); + console.error("No canvas element was provided."); return null; } - if (!Canvas.elements) + if (!Canvas.elements) { Canvas.elements = new Map(); + } Canvas.elements.set(elementId, element); const view = new Canvas(useGL, element, renderFrameCallback); @@ -56,16 +56,16 @@ export class Canvas { if (useGL) { const ctx = Canvas.createWebGLContext(element); if (!ctx) { - console.error(`Failed to create WebGL context: err ${ctx}`); + console.error("Failed to create WebGL context"); return; } - var GL = (globalThis as any).AvaloniaGL; + const GL = (globalThis as any).AvaloniaGL; // make current GL.makeContextCurrent(ctx); - var GLctx = GL.currentContext.GLctx as WebGLRenderingContext; + const GLctx = GL.currentContext.GLctx as WebGLRenderingContext; // read values const fbo = GLctx.getParameter(GLctx.FRAMEBUFFER_BINDING); @@ -75,17 +75,17 @@ export class Canvas { fboId: fbo ? fbo.id : 0, stencil: GLctx.getParameter(GLctx.STENCIL_BITS), sample: 0, // TODO: GLctx.getParameter(GLctx.SAMPLES) - depth: GLctx.getParameter(GLctx.DEPTH_BITS), + depth: GLctx.getParameter(GLctx.DEPTH_BITS) }; } } - public setEnableRenderLoop(enable: boolean) { + public setEnableRenderLoop(enable: boolean): void { this.renderLoopEnabled = enable; // either start the new frame or cancel the existing one if (enable) { - //console.info(`Enabling render loop with callback ${this.renderFrameCallback._id}...`); + // console.info(`Enabling render loop with callback ${this.renderFrameCallback._id}...`); this.requestAnimationFrame(); } else if (this.renderLoopRequest !== 0) { window.cancelAnimationFrame(this.renderLoopRequest); @@ -93,71 +93,76 @@ export class Canvas { } } - public requestAnimationFrame(renderLoop?: boolean) { + public requestAnimationFrame(renderLoop?: boolean): void { // optionally update the render loop - if (renderLoop !== undefined && this.renderLoopEnabled !== renderLoop) + if (renderLoop !== undefined && this.renderLoopEnabled !== renderLoop) { this.setEnableRenderLoop(renderLoop); + } // skip because we have a render loop - if (this.renderLoopRequest !== 0) + if (this.renderLoopRequest !== 0) { return; + } // add the draw to the next frame this.renderLoopRequest = window.requestAnimationFrame(() => { if (this.glInfo) { - var GL = (globalThis as any).AvaloniaGL; + const GL = (globalThis as any).AvaloniaGL; // make current GL.makeContextCurrent(this.glInfo.context); } - if (this.htmlCanvas.width != this.newWidth) { - this.htmlCanvas.width = this.newWidth || 0; + if (this.htmlCanvas.width !== this.newWidth) { + this.htmlCanvas.width = this.newWidth ?? 0; } - if (this.htmlCanvas.height != this.newHeight) { - this.htmlCanvas.height = this.newHeight || 0; + if (this.htmlCanvas.height !== this.newHeight) { + this.htmlCanvas.height = this.newHeight ?? 0; } this.renderFrameCallback(); this.renderLoopRequest = 0; // we may want to draw the next frame - if (this.renderLoopEnabled) + if (this.renderLoopEnabled) { this.requestAnimationFrame(); + } }); } - public setCanvasSize(width: number, height: number) { + public setCanvasSize(width: number, height: number): void { this.newWidth = width; this.newHeight = height; - if (this.htmlCanvas.width != this.newWidth) { + if (this.htmlCanvas.width !== this.newWidth) { this.htmlCanvas.width = this.newWidth; } - if (this.htmlCanvas.height != this.newHeight) { + if (this.htmlCanvas.height !== this.newHeight) { this.htmlCanvas.height = this.newHeight; } if (this.glInfo) { - var GL = (globalThis as any).AvaloniaGL; + const GL = (globalThis as any).AvaloniaGL; // make current GL.makeContextCurrent(this.glInfo.context); } } - public static setCanvasSize(element: HTMLCanvasElement, width: number, height: number) { + public static setCanvasSize(element: HTMLCanvasElement, width: number, height: number): void { const htmlCanvas = element as CanvasElement; - if (!htmlCanvas || !htmlCanvas.Canvas) + if (!htmlCanvas || !htmlCanvas.Canvas) { return; + } htmlCanvas.Canvas.setCanvasSize(width, height); } - public static requestAnimationFrame(element: HTMLCanvasElement, renderLoop?: boolean) { + public static requestAnimationFrame(element: HTMLCanvasElement, renderLoop?: boolean): void { const htmlCanvas = element as CanvasElement; - if (!htmlCanvas || !htmlCanvas.Canvas) + if (!htmlCanvas || !htmlCanvas.Canvas) { return; + } htmlCanvas.Canvas.requestAnimationFrame(renderLoop); } @@ -176,15 +181,15 @@ export class Canvas { minorVersion: 0, enableExtensionsByDefault: 1, explicitSwapControl: 0, - renderViaOffscreenBackBuffer: 1, + renderViaOffscreenBackBuffer: 1 }; - var GL = (globalThis as any).AvaloniaGL; + const GL = (globalThis as any).AvaloniaGL; let ctx: WebGLRenderingContext = GL.createContext(htmlCanvas, contextAttributes); if (!ctx && contextAttributes.majorVersion > 1) { - console.warn('Falling back to WebGL 1.0'); + console.warn("Falling back to WebGL 1.0"); contextAttributes.majorVersion = 1; contextAttributes.minorVersion = 0; ctx = GL.createContext(htmlCanvas, contextAttributes); @@ -196,9 +201,9 @@ export class Canvas { type SizeWatcherElement = { SizeWatcher: SizeWatcherInstance; -} & HTMLElement +} & HTMLElement; -type SizeWatcherInstance = { +interface SizeWatcherInstance { callback: (width: number, height: number) => void; } @@ -206,17 +211,16 @@ export class SizeWatcher { static observer: ResizeObserver; static elements: Map; - public static observe(element: HTMLElement, elementId: string, callback: (width: number, height: number) => void) { - if (!element || !callback) + public static observe(element: HTMLElement, elementId: string, callback: (width: number, height: number) => void): void { + if (!element || !callback) { return; - - //console.info(`Adding size watcher observation with callback ${callback._id}...`); + } SizeWatcher.init(); const watcherElement = element as SizeWatcherElement; watcherElement.SizeWatcher = { - callback: callback + callback }; SizeWatcher.elements.set(elementId, element); @@ -225,38 +229,38 @@ export class SizeWatcher { SizeWatcher.invoke(element); } - public static unobserve(elementId: string) { - if (!elementId || !SizeWatcher.observer) + public static unobserve(elementId: string): void { + if (!elementId || !SizeWatcher.observer) { return; + } - //console.info('Removing size watcher observation...'); - - const element = SizeWatcher.elements.get(elementId)!; - - SizeWatcher.elements.delete(elementId); - SizeWatcher.observer.unobserve(element); + const element = SizeWatcher.elements.get(elementId); + if (element) { + SizeWatcher.elements.delete(elementId); + SizeWatcher.observer.unobserve(element); + } } - static init() { - if (SizeWatcher.observer) + static init(): void { + if (SizeWatcher.observer) { return; - - //console.info('Starting size watcher...'); + } SizeWatcher.elements = new Map(); SizeWatcher.observer = new ResizeObserver((entries) => { - for (let entry of entries) { + for (const entry of entries) { SizeWatcher.invoke(entry.target); } }); } - static invoke(element: Element) { + static invoke(element: Element): void { const watcherElement = element as SizeWatcherElement; const instance = watcherElement.SizeWatcher; - if (!instance || !instance.callback) + if (!instance || !instance.callback) { return; + } return instance.callback(element.clientWidth, element.clientHeight); } @@ -267,13 +271,11 @@ export class DpiWatcher { static timerId: number; static callback: (old: number, newdpi: number) => void; - public static getDpi() { + public static getDpi(): number { return window.devicePixelRatio; } - public static start(callback: (old: number, newdpi: number) => void) : number { - //console.info(`Starting DPI watcher with callback ${callback._id}...`); - + public static start(callback: (old: number, newdpi: number) => void): number { DpiWatcher.lastDpi = window.devicePixelRatio; DpiWatcher.timerId = window.setInterval(DpiWatcher.update, 1000); DpiWatcher.callback = callback; @@ -281,17 +283,14 @@ export class DpiWatcher { return DpiWatcher.lastDpi; } - public static stop() { - //console.info(`Stopping DPI watcher with callback ${DpiWatcher.callback._id}...`); - + public static stop(): void { window.clearInterval(DpiWatcher.timerId); - - //DpiWatcher.callback = undefined; } - static update() { - if (!DpiWatcher.callback) + static update(): void { + if (!DpiWatcher.callback) { return; + } const currentDpi = window.devicePixelRatio; const lastDpi = DpiWatcher.lastDpi; diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/CaretHelper.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/caretHelper.ts similarity index 90% rename from src/Web/Avalonia.Web/webapp/modules/avalonia/CaretHelper.ts rename to src/Web/Avalonia.Web/webapp/modules/avalonia/caretHelper.ts index 5709854087..546b41669f 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia/CaretHelper.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/caretHelper.ts @@ -1,4 +1,4 @@ -// Based on https://github.com/component/textarea-caret-position/blob/master/index.js +// Based on https://github.com/component/textarea-caret-position/blob/master/index.js export class CaretHelper { public static getCaretCoordinates( element: HTMLInputElement | HTMLTextAreaElement, @@ -11,7 +11,7 @@ export class CaretHelper { ); } - const debug = (options && options.debug) || false; + const debug = options?.debug ?? false; if (debug) { const el = document.querySelector( "#input-textarea-caret-position-mirror-div" @@ -27,7 +27,7 @@ export class CaretHelper { const style = div.style; const computed = window.getComputedStyle ? window.getComputedStyle(element) - : ((element as any)["currentStyle"] as CSSStyleDeclaration); // currentStyle for IE < 9 + : ((element as any).currentStyle as CSSStyleDeclaration); // currentStyle for IE < 9 const isInput = element.nodeName === "INPUT"; // Default textarea styles @@ -51,7 +51,7 @@ export class CaretHelper { parseInt(computed.borderBottomWidth); const targetHeight = outerHeight + parseInt(computed.lineHeight); if (height > targetHeight) { - style.lineHeight = height - outerHeight + "px"; + style.lineHeight = `${height - outerHeight}px`; } else if (height === targetHeight) { style.lineHeight = computed.lineHeight; } else { @@ -67,8 +67,9 @@ export class CaretHelper { if (isFirefox) { // Firefox lies about the overflow property for textareas: https://bugzilla.mozilla.org/show_bug.cgi?id=984275 - if (element.scrollHeight > parseInt(computed.height)) + if (element.scrollHeight > parseInt(computed.height)) { style.overflowY = "scroll"; + } } else { style.overflow = "hidden"; // for Chrome to not render a scrollbar; IE keeps overflowY = 'scroll' } @@ -88,9 +89,9 @@ export class CaretHelper { div.appendChild(span); const coordinates = { - top: span.offsetTop + parseInt(computed["borderTopWidth"]), - left: span.offsetLeft + parseInt(computed["borderLeftWidth"]), - height: parseInt(computed["lineHeight"]), + top: span.offsetTop + parseInt(computed.borderTopWidth), + left: span.offsetLeft + parseInt(computed.borderLeftWidth), + height: parseInt(computed.lineHeight) }; if (debug) { @@ -103,8 +104,7 @@ export class CaretHelper { } } - -var properties = [ +const properties = [ "direction", // RTL support "boxSizing", "width", // on Chrome and IE, exclude the scrollbar, so the mirror div wraps exactly as the textarea does @@ -142,7 +142,7 @@ var properties = [ "wordSpacing", "tabSize", - "MozTabSize", + "MozTabSize" ]; const isBrowser = typeof window !== "undefined"; diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/dom.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/dom.ts index 068fe08620..943b8330d5 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia/dom.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/dom.ts @@ -1,6 +1,5 @@ export class AvaloniaDOM { - - public static addClass(element: HTMLElement, className: string) { + public static addClass(element: HTMLElement, className: string): void { element.classList.add(className); } @@ -12,15 +11,15 @@ export class AvaloniaDOM { // Rendering target canvas const canvas = document.createElement("canvas"); - canvas.classList.add('avalonia-canvas'); + canvas.classList.add("avalonia-canvas"); canvas.style.backgroundColor = "#ccc"; canvas.style.width = "100%"; canvas.style.height = "100%"; canvas.style.position = "absolute"; - + // Native controls host const nativeHost = document.createElement("div"); - nativeHost.classList.add('avalonia-native-host'); + nativeHost.classList.add("avalonia-native-host"); nativeHost.style.left = "0px"; nativeHost.style.top = "0px"; nativeHost.style.width = "100%"; @@ -29,7 +28,7 @@ export class AvaloniaDOM { // IME const inputElement = document.createElement("input"); - inputElement.classList.add('avalonia-input-element'); + inputElement.classList.add("avalonia-input-element"); inputElement.autocapitalize = "none"; inputElement.type = "text"; inputElement.spellcheck = false; diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts index b5c8a60a88..768414ccab 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/input.ts @@ -1,7 +1,6 @@ -import { CaretHelper } from "./CaretHelper" +import { CaretHelper } from "./caretHelper"; -enum RawInputModifiers -{ +enum RawInputModifiers { None = 0, Alt = 1, Control = 2, @@ -24,12 +23,7 @@ export class InputHelper { public static subscribeKeyEvents( element: HTMLInputElement, keyDownCallback: (code: string, key: string, modifiers: RawInputModifiers) => boolean, - keyUpCallback: (code: string, key: string, modifiers: RawInputModifiers) => boolean, - inputCallback: (type: string, data: string|null) => boolean, - compositionStartCallback: (args: CompositionEvent) => boolean, - compositionUpdateCallback: (args: CompositionEvent) => boolean, - compositionEndCallback: (args: CompositionEvent) => boolean) - { + keyUpCallback: (code: string, key: string, modifiers: RawInputModifiers) => boolean) { const keyDownHandler = (args: KeyboardEvent) => { if (keyDownCallback(args.code, args.key, this.getModifiers(args))) { args.preventDefault(); @@ -52,14 +46,13 @@ export class InputHelper { } public static subscribeTextEvents( - element: HTMLInputElement, + element: HTMLInputElement, inputCallback: (type: string, data: string | null) => boolean, compositionStartCallback: (args: CompositionEvent) => boolean, compositionUpdateCallback: (args: CompositionEvent) => boolean, compositionEndCallback: (args: CompositionEvent) => boolean) { - const inputHandler = (args: Event) => { - var inputEvent = args as InputEvent; + const inputEvent = args as InputEvent; // todo check cast if (inputCallback(inputEvent.type, inputEvent.data)) { @@ -102,31 +95,27 @@ export class InputHelper { pointerMoveCallback: (args: PointerEvent) => boolean, pointerDownCallback: (args: PointerEvent) => boolean, pointerUpCallback: (args: PointerEvent) => boolean, - wheelCallback: (args: WheelEvent) => boolean, + wheelCallback: (args: WheelEvent) => boolean ) { const pointerMoveHandler = (args: PointerEvent) => { - if (pointerMoveCallback(args)) { args.preventDefault(); } }; const pointerDownHandler = (args: PointerEvent) => { - if (pointerDownCallback(args)) { args.preventDefault(); } }; const pointerUpHandler = (args: PointerEvent) => { - if (pointerUpCallback(args)) { args.preventDefault(); } }; const wheelHandler = (args: WheelEvent) => { - if (wheelCallback(args)) { args.preventDefault(); } @@ -137,8 +126,6 @@ export class InputHelper { element.addEventListener("pointerup", pointerUpHandler); element.addEventListener("wheel", wheelHandler); - - return () => { element.removeEventListener("pointerover", pointerMoveHandler); element.removeEventListener("pointerdown", pointerDownHandler); @@ -146,7 +133,7 @@ export class InputHelper { element.removeEventListener("wheel", wheelHandler); }; } - + public static subscribeInputEvents( element: HTMLInputElement, inputCallback: (value: string) => boolean @@ -179,18 +166,18 @@ export class InputHelper { inputElement.style.left = (x).toFixed(0) + "px"; inputElement.style.top = (y).toFixed(0) + "px"; - let { height, left, top } = CaretHelper.getCaretCoordinates(inputElement, caret); + const { left, top } = CaretHelper.getCaretCoordinates(inputElement, caret); inputElement.style.left = (x - left).toFixed(0) + "px"; inputElement.style.top = (y - top).toFixed(0) + "px"; } public static hide(inputElement: HTMLInputElement) { - inputElement.style.display = 'none'; + inputElement.style.display = "none"; } public static show(inputElement: HTMLInputElement) { - inputElement.style.display = 'block'; + inputElement.style.display = "block"; } public static setSurroundingText(inputElement: HTMLInputElement, text: string, start: number, end: number) { @@ -201,20 +188,16 @@ export class InputHelper { inputElement.value = text; inputElement.setSelectionRange(start, end); inputElement.style.width = "20px"; - inputElement.style.width = inputElement.scrollWidth + "px"; + inputElement.style.width = `${inputElement.scrollWidth}px`; } private static getModifiers(args: KeyboardEvent): RawInputModifiers { - var modifiers = RawInputModifiers.None; - - if (args.ctrlKey) - modifiers |= RawInputModifiers.Control; - if (args.altKey) - modifiers |= RawInputModifiers.Alt; - if (args.shiftKey) - modifiers |= RawInputModifiers.Shift; - if (args.metaKey) - modifiers |= RawInputModifiers.Meta; + let modifiers = RawInputModifiers.None; + + if (args.ctrlKey) { modifiers |= RawInputModifiers.Control; } + if (args.altKey) { modifiers |= RawInputModifiers.Alt; } + if (args.shiftKey) { modifiers |= RawInputModifiers.Shift; } + if (args.metaKey) { modifiers |= RawInputModifiers.Meta; } return modifiers; } diff --git a/src/Web/Avalonia.Web/webapp/modules/storage.ts b/src/Web/Avalonia.Web/webapp/modules/storage.ts index 7b6cd816de..f6dfd9151b 100644 --- a/src/Web/Avalonia.Web/webapp/modules/storage.ts +++ b/src/Web/Avalonia.Web/webapp/modules/storage.ts @@ -1,5 +1,5 @@ -export class StorageProvider { +export class StorageProvider { static isFileApiSupported(): boolean { return (globalThis as any).showOpenFilePicker !== undefined; } -} \ No newline at end of file +} diff --git a/src/Web/Avalonia.Web/webapp/package-lock.json b/src/Web/Avalonia.Web/webapp/package-lock.json index 5df9471195..947f7e12e7 100644 --- a/src/Web/Avalonia.Web/webapp/package-lock.json +++ b/src/Web/Avalonia.Web/webapp/package-lock.json @@ -1,458 +1,484 @@ { "name": "avalonia.web", - "lockfileVersion": 2, "requires": true, - "packages": { - "": { - "name": "avalonia.web", - "devDependencies": { - "@types/emscripten": "^1.39.6", - "@types/wicg-file-system-access": "^2020.9.5", - "esbuild": "^0.15.7", - "typescript": "^4.7.4" - } - }, - "node_modules/@esbuild/android-arm": { + "lockfileVersion": 1, + "dependencies": { + "@esbuild/android-arm": { "version": "0.15.9", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.9.tgz", "integrity": "sha512-VZPy/ETF3fBG5PiinIkA0W/tlsvlEgJccyN2DzWZEl0DlVKRbu91PvY2D6Lxgluj4w9QtYHjOWjAT44C+oQ+EQ==", - "cpu": [ - "arm" - ], "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } + "optional": true }, - "node_modules/@esbuild/linux-loong64": { + "@esbuild/linux-loong64": { "version": "0.15.9", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.9.tgz", "integrity": "sha512-O+NfmkfRrb3uSsTa4jE3WApidSe3N5++fyOVGP1SmMZi4A3BZELkhUUvj5hwmMuNdlpzAZ8iAPz2vmcR7DCFQA==", - "cpu": [ - "loong64" - ], "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "optional": true + }, + "@eslint/eslintrc": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.2.tgz", + "integrity": "sha512-AXYd23w1S/bv3fTs3Lz0vjiYemS08jWkI3hYyS9I1ry+0f+Yjs1wm+sU0BS8qDOPrBIkp4qHYC16I8uVtpLajQ==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.4.0", + "globals": "^13.15.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + } + }, + "@humanwhocodes/config-array": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.5.tgz", + "integrity": "sha512-XVVDtp+dVvRxMoxSiSfasYaG02VEe1qH5cKgMQJWhol6HwzbcqoCMJi8dAGoYAO57jhUyhI6cWuRiTcRaDaYug==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + } + }, + "@humanwhocodes/gitignore-to-minimatch": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/gitignore-to-minimatch/-/gitignore-to-minimatch-1.0.2.tgz", + "integrity": "sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA==", + "dev": true + }, + "@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true + }, + "@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" } }, - "node_modules/@types/emscripten": { + "@types/emscripten": { "version": "1.39.6", "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.39.6.tgz", "integrity": "sha512-H90aoynNhhkQP6DRweEjJp5vfUVdIj7tdPLsu7pq89vODD/lcugKfZOsfgwpvM6XUewEp2N5dCg1Uf3Qe55Dcg==", "dev": true }, - "node_modules/@types/wicg-file-system-access": { + "@types/json-schema": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "dev": true + }, + "@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, + "@types/wicg-file-system-access": { "version": "2020.9.5", "resolved": "https://registry.npmjs.org/@types/wicg-file-system-access/-/wicg-file-system-access-2020.9.5.tgz", "integrity": "sha512-UYK244awtmcUYQfs7FR8710MJcefL2WvkyHMjA8yJzxd1mo0Gfn88sRZ1Bls7hiUhA2w7ne1gpJ9T5g3G0wOyA==", "dev": true }, - "node_modules/esbuild": { - "version": "0.15.9", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.9.tgz", - "integrity": "sha512-OnYr1rkMVxtmMHIAKZLMcEUlJmqcbxBz9QoBU8G9v455na0fuzlT/GLu6l+SRghrk0Mm2fSSciMmzV43Q8e0Gg==", + "@typescript-eslint/eslint-plugin": { + "version": "5.38.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.38.1.tgz", + "integrity": "sha512-ky7EFzPhqz3XlhS7vPOoMDaQnQMn+9o5ICR9CPr/6bw8HrFkzhMSxuA3gRfiJVvs7geYrSeawGJjZoZQKCOglQ==", "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/android-arm": "0.15.9", - "@esbuild/linux-loong64": "0.15.9", - "esbuild-android-64": "0.15.9", - "esbuild-android-arm64": "0.15.9", - "esbuild-darwin-64": "0.15.9", - "esbuild-darwin-arm64": "0.15.9", - "esbuild-freebsd-64": "0.15.9", - "esbuild-freebsd-arm64": "0.15.9", - "esbuild-linux-32": "0.15.9", - "esbuild-linux-64": "0.15.9", - "esbuild-linux-arm": "0.15.9", - "esbuild-linux-arm64": "0.15.9", - "esbuild-linux-mips64le": "0.15.9", - "esbuild-linux-ppc64le": "0.15.9", - "esbuild-linux-riscv64": "0.15.9", - "esbuild-linux-s390x": "0.15.9", - "esbuild-netbsd-64": "0.15.9", - "esbuild-openbsd-64": "0.15.9", - "esbuild-sunos-64": "0.15.9", - "esbuild-windows-32": "0.15.9", - "esbuild-windows-64": "0.15.9", - "esbuild-windows-arm64": "0.15.9" + "requires": { + "@typescript-eslint/scope-manager": "5.38.1", + "@typescript-eslint/type-utils": "5.38.1", + "@typescript-eslint/utils": "5.38.1", + "debug": "^4.3.4", + "ignore": "^5.2.0", + "regexpp": "^3.2.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" } }, - "node_modules/esbuild-android-64": { - "version": "0.15.9", - "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.9.tgz", - "integrity": "sha512-HQCX7FJn9T4kxZQkhPjNZC7tBWZqJvhlLHPU2SFzrQB/7nDXjmTIFpFTjt7Bd1uFpeXmuwf5h5fZm+x/hLnhbw==", - "cpu": [ - "x64" - ], + "@typescript-eslint/parser": { + "version": "5.38.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.38.1.tgz", + "integrity": "sha512-LDqxZBVFFQnQRz9rUZJhLmox+Ep5kdUmLatLQnCRR6523YV+XhRjfYzStQ4MheFA8kMAfUlclHSbu+RKdRwQKw==", "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" + "requires": { + "@typescript-eslint/scope-manager": "5.38.1", + "@typescript-eslint/types": "5.38.1", + "@typescript-eslint/typescript-estree": "5.38.1", + "debug": "^4.3.4" } }, - "node_modules/esbuild-android-arm64": { - "version": "0.15.9", - "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.9.tgz", - "integrity": "sha512-E6zbLfqbFVCNEKircSHnPiSTsm3fCRxeIMPfrkS33tFjIAoXtwegQfVZqMGR0FlsvVxp2NEDOUz+WW48COCjSg==", - "cpu": [ - "arm64" - ], + "@typescript-eslint/scope-manager": { + "version": "5.38.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.38.1.tgz", + "integrity": "sha512-BfRDq5RidVU3RbqApKmS7RFMtkyWMM50qWnDAkKgQiezRtLKsoyRKIvz1Ok5ilRWeD9IuHvaidaLxvGx/2eqTQ==", "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" + "requires": { + "@typescript-eslint/types": "5.38.1", + "@typescript-eslint/visitor-keys": "5.38.1" } }, - "node_modules/esbuild-darwin-64": { - "version": "0.15.9", - "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.9.tgz", - "integrity": "sha512-gI7dClcDN/HHVacZhTmGjl0/TWZcGuKJ0I7/xDGJwRQQn7aafZGtvagOFNmuOq+OBFPhlPv1T6JElOXb0unkSQ==", - "cpu": [ - "x64" - ], + "@typescript-eslint/type-utils": { + "version": "5.38.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.38.1.tgz", + "integrity": "sha512-UU3j43TM66gYtzo15ivK2ZFoDFKKP0k03MItzLdq0zV92CeGCXRfXlfQX5ILdd4/DSpHkSjIgLLLh1NtkOJOAw==", "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" + "requires": { + "@typescript-eslint/typescript-estree": "5.38.1", + "@typescript-eslint/utils": "5.38.1", + "debug": "^4.3.4", + "tsutils": "^3.21.0" } }, - "node_modules/esbuild-darwin-arm64": { - "version": "0.15.9", - "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.9.tgz", - "integrity": "sha512-VZIMlcRN29yg/sv7DsDwN+OeufCcoTNaTl3Vnav7dL/nvsApD7uvhVRbgyMzv0zU/PP0xRhhIpTyc7lxEzHGSw==", - "cpu": [ - "arm64" - ], + "@typescript-eslint/types": { + "version": "5.38.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.38.1.tgz", + "integrity": "sha512-QTW1iHq1Tffp9lNfbfPm4WJabbvpyaehQ0SrvVK2yfV79SytD9XDVxqiPvdrv2LK7DGSFo91TB2FgWanbJAZXg==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "5.38.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.38.1.tgz", + "integrity": "sha512-99b5e/Enoe8fKMLdSuwrfH/C0EIbpUWmeEKHmQlGZb8msY33qn1KlkFww0z26o5Omx7EVjzVDCWEfrfCDHfE7g==", "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" + "requires": { + "@typescript-eslint/types": "5.38.1", + "@typescript-eslint/visitor-keys": "5.38.1", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" } }, - "node_modules/esbuild-freebsd-64": { - "version": "0.15.9", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.9.tgz", - "integrity": "sha512-uM4z5bTvuAXqPxrI204txhlsPIolQPWRMLenvGuCPZTnnGlCMF2QLs0Plcm26gcskhxewYo9LkkmYSS5Czrb5A==", - "cpu": [ - "x64" - ], + "@typescript-eslint/utils": { + "version": "5.38.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.38.1.tgz", + "integrity": "sha512-oIuUiVxPBsndrN81oP8tXnFa/+EcZ03qLqPDfSZ5xIJVm7A9V0rlkQwwBOAGtrdN70ZKDlKv+l1BeT4eSFxwXA==", "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" + "requires": { + "@types/json-schema": "^7.0.9", + "@typescript-eslint/scope-manager": "5.38.1", + "@typescript-eslint/types": "5.38.1", + "@typescript-eslint/typescript-estree": "5.38.1", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0" } }, - "node_modules/esbuild-freebsd-arm64": { - "version": "0.15.9", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.9.tgz", - "integrity": "sha512-HHDjT3O5gWzicGdgJ5yokZVN9K9KG05SnERwl9nBYZaCjcCgj/sX8Ps1jvoFSfNCO04JSsHSOWo4qvxFuj8FoA==", - "cpu": [ - "arm64" - ], + "@typescript-eslint/visitor-keys": { + "version": "5.38.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.38.1.tgz", + "integrity": "sha512-bSHr1rRxXt54+j2n4k54p4fj8AHJ49VDWtjpImOpzQj4qjAiOpPni+V1Tyajh19Api1i844F757cur8wH3YvOA==", "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" + "requires": { + "@typescript-eslint/types": "5.38.1", + "eslint-visitor-keys": "^3.3.0" } }, - "node_modules/esbuild-linux-32": { - "version": "0.15.9", - "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.9.tgz", - "integrity": "sha512-AQIdE8FugGt1DkcekKi5ycI46QZpGJ/wqcMr7w6YUmOmp2ohQ8eO4sKUsOxNOvYL7hGEVwkndSyszR6HpVHLFg==", - "cpu": [ - "ia32" - ], + "acorn": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", + "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", + "dev": true + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" } }, - "node_modules/esbuild-linux-64": { - "version": "0.15.9", - "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.9.tgz", - "integrity": "sha512-4RXjae7g6Qs7StZyiYyXTZXBlfODhb1aBVAjd+ANuPmMhWthQilWo7rFHwJwL7DQu1Fjej2sODAVwLbcIVsAYQ==", - "cpu": [ - "x64" - ], + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "requires": { + "color-convert": "^2.0.1" } }, - "node_modules/esbuild-linux-arm": { - "version": "0.15.9", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.9.tgz", - "integrity": "sha512-3Zf2GVGUOI7XwChH3qrnTOSqfV1V4CAc/7zLVm4lO6JT6wbJrTgEYCCiNSzziSju+J9Jhf9YGWk/26quWPC6yQ==", - "cpu": [ - "arm" - ], + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "array-includes": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.5.tgz", + "integrity": "sha512-iSDYZMMyTPkiFasVqfuAQnWAYcvO/SeBSCGKePoEthjp4LEMTe4uLc7b025o4jAZpHhihh8xPo99TNWUWWkGDQ==", "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5", + "get-intrinsic": "^1.1.1", + "is-string": "^1.0.7" } }, - "node_modules/esbuild-linux-arm64": { - "version": "0.15.9", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.9.tgz", - "integrity": "sha512-a+bTtxJmYmk9d+s2W4/R1SYKDDAldOKmWjWP0BnrWtDbvUBNOm++du0ysPju4mZVoEFgS1yLNW+VXnG/4FNwdQ==", - "cpu": [ - "arm64" - ], + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, + "array.prototype.flat": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.0.tgz", + "integrity": "sha512-12IUEkHsAhA4DY5s0FPgNXIdc8VRSqD9Zp78a5au9abH/SOBrsp082JOWFNTjkMozh8mqcdiKuaLGhPeYztxSw==", "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.2", + "es-shim-unscopables": "^1.0.0" } }, - "node_modules/esbuild-linux-mips64le": { - "version": "0.15.9", - "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.9.tgz", - "integrity": "sha512-Zn9HSylDp89y+TRREMDoGrc3Z4Hs5u56ozZLQCiZAUx2+HdbbXbWdjmw3FdTJ/i7t5Cew6/Q+6kfO3KCcFGlyw==", - "cpu": [ - "mips64el" - ], + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "node_modules/esbuild-linux-ppc64le": { - "version": "0.15.9", - "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.9.tgz", - "integrity": "sha512-OEiOxNAMH9ENFYqRsWUj3CWyN3V8P3ZXyfNAtX5rlCEC/ERXrCEFCJji/1F6POzsXAzxvUJrTSTCy7G6BhA6Fw==", - "cpu": [ - "ppc64" - ], + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "requires": { + "fill-range": "^7.0.1" } }, - "node_modules/esbuild-linux-riscv64": { - "version": "0.15.9", - "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.9.tgz", - "integrity": "sha512-ukm4KsC3QRausEFjzTsOZ/qqazw0YvJsKmfoZZm9QW27OHjk2XKSQGGvx8gIEswft/Sadp03/VZvAaqv5AIwNA==", - "cpu": [ - "riscv64" - ], + "builtins": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz", + "integrity": "sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==", "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "requires": { + "semver": "^7.0.0" } }, - "node_modules/esbuild-linux-s390x": { - "version": "0.15.9", - "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.9.tgz", - "integrity": "sha512-uDOQEH55wQ6ahcIKzQr3VyjGc6Po/xblLGLoUk3fVL1qjlZAibtQr6XRfy5wPJLu/M2o0vQKLq4lyJ2r1tWKcw==", - "cpu": [ - "s390x" - ], + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" } }, - "node_modules/esbuild-netbsd-64": { - "version": "0.15.9", - "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.9.tgz", - "integrity": "sha512-yWgxaYTQz+TqX80wXRq6xAtb7GSBAp6gqLKfOdANg9qEmAI1Bxn04IrQr0Mzm4AhxvGKoHzjHjMgXbCCSSDxcw==", - "cpu": [ - "x64" - ], + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" } }, - "node_modules/esbuild-openbsd-64": { - "version": "0.15.9", - "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.9.tgz", - "integrity": "sha512-JmS18acQl4iSAjrEha1MfEmUMN4FcnnrtTaJ7Qg0tDCOcgpPPQRLGsZqhes0vmx8VA6IqRyScqXvaL7+Q0Uf3A==", - "cpu": [ - "x64" - ], + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" + "requires": { + "color-name": "~1.1.4" } }, - "node_modules/esbuild-sunos-64": { - "version": "0.15.9", - "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.9.tgz", - "integrity": "sha512-UKynGSWpzkPmXW3D2UMOD9BZPIuRaSqphxSCwScfEE05Be3KAmvjsBhht1fLzKpiFVJb0BYMd4jEbWMyJ/z1hQ==", - "cpu": [ - "x64" - ], + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" } }, - "node_modules/esbuild-windows-32": { - "version": "0.15.9", - "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.9.tgz", - "integrity": "sha512-aqXvu4/W9XyTVqO/hw3rNxKE1TcZiEYHPsXM9LwYmKSX9/hjvfIJzXwQBlPcJ/QOxedfoMVH0YnhhQ9Ffb0RGA==", - "cpu": [ - "ia32" - ], + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" + "requires": { + "ms": "2.1.2" } }, - "node_modules/esbuild-windows-64": { - "version": "0.15.9", - "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.9.tgz", - "integrity": "sha512-zm7h91WUmlS4idMtjvCrEeNhlH7+TNOmqw5dJPJZrgFaxoFyqYG6CKDpdFCQXdyKpD5yvzaQBOMVTCBVKGZDEg==", - "cpu": [ - "x64" - ], + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "define-properties": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", + "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" + "requires": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" } }, - "node_modules/esbuild-windows-arm64": { - "version": "0.15.9", - "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.9.tgz", - "integrity": "sha512-yQEVIv27oauAtvtuhJVfSNMztJJX47ismRS6Sv2QMVV9RM+6xjbMWuuwM2nxr5A2/gj/mu2z9YlQxiwoFRCfZA==", - "cpu": [ - "arm64" - ], + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" + "requires": { + "path-type": "^4.0.0" } }, - "node_modules/typescript": { - "version": "4.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.3.tgz", - "integrity": "sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==", + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" + "requires": { + "esutils": "^2.0.2" } - } - }, - "dependencies": { - "@esbuild/android-arm": { - "version": "0.15.9", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.9.tgz", - "integrity": "sha512-VZPy/ETF3fBG5PiinIkA0W/tlsvlEgJccyN2DzWZEl0DlVKRbu91PvY2D6Lxgluj4w9QtYHjOWjAT44C+oQ+EQ==", + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", "dev": true, - "optional": true + "requires": { + "is-arrayish": "^0.2.1" + } }, - "@esbuild/linux-loong64": { - "version": "0.15.9", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.9.tgz", - "integrity": "sha512-O+NfmkfRrb3uSsTa4jE3WApidSe3N5++fyOVGP1SmMZi4A3BZELkhUUvj5hwmMuNdlpzAZ8iAPz2vmcR7DCFQA==", + "es-abstract": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.3.tgz", + "integrity": "sha512-AyrnaKVpMzljIdwjzrj+LxGmj8ik2LckwXacHqrJJ/jxz6dDDBcZ7I7nlHM0FvEW8MfbWJwOd+yT2XzYW49Frw==", "dev": true, - "optional": true + "requires": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.1.3", + "get-symbol-description": "^1.0.0", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "is-callable": "^1.2.6", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.2", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.4.3", + "safe-regex-test": "^1.0.0", + "string.prototype.trimend": "^1.0.5", + "string.prototype.trimstart": "^1.0.5", + "unbox-primitive": "^1.0.2" + } }, - "@types/emscripten": { - "version": "1.39.6", - "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.39.6.tgz", - "integrity": "sha512-H90aoynNhhkQP6DRweEjJp5vfUVdIj7tdPLsu7pq89vODD/lcugKfZOsfgwpvM6XUewEp2N5dCg1Uf3Qe55Dcg==", - "dev": true + "es-shim-unscopables": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", + "integrity": "sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w==", + "dev": true, + "requires": { + "has": "^1.0.3" + } }, - "@types/wicg-file-system-access": { - "version": "2020.9.5", - "resolved": "https://registry.npmjs.org/@types/wicg-file-system-access/-/wicg-file-system-access-2020.9.5.tgz", - "integrity": "sha512-UYK244awtmcUYQfs7FR8710MJcefL2WvkyHMjA8yJzxd1mo0Gfn88sRZ1Bls7hiUhA2w7ne1gpJ9T5g3G0wOyA==", - "dev": true + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } }, "esbuild": { "version": "0.15.9", @@ -624,10 +650,1584 @@ "dev": true, "optional": true }, - "typescript": { - "version": "4.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.3.tgz", - "integrity": "sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==", + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "eslint": { + "version": "8.24.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.24.0.tgz", + "integrity": "sha512-dWFaPhGhTAiPcCgm3f6LI2MBWbogMnTJzFBbhXVRQDJPkr9pGZvVjlVfXd+vyDcWPA2Ic9L2AXPIQM0+vk/cSQ==", + "dev": true, + "requires": { + "@eslint/eslintrc": "^1.3.2", + "@humanwhocodes/config-array": "^0.10.5", + "@humanwhocodes/gitignore-to-minimatch": "^1.0.2", + "@humanwhocodes/module-importer": "^1.0.1", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.1.1", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.4.0", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.1", + "globals": "^13.15.0", + "globby": "^11.1.0", + "grapheme-splitter": "^1.0.4", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-sdsl": "^4.1.4", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "regexpp": "^3.2.0", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0" + }, + "dependencies": { + "eslint-scope": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", + "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + } + } + }, + "eslint-config-standard": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-17.0.0.tgz", + "integrity": "sha512-/2ks1GKyqSOkH7JFvXJicu0iMpoojkwB+f5Du/1SC0PtBL+s8v30k9njRZ21pm2drKYm2342jFnGWzttxPmZVg==", + "dev": true + }, + "eslint-config-standard-with-typescript": { + "version": "23.0.0", + "resolved": "https://registry.npmjs.org/eslint-config-standard-with-typescript/-/eslint-config-standard-with-typescript-23.0.0.tgz", + "integrity": "sha512-iaaWifImn37Z1OXbNW1es7KI+S7D408F9ys0bpaQf2temeBWlvb0Nc5qHkOgYaRb5QxTZT32GGeN1gtswASOXA==", + "dev": true, + "requires": { + "@typescript-eslint/parser": "^5.0.0", + "eslint-config-standard": "17.0.0" + } + }, + "eslint-import-resolver-node": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz", + "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==", + "dev": true, + "requires": { + "debug": "^3.2.7", + "resolve": "^1.20.0" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "eslint-module-utils": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz", + "integrity": "sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==", + "dev": true, + "requires": { + "debug": "^3.2.7" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "eslint-plugin-es": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-4.1.0.tgz", + "integrity": "sha512-GILhQTnjYE2WorX5Jyi5i4dz5ALWxBIdQECVQavL6s7cI76IZTDWleTHkxz/QT3kvcs2QlGHvKLYsSlPOlPXnQ==", + "dev": true, + "requires": { + "eslint-utils": "^2.0.0", + "regexpp": "^3.0.0" + }, + "dependencies": { + "eslint-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", + "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "eslint-visitor-keys": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", + "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "dev": true + } + } + }, + "eslint-plugin-import": { + "version": "2.26.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz", + "integrity": "sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==", + "dev": true, + "requires": { + "array-includes": "^3.1.4", + "array.prototype.flat": "^1.2.5", + "debug": "^2.6.9", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-module-utils": "^2.7.3", + "has": "^1.0.3", + "is-core-module": "^2.8.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.values": "^1.1.5", + "resolve": "^1.22.0", + "tsconfig-paths": "^3.14.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + } + } + }, + "eslint-plugin-n": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-15.3.0.tgz", + "integrity": "sha512-IyzPnEWHypCWasDpxeJnim60jhlumbmq0pubL6IOcnk8u2y53s5QfT8JnXy7skjHJ44yWHRb11PLtDHuu1kg/Q==", + "dev": true, + "requires": { + "builtins": "^5.0.1", + "eslint-plugin-es": "^4.1.0", + "eslint-utils": "^3.0.0", + "ignore": "^5.1.1", + "is-core-module": "^2.10.0", + "minimatch": "^3.1.2", + "resolve": "^1.22.1", + "semver": "^7.3.7" + } + }, + "eslint-plugin-promise": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.0.1.tgz", + "integrity": "sha512-uM4Tgo5u3UWQiroOyDEsYcVMOo7re3zmno0IZmB5auxoaQNIceAbXEkSt8RNrKtaYehARHG06pYK6K1JhtP0Zw==", + "dev": true + }, + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^2.0.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true + } + } + }, + "eslint-visitor-keys": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "dev": true + }, + "espree": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.0.tgz", + "integrity": "sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==", + "dev": true, + "requires": { + "acorn": "^8.8.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.3.0" + } + }, + "esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + } + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + } + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-glob": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "function.prototype.name": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", + "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + } + }, + "functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true + }, + "get-intrinsic": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", + "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + } + }, + "get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + } + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globals": { + "version": "13.17.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz", + "integrity": "sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + } + }, + "graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + }, + "grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.1" + } + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true + }, + "has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "ignore": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "internal-slot": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "requires": { + "has-bigints": "^1.0.1" + } + }, + "is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true + }, + "is-core-module": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.10.0.tgz", + "integrity": "sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "js-sdsl": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.4.tgz", + "integrity": "sha512-Y2/yD55y5jteOAmY50JbUZYwk3CP3wnLPEZnlR1w9oKhITrBEtAxwuWKebFf8hMrPMgbYwFoWK/lH2sBkErELw==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + } + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", + "dev": true + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "npm-run-all": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", + "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "chalk": "^2.4.1", + "cross-spawn": "^6.0.5", + "memorystream": "^0.3.1", + "minimatch": "^3.0.4", + "pidtree": "^0.3.0", + "read-pkg": "^3.0.0", + "shell-quote": "^1.6.1", + "string.prototype.padend": "^3.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dev": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "dev": true + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + } + }, + "object.values": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz", + "integrity": "sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "dev": true, + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "pidtree": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz", + "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==", + "dev": true + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==", + "dev": true, + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + }, + "dependencies": { + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + } + } + }, + "regexp.prototype.flags": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", + "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "functions-have-names": "^1.2.2" + } + }, + "regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true + }, + "resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dev": true, + "requires": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "safe-regex-test": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", + "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + } + }, + "semver": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "shell-quote": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz", + "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==", + "dev": true + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "spdx-correct": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.12.tgz", + "integrity": "sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA==", + "dev": true + }, + "string.prototype.padend": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.3.tgz", + "integrity": "sha512-jNIIeokznm8SD/TZISQsZKYu7RJyheFNt84DUPrh482GC8RVp2MKqm2O5oBRdGxbDQoXrhhWtPIWQOiy20svUg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + } + }, + "string.prototype.trimend": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", + "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + } + }, + "string.prototype.trimstart": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", + "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "tsconfig-paths": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz", + "integrity": "sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==", + "dev": true, + "requires": { + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + }, + "typescript": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.3.tgz", + "integrity": "sha512-goMHfm00nWPa8UvR/CPSvykqf6dVV8x/dp0c5mFTMTIu0u0FlGWRioyy7Nn0PGAdHxpJZnuO/ut+PpQ8UiHAig==", + "dev": true + }, + "unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + } + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "requires": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true } } diff --git a/src/Web/Avalonia.Web/webapp/package.json b/src/Web/Avalonia.Web/webapp/package.json index ca029ce534..8845dec604 100644 --- a/src/Web/Avalonia.Web/webapp/package.json +++ b/src/Web/Avalonia.Web/webapp/package.json @@ -1,13 +1,22 @@ { "name": "avalonia.web", "scripts": { - "prebuild": "node ./node_modules/typescript/bin/tsc -noEmit", + "typecheck": "npx tsc -noEmit", + "eslint": "npx eslint . --fix", + "prebuild": "npm-run-all typecheck eslint", "build": "node build.js" }, "devDependencies": { "@types/emscripten": "^1.39.6", "@types/wicg-file-system-access": "^2020.9.5", - "typescript": "^4.7.4", - "esbuild": "^0.15.7" + "@typescript-eslint/eslint-plugin": "^5.38.1", + "esbuild": "^0.15.7", + "eslint": "^8.24.0", + "eslint-config-standard-with-typescript": "^23.0.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-n": "^15.3.0", + "eslint-plugin-promise": "^6.0.1", + "npm-run-all": "^4.1.5", + "typescript": "^4.8.3" } } From 4dcbc24359759b001c0e8fb70e4848db7c13b606 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 27 Sep 2022 22:11:10 -0400 Subject: [PATCH 059/169] Port StorageProvider to use new API --- .../Platform/Storage/IStorageFile.cs | 2 + .../Utilities/AvaloniaResourcesIndex.cs | 2 - .../GenerateAvaloniaResourcesTask.cs | 1 - src/Web/Avalonia.Web/Avalonia.Web.csproj | 1 + src/Web/Avalonia.Web/AvaloniaView.cs | 8 - src/Web/Avalonia.Web/BrowserTopLevelImpl.cs | 5 +- src/Web/Avalonia.Web/Interop/InputHelper.cs | 8 +- src/Web/Avalonia.Web/Interop/StorageHelper.cs | 55 ++++ src/Web/Avalonia.Web/Interop/StreamHelper.cs | 35 +-- .../Storage/BlobReadableStream.cs | 27 +- .../Storage/BrowserStorageProvider.cs | 257 ++++++++++++++++++ .../Avalonia.Web/Storage/WriteableStream.cs | 27 +- .../Avalonia.Web/webapp/modules/avalonia.ts | 6 +- .../webapp/modules/avalonia/caniuse.ts | 13 + .../webapp/modules/avalonia/stream.ts | 40 +++ .../Avalonia.Web/webapp/modules/storage.ts | 7 +- .../webapp/modules/storage/indexedDb.ts | 84 ++++++ .../webapp/modules/storage/storageItem.ts | 111 ++++++++ .../webapp/modules/storage/storageProvider.ts | 70 +++++ src/Web/Avalonia.Web/webapp/types/dotnet.d.ts | 23 +- 20 files changed, 691 insertions(+), 91 deletions(-) create mode 100644 src/Web/Avalonia.Web/Interop/StorageHelper.cs create mode 100644 src/Web/Avalonia.Web/Storage/BrowserStorageProvider.cs create mode 100644 src/Web/Avalonia.Web/webapp/modules/avalonia/caniuse.ts create mode 100644 src/Web/Avalonia.Web/webapp/modules/avalonia/stream.ts create mode 100644 src/Web/Avalonia.Web/webapp/modules/storage/indexedDb.ts create mode 100644 src/Web/Avalonia.Web/webapp/modules/storage/storageItem.ts create mode 100644 src/Web/Avalonia.Web/webapp/modules/storage/storageProvider.ts diff --git a/src/Avalonia.Base/Platform/Storage/IStorageFile.cs b/src/Avalonia.Base/Platform/Storage/IStorageFile.cs index 46aa6efa72..4aa84e3ec4 100644 --- a/src/Avalonia.Base/Platform/Storage/IStorageFile.cs +++ b/src/Avalonia.Base/Platform/Storage/IStorageFile.cs @@ -18,6 +18,7 @@ public interface IStorageFile : IStorageItem /// /// Opens a stream for read access. /// + /// Task OpenReadAsync(); /// @@ -28,5 +29,6 @@ public interface IStorageFile : IStorageItem /// /// Opens stream for writing to the file. /// + /// Task OpenWriteAsync(); } diff --git a/src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs b/src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs index bd3b86f06d..b8692bb771 100644 --- a/src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs +++ b/src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs @@ -1,9 +1,7 @@ using System; using System.Collections.Generic; using System.IO; -using System.Runtime.CompilerServices; using System.Runtime.Serialization; -using System.Runtime.Serialization.Json; using System.Xml.Linq; using System.Linq; diff --git a/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs b/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs index c20b2f656e..75758d1315 100644 --- a/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs +++ b/src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.Serialization; -using System.Runtime.Serialization.Json; using System.Text; using Avalonia.Markup.Xaml.PortableXaml; using Avalonia.Utilities; diff --git a/src/Web/Avalonia.Web/Avalonia.Web.csproj b/src/Web/Avalonia.Web/Avalonia.Web.csproj index fd5f976f4b..04b31b1a2f 100644 --- a/src/Web/Avalonia.Web/Avalonia.Web.csproj +++ b/src/Web/Avalonia.Web/Avalonia.Web.csproj @@ -1,6 +1,7 @@ net7.0 + preview enable true diff --git a/src/Web/Avalonia.Web/AvaloniaView.cs b/src/Web/Avalonia.Web/AvaloniaView.cs index 6d7ddeb022..6cc5e42902 100644 --- a/src/Web/Avalonia.Web/AvaloniaView.cs +++ b/src/Web/Avalonia.Web/AvaloniaView.cs @@ -1,5 +1,4 @@ using System; -using System.Runtime.CompilerServices; using System.Runtime.InteropServices.JavaScript; using Avalonia.Controls; using Avalonia.Controls.Embedding; @@ -7,7 +6,6 @@ using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Input.TextInput; -using Avalonia.Platform.Storage; using Avalonia.Rendering.Composition; using Avalonia.Threading; using Avalonia.Web.Interop; @@ -337,12 +335,6 @@ namespace Avalonia.Web //return _nativeControlHost ?? throw new InvalidOperationException("Blazor View wasn't initialized yet"); } - internal IStorageProvider GetStorageProvider() - { - throw new NotImplementedException(); - //return _storageProvider ?? throw new InvalidOperationException("Blazor View wasn't initialized yet"); - } - private void ForceBlit() { // Note: this is technically a hack, but it's a kinda unique use case when diff --git a/src/Web/Avalonia.Web/BrowserTopLevelImpl.cs b/src/Web/Avalonia.Web/BrowserTopLevelImpl.cs index 047b462a64..d52d7164ce 100644 --- a/src/Web/Avalonia.Web/BrowserTopLevelImpl.cs +++ b/src/Web/Avalonia.Web/BrowserTopLevelImpl.cs @@ -11,8 +11,7 @@ using Avalonia.Platform; using Avalonia.Platform.Storage; using Avalonia.Rendering; using Avalonia.Rendering.Composition; - -#nullable enable +using Avalonia.Web.Storage; namespace Avalonia.Web { @@ -223,6 +222,6 @@ namespace Avalonia.Web public ITextInputMethodImpl TextInputMethod => _avaloniaView; public INativeControlHostImpl? NativeControlHost => _avaloniaView.GetNativeControlHostImpl(); - public IStorageProvider StorageProvider => _avaloniaView.GetStorageProvider(); + public IStorageProvider StorageProvider { get; } = new BrowserStorageProvider(); } } diff --git a/src/Web/Avalonia.Web/Interop/InputHelper.cs b/src/Web/Avalonia.Web/Interop/InputHelper.cs index e728acdc52..b32b6566f2 100644 --- a/src/Web/Avalonia.Web/Interop/InputHelper.cs +++ b/src/Web/Avalonia.Web/Interop/InputHelper.cs @@ -1,8 +1,6 @@ using System; using System.Runtime.InteropServices.JavaScript; using System.Threading.Tasks; -using static System.Net.Mime.MediaTypeNames; -using static System.Runtime.InteropServices.JavaScript.JSType; namespace Avalonia.Web.Interop; @@ -39,7 +37,7 @@ internal static partial class InputHelper Func pointerUp, [JSMarshalAs>] Func wheel); - + [JSImport("InputHelper.subscribeInputEvents", "avalonia.ts")] public static partial void SubscribeInputEvents( @@ -72,9 +70,9 @@ internal static partial class InputHelper [JSImport("InputHelper.setBounds", "avalonia.ts")] public static partial void SetBounds(JSObject htmlElement, int x, int y, int width, int height, int caret); - [JSImport("navigator.clipboard.readText")] + [JSImport("globalThis.navigator.clipboard.readText")] public static partial Task ReadClipboardTextAsync(); - [JSImport("navigator.clipboard.writeText")] + [JSImport("globalThis.navigator.clipboard.writeText")] public static partial Task WriteClipboardTextAsync(string text); } diff --git a/src/Web/Avalonia.Web/Interop/StorageHelper.cs b/src/Web/Avalonia.Web/Interop/StorageHelper.cs new file mode 100644 index 0000000000..4dc55fd959 --- /dev/null +++ b/src/Web/Avalonia.Web/Interop/StorageHelper.cs @@ -0,0 +1,55 @@ +using System.Runtime.InteropServices.JavaScript; +using System.Threading.Tasks; + +namespace Avalonia.Web.Interop; + +internal static partial class StorageHelper +{ + [JSImport("Caniuse.canShowOpenFilePicker", "avalonia.ts")] + public static partial bool CanShowOpenFilePicker(); + + [JSImport("Caniuse.canShowSaveFilePicker", "avalonia.ts")] + public static partial bool CanShowSaveFilePicker(); + + [JSImport("Caniuse.canShowDirectoryPicker", "avalonia.ts")] + public static partial bool CanShowDirectoryPicker(); + + [JSImport("StorageProvider.selectFolderDialog", "storage.ts")] + public static partial Task SelectFolderDialog(JSObject? startIn); + + [JSImport("StorageProvider.openFileDialog", "storage.ts")] + public static partial Task OpenFileDialog(JSObject? startIn, bool multiple, + [JSMarshalAs>] object[]? types, bool excludeAcceptAllOption); + + [JSImport("StorageProvider.saveFileDialog", "storage.ts")] + public static partial Task SaveFileDialog(JSObject? startIn, string? suggestedName, + [JSMarshalAs>] object[]? types, bool excludeAcceptAllOption); + + [JSImport("StorageProvider.openBookmark", "storage.ts")] + public static partial Task OpenBookmark(string key); + + [JSImport("StorageItem.saveBookmark", "storage.ts")] + public static partial Task SaveBookmark(JSObject item); + + [JSImport("StorageItem.deleteBookmark", "storage.ts")] + public static partial Task DeleteBookmark(JSObject item); + + [JSImport("StorageItem.getProperties", "storage.ts")] + public static partial Task GetProperties(JSObject item); + + [JSImport("StorageItem.openWrite", "storage.ts")] + public static partial Task OpenWrite(JSObject item); + + [JSImport("StorageItem.openRead", "storage.ts")] + public static partial Task OpenRead(JSObject item); + + [JSImport("StorageItem.getItems", "storage.ts")] + [return: JSMarshalAs>] + public static partial Task GetItems(JSObject item); + + [JSImport("StorageItems.itemsArray", "storage.ts")] + public static partial JSObject[] ItemsArray(JSObject item); + + [JSImport("StorageProvider.createAcceptType", "storage.ts")] + public static partial JSObject CreateAcceptType(string description, string[] mimeTypes); +} diff --git a/src/Web/Avalonia.Web/Interop/StreamHelper.cs b/src/Web/Avalonia.Web/Interop/StreamHelper.cs index c77d87fbe4..0a83bbb871 100644 --- a/src/Web/Avalonia.Web/Interop/StreamHelper.cs +++ b/src/Web/Avalonia.Web/Interop/StreamHelper.cs @@ -13,43 +13,28 @@ internal static partial class StreamHelper public static partial void Seek(JSObject stream, [JSMarshalAs] long position); [JSImport("StreamHelper.truncate", "avalonia.ts")] - public static partial void Truncate(JSObject stream, [JSMarshalAs] long position); - - [JSImport("StreamHelper.write", "avalonia.ts")] - public static partial void Write(JSObject stream, [JSMarshalAs] Span data); + public static partial void Truncate(JSObject stream, [JSMarshalAs] long size); [JSImport("StreamHelper.write", "avalonia.ts")] public static partial Task WriteAsync(JSObject stream, [JSMarshalAs] ArraySegment data); - [JSImport("StreamHelper.close", "avalonia.ts")] - public static partial void Close(JSObject stream); - [JSImport("StreamHelper.close", "avalonia.ts")] public static partial Task CloseAsync(JSObject stream); - [JSImport("StreamHelper.size", "avalonia.ts")] - [return: JSMarshalAs] - public static partial long Size(JSObject stream); - [JSImport("StreamHelper.byteLength", "avalonia.ts")] [return: JSMarshalAs] public static partial long ByteLength(JSObject stream); - [JSImport("StreamHelper.sliceToArray", "avalonia.ts")] - [return: JSMarshalAs] - public static partial Span Slice(JSObject stream, [JSMarshalAs] long offset, int count); + [JSImport("StreamHelper.sliceArrayBuffer", "avalonia.ts")] + private static partial Task SliceToArrayBuffer(JSObject stream, [JSMarshalAs] long offset, int count); + + [JSImport("StreamHelper.toMemoryView", "avalonia.ts")] + [return: JSMarshalAs>] + private static partial byte[] ArrayBufferToMemoryView(JSObject stream); - public static async Task> SliceAsync(JSObject stream, long offset, int count) + public static async Task SliceAsync(JSObject stream, long offset, int count) { - using var buffer = await SliceToBufferAsync(stream, offset, count); - return BufferToArray(buffer); + using var buffer = await SliceToArrayBuffer(stream, offset, count); + return ArrayBufferToMemoryView(buffer); } - - [JSImport("StreamHelper.slice", "avalonia.ts")] - [return: JSMarshalAs>] - private static partial Task SliceToBufferAsync(JSObject stream, [JSMarshalAs] long offset, int count); - - [JSImport("StreamHelper.toArray", "avalonia.ts")] - [return: JSMarshalAs] - private static partial ArraySegment BufferToArray(JSObject stream); } diff --git a/src/Web/Avalonia.Web/Storage/BlobReadableStream.cs b/src/Web/Avalonia.Web/Storage/BlobReadableStream.cs index a48f809c06..640c2fd745 100644 --- a/src/Web/Avalonia.Web/Storage/BlobReadableStream.cs +++ b/src/Web/Avalonia.Web/Storage/BlobReadableStream.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; namespace Avalonia.Web.Storage; -[System.Runtime.Versioning.SupportedOSPlatform("browser")] // gets rid of callsite warnings +[System.Runtime.Versioning.SupportedOSPlatform("browser")] internal class BlobReadableStream : Stream { private JSObject? _jSReference; @@ -20,7 +20,7 @@ internal class BlobReadableStream : Stream _length = StreamHelper.ByteLength(JSReference); } - private JSObject JSReference => _jSReference ?? throw new ObjectDisposedException(nameof(JSWriteableStream)); + private JSObject JSReference => _jSReference ?? throw new ObjectDisposedException(nameof(WriteableStream)); public override bool CanRead => true; @@ -55,21 +55,8 @@ internal class BlobReadableStream : Stream => throw new NotSupportedException(); public override int Read(byte[] buffer, int offset, int count) - => Read(buffer.AsSpan(offset, count)); - - public override int Read(Span buffer) { - var numBytesToRead = (int)Math.Min(buffer.Length, Length - _position); - var bytesRead = StreamHelper.Slice(JSReference, _position, numBytesToRead); - if (bytesRead.Length != numBytesToRead) - { - throw new EndOfStreamException("Failed to read the requested number of bytes from the stream."); - } - - _position += bytesRead.Length; - bytesRead.CopyTo(buffer); - - return bytesRead.Length; + throw new InvalidOperationException("Browser supports only ReadAsync"); } public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) @@ -79,15 +66,15 @@ internal class BlobReadableStream : Stream { var numBytesToRead = (int)Math.Min(buffer.Length, Length - _position); var bytesRead = await StreamHelper.SliceAsync(JSReference, _position, numBytesToRead); - if (bytesRead.Count != numBytesToRead) + if (bytesRead.Length != numBytesToRead) { throw new EndOfStreamException("Failed to read the requested number of bytes from the stream."); } - _position += bytesRead.Count; - bytesRead.AsMemory().CopyTo(buffer); + _position += bytesRead.Length; + bytesRead.CopyTo(buffer); - return bytesRead.Count; + return bytesRead.Length; } protected override void Dispose(bool disposing) diff --git a/src/Web/Avalonia.Web/Storage/BrowserStorageProvider.cs b/src/Web/Avalonia.Web/Storage/BrowserStorageProvider.cs new file mode 100644 index 0000000000..55ae2bdef0 --- /dev/null +++ b/src/Web/Avalonia.Web/Storage/BrowserStorageProvider.cs @@ -0,0 +1,257 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.JavaScript; +using System.Runtime.Versioning; +using System.Threading.Tasks; + +using Avalonia.Platform.Storage; +using Avalonia.Web.Interop; + +namespace Avalonia.Web.Storage; + +internal record FilePickerAcceptType(string Description, IReadOnlyDictionary> Accept); + +[SupportedOSPlatform("browser")] +internal class BrowserStorageProvider : IStorageProvider +{ + internal const string PickerCancelMessage = "The user aborted a request"; + internal const string NoPermissionsMessage = "Permissions denied"; + + private readonly Lazy> _lazyModule = new(() => JSHost.ImportAsync("storage.ts", "./storage.js")); + + public bool CanOpen => StorageHelper.CanShowOpenFilePicker(); + public bool CanSave => StorageHelper.CanShowSaveFilePicker(); + public bool CanPickFolder => StorageHelper.CanShowDirectoryPicker(); + + public async Task> OpenFilePickerAsync(FilePickerOpenOptions options) + { + _ = await _lazyModule.Value; + var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle; + + var (types, exludeAll) = ConvertFileTypes(options.FileTypeFilter); + + try + { + using var items = await StorageHelper.OpenFileDialog(startIn, options.AllowMultiple, types, exludeAll); + if (items is null) + { + return Array.Empty(); + } + + var itemsArray = StorageHelper.ItemsArray(items); + return itemsArray.Select(item => new JSStorageFile(item)).ToArray(); + } + catch (JSException ex) when (ex.Message.Contains(PickerCancelMessage, StringComparison.Ordinal)) + { + return Array.Empty(); + } + finally + { + if (types is not null) + { + foreach (var type in types) + { + type.Dispose(); + } + } + } + } + + public async Task SaveFilePickerAsync(FilePickerSaveOptions options) + { + _ = await _lazyModule.Value; + var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle; + + var (types, exludeAll) = ConvertFileTypes(options.FileTypeChoices); + + try + { + var item = await StorageHelper.SaveFileDialog(startIn, options.SuggestedFileName, types, exludeAll); + return item is not null ? new JSStorageFile(item) : null; + } + catch (JSException ex) when (ex.Message.Contains(PickerCancelMessage, StringComparison.Ordinal)) + { + return null; + } + finally + { + if (types is not null) + { + foreach (var type in types) + { + type.Dispose(); + } + } + } + } + + public async Task> OpenFolderPickerAsync(FolderPickerOpenOptions options) + { + _ = await _lazyModule.Value; + var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle; + + try + { + var item = await StorageHelper.SelectFolderDialog(startIn); + return item is not null ? new[] { new JSStorageFolder(item) } : Array.Empty(); + } + catch (JSException ex) when (ex.Message.Contains(PickerCancelMessage, StringComparison.Ordinal)) + { + return Array.Empty(); + } + } + + public async Task OpenFileBookmarkAsync(string bookmark) + { + _ = await _lazyModule.Value; + var item = await StorageHelper.OpenBookmark(bookmark); + return item is not null ? new JSStorageFile(item) : null; + } + + public async Task OpenFolderBookmarkAsync(string bookmark) + { + _ = await _lazyModule.Value; + var item = await StorageHelper.OpenBookmark(bookmark); + return item is not null ? new JSStorageFolder(item) : null; + } + + private static (JSObject[]? types, bool excludeAllOption) ConvertFileTypes(IEnumerable? input) + { + var types = input? + .Where(t => t.MimeTypes?.Any() == true && t != FilePickerFileTypes.All) + .Select(t => StorageHelper.CreateAcceptType(t.Name, t.MimeTypes!.ToArray())) + .ToArray(); + if (types?.Length == 0) + { + types = null; + } + + var inlcudeAll = input?.Contains(FilePickerFileTypes.All) == true || types is null; + + return (types, !inlcudeAll); + } +} + +internal abstract class JSStorageItem : IStorageBookmarkItem +{ + internal JSObject? _fileHandle; + + protected JSStorageItem(JSObject fileHandle) + { + _fileHandle = fileHandle ?? throw new ArgumentNullException(nameof(fileHandle)); + } + + internal JSObject FileHandle => _fileHandle ?? throw new ObjectDisposedException(nameof(JSStorageItem)); + + public string Name => FileHandle.GetPropertyAsString("name") ?? string.Empty; + + public bool TryGetUri([NotNullWhen(true)] out Uri? uri) + { + uri = new Uri(Name, UriKind.Relative); + return false; + } + + public async Task GetBasicPropertiesAsync() + { + using var properties = await StorageHelper.GetProperties(FileHandle); + var size = (long?)properties?.GetPropertyAsDouble("Size"); + var lastModified = (long?)properties?.GetPropertyAsDouble("LastModified"); + + return new StorageItemProperties( + (ulong?)size, + dateCreated: null, + dateModified: lastModified > 0 ? DateTimeOffset.FromUnixTimeMilliseconds(lastModified.Value) : null); + } + + public bool CanBookmark => true; + + public Task SaveBookmarkAsync() + { + return StorageHelper.SaveBookmark(FileHandle); + } + + public Task GetParentAsync() + { + return Task.FromResult(null); + } + + public Task ReleaseBookmarkAsync() + { + return StorageHelper.DeleteBookmark(FileHandle); + } + + public void Dispose() + { + _fileHandle?.Dispose(); + _fileHandle = null; + } +} + +internal class JSStorageFile : JSStorageItem, IStorageBookmarkFile +{ + public JSStorageFile(JSObject fileHandle) : base(fileHandle) + { + } + + public bool CanOpenRead => true; + public async Task OpenReadAsync() + { + try + { + var blob = await StorageHelper.OpenRead(FileHandle); + return new BlobReadableStream(blob); + } + catch (JSException ex) when (ex.Message == BrowserStorageProvider.NoPermissionsMessage) + { + throw new UnauthorizedAccessException("User denied permissions to open the file", ex); + } + } + + public bool CanOpenWrite => true; + public async Task OpenWriteAsync() + { + try + { + using var properties = await StorageHelper.GetProperties(FileHandle); + var streamWriter = await StorageHelper.OpenWrite(FileHandle); + var size = (long?)properties?.GetPropertyAsDouble("Size") ?? 0; + + return new WriteableStream(streamWriter, size); + } + catch (JSException ex) when (ex.Message == BrowserStorageProvider.NoPermissionsMessage) + { + throw new UnauthorizedAccessException("User denied permissions to open the file", ex); + } + } +} + +internal class JSStorageFolder : JSStorageItem, IStorageBookmarkFolder +{ + public JSStorageFolder(JSObject fileHandle) : base(fileHandle) + { + } + + public async Task> GetItemsAsync() + { + using var items = await StorageHelper.GetItems(FileHandle); + if (items is null) + { + return Array.Empty(); + } + + var itemsArray = StorageHelper.ItemsArray(items); + + return itemsArray + .Select(reference => reference.GetPropertyAsString("kind") switch + { + "directory" => (IStorageItem)new JSStorageFolder(reference), + "file" => new JSStorageFile(reference), + _ => null + }) + .Where(i => i is not null) + .ToArray()!; + } +} diff --git a/src/Web/Avalonia.Web/Storage/WriteableStream.cs b/src/Web/Avalonia.Web/Storage/WriteableStream.cs index 100b7ccfed..f9699fef7a 100644 --- a/src/Web/Avalonia.Web/Storage/WriteableStream.cs +++ b/src/Web/Avalonia.Web/Storage/WriteableStream.cs @@ -6,22 +6,22 @@ using System.Threading.Tasks; namespace Avalonia.Web.Storage; -[System.Runtime.Versioning.SupportedOSPlatform("browser")] // gets rid of callsite warnings +[System.Runtime.Versioning.SupportedOSPlatform("browser")] // Loose wrapper implementaion of a stream on top of FileAPI FileSystemWritableFileStream -internal sealed class JSWriteableStream : Stream +internal sealed class WriteableStream : Stream { private JSObject? _jSReference; // Unfortunatelly we can't read current length/position, so we need to keep it C#-side only. private long _length, _position; - internal JSWriteableStream(JSObject jSReference, long initialLength) + internal WriteableStream(JSObject jSReference, long initialLength) { _jSReference = jSReference; _length = initialLength; } - private JSObject JSReference => _jSReference ?? throw new ObjectDisposedException(nameof(JSWriteableStream)); + private JSObject JSReference => _jSReference ?? throw new ObjectDisposedException(nameof(WriteableStream)); public override bool CanRead => false; @@ -75,20 +75,7 @@ internal sealed class JSWriteableStream : Stream public override void Write(byte[] buffer, int offset, int count) { - StreamHelper.Write(JSReference, buffer.AsSpan(offset, count)); - } - - public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - if (offset != 0 || count != buffer.Length) - { - // TODO, we need to pass prepared buffer to the JS - // Can't use ArrayPool as it can return bigger array than requested - // Can't use Span/Memory, as it's not supported by JS interop yet. - // Alternatively we can pass original buffer and offset+count, so it can be trimmed on the JS side (but is it more efficient tho?) - buffer = buffer.AsMemory(offset, count).ToArray(); - } - return WriteAsyncInternal(buffer, cancellationToken); + throw new InvalidOperationException("Browser supports only WriteAsync"); } public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) @@ -110,7 +97,7 @@ internal sealed class JSWriteableStream : Stream _jSReference = null; try { - StreamHelper.Close(JSReference); + _ = StreamHelper.CloseAsync(jsReference); } finally { @@ -126,7 +113,7 @@ internal sealed class JSWriteableStream : Stream _jSReference = null; try { - await StreamHelper.CloseAsync(JSReference); + await StreamHelper.CloseAsync(jsReference); } finally { diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia.ts index 542d48cef5..796bae61de 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia.ts @@ -3,6 +3,8 @@ import { SizeWatcher, DpiWatcher, Canvas } from "./avalonia/canvas"; import { InputHelper } from "./avalonia/input"; import { AvaloniaDOM } from "./avalonia/dom"; +import { Caniuse } from "./avalonia/caniuse"; +import { StreamHelper } from "./avalonia/stream"; export async function createAvaloniaRuntime(api: RuntimeAPI): Promise { api.setModuleImports("avalonia.ts", { @@ -10,6 +12,8 @@ export async function createAvaloniaRuntime(api: RuntimeAPI): Promise { InputHelper, SizeWatcher, DpiWatcher, - AvaloniaDOM + AvaloniaDOM, + Caniuse, + StreamHelper }); } diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/caniuse.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/caniuse.ts new file mode 100644 index 0000000000..6dedcb724f --- /dev/null +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/caniuse.ts @@ -0,0 +1,13 @@ +export class Caniuse { + public static canShowOpenFilePicker(): boolean { + return typeof window.showOpenFilePicker !== "undefined"; + } + + public static canShowSaveFilePicker(): boolean { + return typeof window.showSaveFilePicker !== "undefined"; + } + + public static canShowDirectoryPicker(): boolean { + return typeof window.showDirectoryPicker !== "undefined"; + } +} diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/stream.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/stream.ts new file mode 100644 index 0000000000..1f2c181edc --- /dev/null +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/stream.ts @@ -0,0 +1,40 @@ +import { IMemoryView } from "../../types/dotnet"; + +export class StreamHelper { + public static async seek(stream: FileSystemWritableFileStream, position: number) { + return await stream.seek(position); + } + + public static async truncate(stream: FileSystemWritableFileStream, size: number) { + return await stream.truncate(size); + } + + public static async close(stream: FileSystemWritableFileStream) { + return await stream.close(); + } + + public static async write(stream: FileSystemWritableFileStream, span: IMemoryView) { + const array = new Uint8Array(span.byteLength); + span.copyTo(array); + + const data: WriteParams = { + type: "write", + data: array + }; + + return await stream.write(data); + } + + public static byteLength(stream: Blob) { + return stream.size; + } + + public static async sliceArrayBuffer(stream: Blob, offset: number, count: number) { + const buffer = await stream.slice(offset, offset + count).arrayBuffer(); + return new Uint8Array(buffer); + } + + public static toMemoryView(buffer: Uint8Array): Uint8Array { + return buffer; + } +} diff --git a/src/Web/Avalonia.Web/webapp/modules/storage.ts b/src/Web/Avalonia.Web/webapp/modules/storage.ts index f6dfd9151b..8b9987afa0 100644 --- a/src/Web/Avalonia.Web/webapp/modules/storage.ts +++ b/src/Web/Avalonia.Web/webapp/modules/storage.ts @@ -1,5 +1,2 @@ -export class StorageProvider { - static isFileApiSupported(): boolean { - return (globalThis as any).showOpenFilePicker !== undefined; - } -} +export { StorageItem, StorageItems } from "./storage/storageItem"; +export { StorageProvider } from "./storage/storageProvider"; diff --git a/src/Web/Avalonia.Web/webapp/modules/storage/indexedDb.ts b/src/Web/Avalonia.Web/webapp/modules/storage/indexedDb.ts new file mode 100644 index 0000000000..54b095d39e --- /dev/null +++ b/src/Web/Avalonia.Web/webapp/modules/storage/indexedDb.ts @@ -0,0 +1,84 @@ +class InnerDbConnection { + constructor(private readonly database: IDBDatabase) { } + + private openStore(store: string, mode: IDBTransactionMode): IDBObjectStore { + const tx = this.database.transaction(store, mode); + return tx.objectStore(store); + } + + public async put(store: string, obj: any, key?: IDBValidKey): Promise { + const os = this.openStore(store, "readwrite"); + + return await new Promise((resolve, reject) => { + const response = os.put(obj, key); + response.onsuccess = () => { + resolve(response.result); + }; + response.onerror = () => { + reject(response.error); + }; + }); + } + + public get(store: string, key: IDBValidKey): any { + const os = this.openStore(store, "readonly"); + + return new Promise((resolve, reject) => { + const response = os.get(key); + response.onsuccess = () => { + resolve(response.result); + }; + response.onerror = () => { + reject(response.error); + }; + }); + } + + public async delete(store: string, key: IDBValidKey): Promise { + const os = this.openStore(store, "readwrite"); + + return await new Promise((resolve, reject) => { + const response = os.delete(key); + response.onsuccess = () => { + resolve(); + }; + response.onerror = () => { + reject(response.error); + }; + }); + } + + public close() { + this.database.close(); + } +} + +class IndexedDbWrapper { + constructor(private readonly databaseName: string, private readonly objectStores: [string]) { + } + + public async connect(): Promise { + const conn = window.indexedDB.open(this.databaseName, 1); + + conn.onupgradeneeded = event => { + const db = (event.target as IDBRequest).result; + this.objectStores.forEach(store => { + db.createObjectStore(store); + }); + }; + + return await new Promise((resolve, reject) => { + conn.onsuccess = event => { + resolve(new InnerDbConnection((event.target as IDBRequest).result)); + }; + conn.onerror = event => { + reject((event.target as IDBRequest).error); + }; + }); + } +} + +export const fileBookmarksStore: string = "fileBookmarks"; +export const avaloniaDb = new IndexedDbWrapper("AvaloniaDb", [ + fileBookmarksStore +]); diff --git a/src/Web/Avalonia.Web/webapp/modules/storage/storageItem.ts b/src/Web/Avalonia.Web/webapp/modules/storage/storageItem.ts new file mode 100644 index 0000000000..b4cf3d278b --- /dev/null +++ b/src/Web/Avalonia.Web/webapp/modules/storage/storageItem.ts @@ -0,0 +1,111 @@ +import { avaloniaDb, fileBookmarksStore } from "./indexedDb"; + +export class StorageItem { + constructor(public handle: FileSystemHandle, private readonly bookmarkId?: string) { } + + public get name(): string { + return this.handle.name; + } + + public get kind(): string { + return this.handle.kind; + } + + public static async openRead(item: StorageItem): Promise { + if (!(item.handle instanceof FileSystemFileHandle)) { + throw new Error("StorageItem is not a file"); + } + + await item.verityPermissions("read"); + + const file = await item.handle.getFile(); + return file; + } + + public static async openWrite(item: StorageItem): Promise { + if (!(item.handle instanceof FileSystemFileHandle)) { + throw new Error("StorageItem is not a file"); + } + + await item.verityPermissions("readwrite"); + + return await item.handle.createWritable({ keepExistingData: true }); + } + + public static async getProperties(item: StorageItem): Promise<{ Size: number; LastModified: number; Type: string } | null> { + const file = item.handle instanceof FileSystemFileHandle && + await item.handle.getFile(); + + if (!file) { + return null; + } + + return { + Size: file.size, + LastModified: file.lastModified, + Type: file.type + }; + } + + public static async getItems(item: StorageItem): Promise { + if (item.handle.kind !== "directory") { + return new StorageItems([]); + } + + const items: StorageItem[] = []; + for await (const [, value] of (item.handle as any).entries()) { + items.push(new StorageItem(value)); + } + return new StorageItems(items); + } + + private async verityPermissions(mode: FileSystemPermissionMode): Promise { + if (await this.handle.queryPermission({ mode }) === "granted") { + return; + } + + if (await this.handle.requestPermission({ mode }) === "denied") { + throw new Error("Permissions denied"); + } + } + + public static async saveBookmark(item: StorageItem): Promise { + // If file was previously bookmarked, just return old one. + if (item.bookmarkId) { + return item.bookmarkId; + } + + const connection = await avaloniaDb.connect(); + try { + const key = await connection.put(fileBookmarksStore, item.handle, item.generateBookmarkId()); + return key as string; + } finally { + connection.close(); + } + } + + public static async deleteBookmark(item: StorageItem): Promise { + if (!item.bookmarkId) { + return; + } + + const connection = await avaloniaDb.connect(); + try { + await connection.delete(fileBookmarksStore, item.bookmarkId); + } finally { + connection.close(); + } + } + + private generateBookmarkId(): string { + return Date.now().toString(36) + Math.random().toString(36).substring(2); + } +} + +export class StorageItems { + constructor(private readonly items: StorageItem[]) { } + + public static itemsArray(instance: StorageItems): StorageItem[] { + return instance.items; + } +} diff --git a/src/Web/Avalonia.Web/webapp/modules/storage/storageProvider.ts b/src/Web/Avalonia.Web/webapp/modules/storage/storageProvider.ts new file mode 100644 index 0000000000..0198f92528 --- /dev/null +++ b/src/Web/Avalonia.Web/webapp/modules/storage/storageProvider.ts @@ -0,0 +1,70 @@ +import { avaloniaDb, fileBookmarksStore } from "./indexedDb"; +import { StorageItem, StorageItems } from "./storageItem"; + +declare global { + type WellKnownDirectory = "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos"; + type StartInDirectory = WellKnownDirectory | FileSystemHandle; + interface OpenFilePickerOptions { + startIn?: StartInDirectory; + } + interface SaveFilePickerOptions { + startIn?: StartInDirectory; + } +} + +export class StorageProvider { + public static async selectFolderDialog( + startIn: StorageItem | null): Promise { + // 'Picker' API doesn't accept "null" as a parameter, so it should be set to undefined. + const options: DirectoryPickerOptions = { + startIn: (startIn?.handle ?? undefined) + }; + + const handle = await window.showDirectoryPicker(options); + return new StorageItem(handle); + } + + public static async openFileDialog( + startIn: StorageItem | null, multiple: boolean, + types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean): Promise { + const options: OpenFilePickerOptions = { + startIn: (startIn?.handle ?? undefined), + multiple, + excludeAcceptAllOption, + types: (types ?? undefined) + }; + + const handles = await window.showOpenFilePicker(options); + return new StorageItems(handles.map((handle: FileSystemHandle) => new StorageItem(handle))); + } + + public static async saveFileDialog( + startIn: StorageItem | null, suggestedName: string | null, + types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean): Promise { + const options: SaveFilePickerOptions = { + startIn: (startIn?.handle ?? undefined), + suggestedName: (suggestedName ?? undefined), + excludeAcceptAllOption, + types: (types ?? undefined) + }; + + const handle = await window.showSaveFilePicker(options); + return new StorageItem(handle); + } + + public static async openBookmark(key: string): Promise { + const connection = await avaloniaDb.connect(); + try { + const handle = await connection.get(fileBookmarksStore, key); + return handle && new StorageItem(handle, key); + } finally { + connection.close(); + } + } + + public static createAcceptType(description: string, mimeTypes: string[]): FilePickerAcceptType { + const accept: Record = {}; + mimeTypes.forEach(a => { accept[a] = []; }); + return { description, accept }; + } +} diff --git a/src/Web/Avalonia.Web/webapp/types/dotnet.d.ts b/src/Web/Avalonia.Web/webapp/types/dotnet.d.ts index bc2068cb59..0067ee3e0e 100644 --- a/src/Web/Avalonia.Web/webapp/types/dotnet.d.ts +++ b/src/Web/Avalonia.Web/webapp/types/dotnet.d.ts @@ -246,4 +246,25 @@ declare global { declare const dotnet: ModuleAPI["dotnet"]; declare const exit: ModuleAPI["exit"]; -export { CreateDotnetRuntimeType, DotnetModuleConfig, EmscriptenModule, ModuleAPI, MonoConfig, RuntimeAPI, createDotnetRuntime as default, dotnet, exit }; \ No newline at end of file +export { CreateDotnetRuntimeType, DotnetModuleConfig, EmscriptenModule, ModuleAPI, MonoConfig, RuntimeAPI, createDotnetRuntime as default, dotnet, exit }; + +export interface IMemoryView { + /** + * copies elements from provided source to the wasm memory. + * target has to have the elements of the same type as the underlying C# array. + * same as TypedArray.set() + */ + set(source: TypedArray, targetOffset?: number): void; + /** + * copies elements from wasm memory to provided target. + * target has to have the elements of the same type as the underlying C# array. + */ + copyTo(target: TypedArray, sourceOffset?: number): void; + /** + * same as TypedArray.slice() + */ + slice(start?: number, end?: number): TypedArray; + + get length(): number; + get byteLength(): number; +} From a090e0efb83c09d68b36ba4040f8af296b22bb1e Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 27 Sep 2022 22:16:56 -0400 Subject: [PATCH 060/169] Move Skia related files to the new folder --- src/Web/Avalonia.Web/AvaloniaView.cs | 2 ++ src/Web/Avalonia.Web/BrowserSingleViewLifetime.cs | 1 + src/Web/Avalonia.Web/BrowserTopLevelImpl.cs | 1 + src/Web/Avalonia.Web/{ => Skia}/BrowserSkiaGpu.cs | 2 +- .../Avalonia.Web/{ => Skia}/BrowserSkiaGpuRenderSession.cs | 2 +- src/Web/Avalonia.Web/{ => Skia}/BrowserSkiaGpuRenderTarget.cs | 2 +- src/Web/Avalonia.Web/{ => Skia}/BrowserSkiaRasterSurface.cs | 2 +- src/Web/Avalonia.Web/{ => Skia}/BrowserSkiaSurface.cs | 4 ++-- src/Web/Avalonia.Web/{ => Skia}/IBrowserSkiaSurface.cs | 2 +- 9 files changed, 11 insertions(+), 7 deletions(-) rename src/Web/Avalonia.Web/{ => Skia}/BrowserSkiaGpu.cs (95%) rename src/Web/Avalonia.Web/{ => Skia}/BrowserSkiaGpuRenderSession.cs (97%) rename src/Web/Avalonia.Web/{ => Skia}/BrowserSkiaGpuRenderTarget.cs (97%) rename src/Web/Avalonia.Web/{ => Skia}/BrowserSkiaRasterSurface.cs (98%) rename src/Web/Avalonia.Web/{ => Skia}/BrowserSkiaSurface.cs (95%) rename src/Web/Avalonia.Web/{ => Skia}/IBrowserSkiaSurface.cs (84%) diff --git a/src/Web/Avalonia.Web/AvaloniaView.cs b/src/Web/Avalonia.Web/AvaloniaView.cs index 6cc5e42902..1ed1dad3e7 100644 --- a/src/Web/Avalonia.Web/AvaloniaView.cs +++ b/src/Web/Avalonia.Web/AvaloniaView.cs @@ -9,6 +9,8 @@ using Avalonia.Input.TextInput; using Avalonia.Rendering.Composition; using Avalonia.Threading; using Avalonia.Web.Interop; +using Avalonia.Web.Skia; + using SkiaSharp; namespace Avalonia.Web diff --git a/src/Web/Avalonia.Web/BrowserSingleViewLifetime.cs b/src/Web/Avalonia.Web/BrowserSingleViewLifetime.cs index 02d9b01319..091ab3f68c 100644 --- a/src/Web/Avalonia.Web/BrowserSingleViewLifetime.cs +++ b/src/Web/Avalonia.Web/BrowserSingleViewLifetime.cs @@ -3,6 +3,7 @@ using System; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Media; +using Avalonia.Web.Skia; namespace Avalonia.Web { diff --git a/src/Web/Avalonia.Web/BrowserTopLevelImpl.cs b/src/Web/Avalonia.Web/BrowserTopLevelImpl.cs index d52d7164ce..d08c153966 100644 --- a/src/Web/Avalonia.Web/BrowserTopLevelImpl.cs +++ b/src/Web/Avalonia.Web/BrowserTopLevelImpl.cs @@ -11,6 +11,7 @@ using Avalonia.Platform; using Avalonia.Platform.Storage; using Avalonia.Rendering; using Avalonia.Rendering.Composition; +using Avalonia.Web.Skia; using Avalonia.Web.Storage; namespace Avalonia.Web diff --git a/src/Web/Avalonia.Web/BrowserSkiaGpu.cs b/src/Web/Avalonia.Web/Skia/BrowserSkiaGpu.cs similarity index 95% rename from src/Web/Avalonia.Web/BrowserSkiaGpu.cs rename to src/Web/Avalonia.Web/Skia/BrowserSkiaGpu.cs index 20765b5449..f80838232b 100644 --- a/src/Web/Avalonia.Web/BrowserSkiaGpu.cs +++ b/src/Web/Avalonia.Web/Skia/BrowserSkiaGpu.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using Avalonia.Skia; -namespace Avalonia.Web +namespace Avalonia.Web.Skia { public class BrowserSkiaGpu : ISkiaGpu { diff --git a/src/Web/Avalonia.Web/BrowserSkiaGpuRenderSession.cs b/src/Web/Avalonia.Web/Skia/BrowserSkiaGpuRenderSession.cs similarity index 97% rename from src/Web/Avalonia.Web/BrowserSkiaGpuRenderSession.cs rename to src/Web/Avalonia.Web/Skia/BrowserSkiaGpuRenderSession.cs index 022ed20022..a7f7d9db3d 100644 --- a/src/Web/Avalonia.Web/BrowserSkiaGpuRenderSession.cs +++ b/src/Web/Avalonia.Web/Skia/BrowserSkiaGpuRenderSession.cs @@ -1,7 +1,7 @@ using Avalonia.Skia; using SkiaSharp; -namespace Avalonia.Web +namespace Avalonia.Web.Skia { internal class BrowserSkiaGpuRenderSession : ISkiaGpuRenderSession { diff --git a/src/Web/Avalonia.Web/BrowserSkiaGpuRenderTarget.cs b/src/Web/Avalonia.Web/Skia/BrowserSkiaGpuRenderTarget.cs similarity index 97% rename from src/Web/Avalonia.Web/BrowserSkiaGpuRenderTarget.cs rename to src/Web/Avalonia.Web/Skia/BrowserSkiaGpuRenderTarget.cs index 644ebf19c8..dba9b34166 100644 --- a/src/Web/Avalonia.Web/BrowserSkiaGpuRenderTarget.cs +++ b/src/Web/Avalonia.Web/Skia/BrowserSkiaGpuRenderTarget.cs @@ -1,7 +1,7 @@ using Avalonia.Skia; using SkiaSharp; -namespace Avalonia.Web +namespace Avalonia.Web.Skia { internal class BrowserSkiaGpuRenderTarget : ISkiaGpuRenderTarget { diff --git a/src/Web/Avalonia.Web/BrowserSkiaRasterSurface.cs b/src/Web/Avalonia.Web/Skia/BrowserSkiaRasterSurface.cs similarity index 98% rename from src/Web/Avalonia.Web/BrowserSkiaRasterSurface.cs rename to src/Web/Avalonia.Web/Skia/BrowserSkiaRasterSurface.cs index 0946e72d1d..c7005583ac 100644 --- a/src/Web/Avalonia.Web/BrowserSkiaRasterSurface.cs +++ b/src/Web/Avalonia.Web/Skia/BrowserSkiaRasterSurface.cs @@ -5,7 +5,7 @@ using Avalonia.Platform; using Avalonia.Skia; using SkiaSharp; -namespace Avalonia.Web +namespace Avalonia.Web.Skia { internal class BrowserSkiaRasterSurface : IBrowserSkiaSurface, IFramebufferPlatformSurface, IDisposable { diff --git a/src/Web/Avalonia.Web/BrowserSkiaSurface.cs b/src/Web/Avalonia.Web/Skia/BrowserSkiaSurface.cs similarity index 95% rename from src/Web/Avalonia.Web/BrowserSkiaSurface.cs rename to src/Web/Avalonia.Web/Skia/BrowserSkiaSurface.cs index 6970cb1f22..27a206c0ec 100644 --- a/src/Web/Avalonia.Web/BrowserSkiaSurface.cs +++ b/src/Web/Avalonia.Web/Skia/BrowserSkiaSurface.cs @@ -1,7 +1,7 @@ using Avalonia.Web.Interop; using SkiaSharp; -namespace Avalonia.Web +namespace Avalonia.Web.Skia { internal class BrowserSkiaSurface : IBrowserSkiaSurface { @@ -14,7 +14,7 @@ namespace Avalonia.Web Scaling = scaling; Origin = origin; } - + public SKColorType ColorType { get; set; } public PixelSize Size { get; set; } diff --git a/src/Web/Avalonia.Web/IBrowserSkiaSurface.cs b/src/Web/Avalonia.Web/Skia/IBrowserSkiaSurface.cs similarity index 84% rename from src/Web/Avalonia.Web/IBrowserSkiaSurface.cs rename to src/Web/Avalonia.Web/Skia/IBrowserSkiaSurface.cs index 4b492f6b17..7301ae45cd 100644 --- a/src/Web/Avalonia.Web/IBrowserSkiaSurface.cs +++ b/src/Web/Avalonia.Web/Skia/IBrowserSkiaSurface.cs @@ -1,4 +1,4 @@ -namespace Avalonia.Web +namespace Avalonia.Web.Skia { internal interface IBrowserSkiaSurface { From 1c58c4dba114ed73afdbadd8db90da2c0fa2d083 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 27 Sep 2022 23:33:02 -0400 Subject: [PATCH 061/169] Add native control host support --- .../Avalonia.Web.Sample.csproj | 1 + .../EmbedSample.Browser.cs | 44 ++++++ src/Web/Avalonia.Web.Sample/Program.cs | 7 +- src/Web/Avalonia.Web.Sample/embed.js | 11 ++ src/Web/Avalonia.Web/AvaloniaView.cs | 3 +- .../Avalonia.Web/BrowserNativeControlHost.cs | 136 ++++++++++++++++++ src/Web/Avalonia.Web/BrowserTopLevelImpl.cs | 6 +- .../Interop/NativeControlHostHelper.cs | 28 ++++ src/Web/Avalonia.Web/JSObjectControlHandle.cs | 30 ++++ .../Avalonia.Web/webapp/modules/avalonia.ts | 7 +- .../modules/avalonia/nativeControlHost.ts | 55 +++++++ 11 files changed, 318 insertions(+), 10 deletions(-) create mode 100644 src/Web/Avalonia.Web.Sample/EmbedSample.Browser.cs create mode 100644 src/Web/Avalonia.Web.Sample/embed.js create mode 100644 src/Web/Avalonia.Web/BrowserNativeControlHost.cs create mode 100644 src/Web/Avalonia.Web/Interop/NativeControlHostHelper.cs create mode 100644 src/Web/Avalonia.Web/JSObjectControlHandle.cs create mode 100644 src/Web/Avalonia.Web/webapp/modules/avalonia/nativeControlHost.ts diff --git a/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj b/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj index ad8d14e527..13aad8c13e 100644 --- a/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj +++ b/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj @@ -30,6 +30,7 @@ + diff --git a/src/Web/Avalonia.Web.Sample/EmbedSample.Browser.cs b/src/Web/Avalonia.Web.Sample/EmbedSample.Browser.cs new file mode 100644 index 0000000000..5baa4a6b35 --- /dev/null +++ b/src/Web/Avalonia.Web.Sample/EmbedSample.Browser.cs @@ -0,0 +1,44 @@ +using System; +using System.Runtime.InteropServices.JavaScript; + +using Avalonia; +using Avalonia.Platform; +using Avalonia.Web; + +using ControlCatalog.Pages; + +namespace ControlCatalog.Web; + +public class EmbedSampleWeb : INativeDemoControl +{ + public IPlatformHandle CreateControl(bool isSecond, IPlatformHandle parent, Func createDefault) + { + if (isSecond) + { + var iframe = EmbedInterop.CreateElement("iframe"); + iframe.SetProperty("src", "https://www.youtube.com/embed/kZCIporjJ70"); + + return new JSObjectControlHandle(iframe); + } + else + { + var defaultHandle = (JSObjectControlHandle)createDefault(); + + _ = JSHost.ImportAsync("embed.js", "./embed.js").ContinueWith(_ => + { + EmbedInterop.AddAppButton(defaultHandle.Object); + }); + + return defaultHandle; + } + } +} + +internal static partial class EmbedInterop +{ + [JSImport("globalThis.document.createElement")] + public static partial JSObject CreateElement(string tagName); + + [JSImport("addAppButton", "embed.js")] + public static partial void AddAppButton(JSObject parentObject); +} diff --git a/src/Web/Avalonia.Web.Sample/Program.cs b/src/Web/Avalonia.Web.Sample/Program.cs index 233f95852f..52acabb0fa 100644 --- a/src/Web/Avalonia.Web.Sample/Program.cs +++ b/src/Web/Avalonia.Web.Sample/Program.cs @@ -1,12 +1,17 @@ using Avalonia; using Avalonia.Web; using ControlCatalog; +using ControlCatalog.Web; internal partial class Program { private static void Main(string[] args) { - BuildAvaloniaApp().SetupBrowserApp("out"); + BuildAvaloniaApp() + .AfterSetup(_ => + { + ControlCatalog.Pages.EmbedSample.Implementation = new EmbedSampleWeb(); + }).SetupBrowserApp("out"); } public static AppBuilder BuildAvaloniaApp() diff --git a/src/Web/Avalonia.Web.Sample/embed.js b/src/Web/Avalonia.Web.Sample/embed.js new file mode 100644 index 0000000000..f393c80314 --- /dev/null +++ b/src/Web/Avalonia.Web.Sample/embed.js @@ -0,0 +1,11 @@ +export function addAppButton(parent) { + var button = globalThis.document.createElement('button'); + button.innerText = 'Hello world'; + var clickCount = 0; + button.onclick = () => { + clickCount++; + button.innerText = 'Click count ' + clickCount; + }; + parent.appendChild(button); + return button; +} diff --git a/src/Web/Avalonia.Web/AvaloniaView.cs b/src/Web/Avalonia.Web/AvaloniaView.cs index 1ed1dad3e7..e81620ffde 100644 --- a/src/Web/Avalonia.Web/AvaloniaView.cs +++ b/src/Web/Avalonia.Web/AvaloniaView.cs @@ -333,8 +333,7 @@ namespace Avalonia.Web internal INativeControlHostImpl GetNativeControlHostImpl() { - throw new NotImplementedException(); - //return _nativeControlHost ?? throw new InvalidOperationException("Blazor View wasn't initialized yet"); + return new BrowserNativeControlHost(_nativeControlsContainer); } private void ForceBlit() diff --git a/src/Web/Avalonia.Web/BrowserNativeControlHost.cs b/src/Web/Avalonia.Web/BrowserNativeControlHost.cs new file mode 100644 index 0000000000..4cdcf627e6 --- /dev/null +++ b/src/Web/Avalonia.Web/BrowserNativeControlHost.cs @@ -0,0 +1,136 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices.JavaScript; + +using Avalonia.Controls.Platform; +using Avalonia.Platform; +using Avalonia.Web.Interop; + +namespace Avalonia.Web +{ + internal class BrowserNativeControlHost : INativeControlHostImpl + { + private readonly JSObject _hostElement; + + public BrowserNativeControlHost(JSObject element) + { + _hostElement = element; + } + + public INativeControlHostDestroyableControlHandle CreateDefaultChild(IPlatformHandle parent) + { + var element = NativeControlHostHelper.CreateDefaultChild(null); + return new JSObjectControlHandle(element); + } + + public INativeControlHostControlTopLevelAttachment CreateNewAttachment(Func create) + { + Attachment? a = null; + try + { + var child = create(new JSObjectControlHandle(_hostElement)); + var attachmenetReference = NativeControlHostHelper.CreateAttachment(); + // It has to be assigned to the variable before property setter is called so we dispose it on exception +#pragma warning disable IDE0017 // Simplify object initialization + a = new Attachment(attachmenetReference, child); +#pragma warning restore IDE0017 // Simplify object initialization + a.AttachedTo = this; + return a; + } + catch + { + a?.Dispose(); + throw; + } + } + + public INativeControlHostControlTopLevelAttachment CreateNewAttachment(IPlatformHandle handle) + { + var attachmenetReference = NativeControlHostHelper.CreateAttachment(); + var a = new Attachment(attachmenetReference, handle); + a.AttachedTo = this; + return a; + } + + public bool IsCompatibleWith(IPlatformHandle handle) => handle is JSObjectControlHandle; + + private class Attachment : INativeControlHostControlTopLevelAttachment + { + private const string InitializeWithChildHandleSymbol = "InitializeWithChildHandle"; + private const string AttachToSymbol = "AttachTo"; + private const string ShowInBoundsSymbol = "ShowInBounds"; + private const string HideWithSizeSymbol = "HideWithSize"; + private const string ReleaseChildSymbol = "ReleaseChild"; + + private JSObject? _native; + private BrowserNativeControlHost? _attachedTo; + + public Attachment(JSObject native, IPlatformHandle handle) + { + _native = native; + NativeControlHostHelper.InitializeWithChildHandle(_native, ((JSObjectControlHandle)handle).Object); + } + + public void Dispose() + { + if (_native != null) + { + NativeControlHostHelper.ReleaseChild(_native); + _native.Dispose(); + _native = null; + } + } + + public INativeControlHostImpl? AttachedTo + { + get => _attachedTo!; + set + { + CheckDisposed(); + + var host = (BrowserNativeControlHost?)value; + if (host == null) + { + NativeControlHostHelper.AttachTo(_native, null); + } + else + { + NativeControlHostHelper.AttachTo(_native, host._hostElement); + } + _attachedTo = host; + } + } + + public bool IsCompatibleWith(INativeControlHostImpl host) => host is BrowserNativeControlHost; + + public void HideWithSize(Size size) + { + CheckDisposed(); + if (_attachedTo == null) + return; + + NativeControlHostHelper.HideWithSize(_native, Math.Max(1, size.Width), Math.Max(1, size.Height)); + } + + public void ShowInBounds(Rect bounds) + { + CheckDisposed(); + + if (_attachedTo == null) + throw new InvalidOperationException("Native control isn't attached to a toplevel"); + + bounds = new Rect(bounds.X, bounds.Y, Math.Max(1, bounds.Width), + Math.Max(1, bounds.Height)); + + NativeControlHostHelper.ShowInBounds(_native, bounds.X, bounds.Y, bounds.Width, bounds.Height); + } + + [MemberNotNull(nameof(_native))] + private void CheckDisposed() + { + if (_native == null) + throw new ObjectDisposedException(nameof(Attachment)); + } + } + } +} diff --git a/src/Web/Avalonia.Web/BrowserTopLevelImpl.cs b/src/Web/Avalonia.Web/BrowserTopLevelImpl.cs index d08c153966..b955da6df2 100644 --- a/src/Web/Avalonia.Web/BrowserTopLevelImpl.cs +++ b/src/Web/Avalonia.Web/BrowserTopLevelImpl.cs @@ -35,13 +35,11 @@ namespace Avalonia.Web AcrylicCompensationLevels = new AcrylicPlatformCompensationLevels(1, 1, 1); _touchDevice = new TouchDevice(); _penDevice = new PenDevice(); + NativeControlHost = _avaloniaView.GetNativeControlHostImpl(); } public ulong Timestamp => (ulong)_sw.ElapsedMilliseconds; - - - public void SetClientSize(Size newSize, double dpi) { if (Math.Abs(RenderScaling - dpi) > 0.0001) @@ -222,7 +220,7 @@ namespace Avalonia.Web public ITextInputMethodImpl TextInputMethod => _avaloniaView; - public INativeControlHostImpl? NativeControlHost => _avaloniaView.GetNativeControlHostImpl(); + public INativeControlHostImpl? NativeControlHost { get; } public IStorageProvider StorageProvider { get; } = new BrowserStorageProvider(); } } diff --git a/src/Web/Avalonia.Web/Interop/NativeControlHostHelper.cs b/src/Web/Avalonia.Web/Interop/NativeControlHostHelper.cs new file mode 100644 index 0000000000..5cc86bf622 --- /dev/null +++ b/src/Web/Avalonia.Web/Interop/NativeControlHostHelper.cs @@ -0,0 +1,28 @@ +using System; +using System.Runtime.InteropServices.JavaScript; + +namespace Avalonia.Web.Interop; + +internal static partial class NativeControlHostHelper +{ + [JSImport("NativeControlHost.createDefaultChild", "avalonia.ts")] + internal static partial JSObject CreateDefaultChild(JSObject? parent); + + [JSImport("NativeControlHost.createAttachment", "avalonia.ts")] + internal static partial JSObject CreateAttachment(); + + [JSImport("NativeControlHost.initializeWithChildHandle", "avalonia.ts")] + internal static partial void InitializeWithChildHandle(JSObject element, JSObject child); + + [JSImport("NativeControlHost.attachTo", "avalonia.ts")] + internal static partial void AttachTo(JSObject element, JSObject? host); + + [JSImport("NativeControlHost.showInBounds", "avalonia.ts")] + internal static partial void ShowInBounds(JSObject element, double x, double y, double width, double height); + + [JSImport("NativeControlHost.hideWithSize", "avalonia.ts")] + internal static partial void HideWithSize(JSObject element, double width, double height); + + [JSImport("NativeControlHost.releaseChild", "avalonia.ts")] + internal static partial void ReleaseChild(JSObject element); +} diff --git a/src/Web/Avalonia.Web/JSObjectControlHandle.cs b/src/Web/Avalonia.Web/JSObjectControlHandle.cs new file mode 100644 index 0000000000..e56ca123eb --- /dev/null +++ b/src/Web/Avalonia.Web/JSObjectControlHandle.cs @@ -0,0 +1,30 @@ +using System; +using System.Runtime.InteropServices.JavaScript; + +using Avalonia.Controls.Platform; + +namespace Avalonia.Web; + +public class JSObjectControlHandle : INativeControlHostDestroyableControlHandle +{ + internal const string ElementReferenceDescriptor = "JSObject"; + + public JSObjectControlHandle(JSObject reference) + { + Object = reference; + } + + public JSObject Object { get; } + + public IntPtr Handle => throw new NotSupportedException(); + + public string? HandleDescriptor => ElementReferenceDescriptor; + + public void Destroy() + { + if (Object is JSObject inProcess && !inProcess.IsDisposed) + { + inProcess.Dispose(); + } + } +} diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia.ts index 796bae61de..6992921f5b 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia.ts @@ -1,19 +1,20 @@ import { RuntimeAPI } from "../types/dotnet"; import { SizeWatcher, DpiWatcher, Canvas } from "./avalonia/canvas"; - import { InputHelper } from "./avalonia/input"; import { AvaloniaDOM } from "./avalonia/dom"; import { Caniuse } from "./avalonia/caniuse"; import { StreamHelper } from "./avalonia/stream"; +import { NativeControlHost } from "./avalonia/NativeControlHost"; export async function createAvaloniaRuntime(api: RuntimeAPI): Promise { api.setModuleImports("avalonia.ts", { + Caniuse, Canvas, InputHelper, SizeWatcher, DpiWatcher, AvaloniaDOM, - Caniuse, - StreamHelper + StreamHelper, + NativeControlHost }); } diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/nativeControlHost.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/nativeControlHost.ts new file mode 100644 index 0000000000..c65d5836df --- /dev/null +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/nativeControlHost.ts @@ -0,0 +1,55 @@ +class NativeControlHostTopLevelAttachment { + _child?: HTMLElement; + _host?: HTMLElement; +} + +export class NativeControlHost { + public static createDefaultChild(parent?: HTMLElement): HTMLElement { + return document.createElement("div"); + } + + public static createAttachment(): NativeControlHostTopLevelAttachment { + return new NativeControlHostTopLevelAttachment(); + } + + public static initializeWithChildHandle(element: NativeControlHostTopLevelAttachment, child: HTMLElement): void { + element._child = child; + element._child.style.position = "absolute"; + } + + public static attachTo(element: NativeControlHostTopLevelAttachment, host?: HTMLElement): void { + if (element._host && element._child) { + element._host.removeChild(element._child); + } + + element._host = host; + + if (element._host && element._child) { + element._host.appendChild(element._child); + } + } + + public static showInBounds(element: NativeControlHostTopLevelAttachment, x: number, y: number, width: number, height: number): void { + if (element._child) { + element._child.style.top = `${y}px`; + element._child.style.left = `${x}px`; + element._child.style.width = `${width}px`; + element._child.style.height = `${height}px`; + element._child.style.display = "block"; + } + } + + public static hideWithSize(element: NativeControlHostTopLevelAttachment, width: number, height: number): void { + if (element._child) { + element._child.style.width = `${width}px`; + element._child.style.height = `${height}px`; + element._child.style.display = "none"; + } + } + + public static releaseChild(element: NativeControlHostTopLevelAttachment): void { + if (element._child) { + element._child = undefined; + } + } +} From 0415da9d29eb7ec7db86ca170f292d4d4a64b3d0 Mon Sep 17 00:00:00 2001 From: Gillibald Date: Wed, 28 Sep 2022 03:10:33 -0700 Subject: [PATCH 062/169] Emscripten interop sample --- src/Web/Avalonia.Web.Sample/Program.cs | 2 ++ src/Web/Avalonia.Web/Avalonia.Web.targets | 1 + src/Web/Avalonia.Web/Interop/Emscripten.cs | 31 ++++++++++++++++++++ src/Web/Avalonia.Web/Interop/libEmscripten.c | 9 ++++++ 4 files changed, 43 insertions(+) create mode 100644 src/Web/Avalonia.Web/Interop/Emscripten.cs create mode 100644 src/Web/Avalonia.Web/Interop/libEmscripten.c diff --git a/src/Web/Avalonia.Web.Sample/Program.cs b/src/Web/Avalonia.Web.Sample/Program.cs index 233f95852f..2570aa2d58 100644 --- a/src/Web/Avalonia.Web.Sample/Program.cs +++ b/src/Web/Avalonia.Web.Sample/Program.cs @@ -6,6 +6,8 @@ internal partial class Program { private static void Main(string[] args) { + Emscripten.Log(EM_LOG.ERROR, "MyError"); + BuildAvaloniaApp().SetupBrowserApp("out"); } diff --git a/src/Web/Avalonia.Web/Avalonia.Web.targets b/src/Web/Avalonia.Web/Avalonia.Web.targets index d1bec2aa93..537d746ab9 100644 --- a/src/Web/Avalonia.Web/Avalonia.Web.targets +++ b/src/Web/Avalonia.Web/Avalonia.Web.targets @@ -1,5 +1,6 @@ + diff --git a/src/Web/Avalonia.Web/Interop/Emscripten.cs b/src/Web/Avalonia.Web/Interop/Emscripten.cs new file mode 100644 index 0000000000..bfb42fa356 --- /dev/null +++ b/src/Web/Avalonia.Web/Interop/Emscripten.cs @@ -0,0 +1,31 @@ +using System; +using System.Runtime.InteropServices; + +namespace Avalonia.Web +{ + public static partial class Emscripten + { + const string Prefix = "av_"; + const string Library = "libEmscripten"; + + [LibraryImport(Library, EntryPoint = Prefix + "log", StringMarshalling = StringMarshalling.Utf8)] + public static partial void Log(EM_LOG flags, string format); + + [LibraryImport(Library, EntryPoint = Prefix + "debugger")] + public static partial void Debugger(); + } + + [Flags] + public enum EM_LOG : int + { + CONSOLE = 1, + WARN = 2, + ERROR = 4, + C_STACK = 8, + JS_STACK = 16, + NO_PATHS = 64, + FUNC_PARAMS = 128, + DEBUG = 256, + INFO = 512, + } +} diff --git a/src/Web/Avalonia.Web/Interop/libEmscripten.c b/src/Web/Avalonia.Web/Interop/libEmscripten.c new file mode 100644 index 0000000000..708be1abc8 --- /dev/null +++ b/src/Web/Avalonia.Web/Interop/libEmscripten.c @@ -0,0 +1,9 @@ +#include + +void av_log(int flags, const char* format) { + emscripten_log(flags, format); +} + +void av_debugger() { + emscripten_debugger(); +} From 6f2b426e395b5c1f56e0e8dc9476c8c0a947a82a Mon Sep 17 00:00:00 2001 From: dif-sam <41672086+dif-sam@users.noreply.github.com> Date: Mon, 3 Oct 2022 21:50:08 +0400 Subject: [PATCH 063/169] Update flag for XSetWMNormalHints That small change can fix bug with StartupLocation on Ubuntu. (https://stackoverflow.com/questions/11069666/cannot-get-xcreatesimplewindow-to-open-window-at-the-right-position) --- src/Avalonia.X11/X11Window.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 7b34705b3b..f24c33cafa 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -302,7 +302,7 @@ namespace Avalonia.X11 min_height = min.Height }; hints.height_inc = hints.width_inc = 1; - var flags = XSizeHintsFlags.PMinSize | XSizeHintsFlags.PResizeInc; + var flags = XSizeHintsFlags.PMinSize | XSizeHintsFlags.PResizeInc | XSizeHintsFlags.PPosition | XSizeHintsFlags.PSize; // People might be passing double.MaxValue if (max.Width < 100000 && max.Height < 100000) { From 14029cb934e7509e0b497de073fa680a1b4fc5a8 Mon Sep 17 00:00:00 2001 From: Arhell Date: Tue, 4 Oct 2022 01:05:27 +0300 Subject: [PATCH 064/169] update src folder links --- src/Avalonia.Base/Input/Cursor.cs | 2 +- src/Avalonia.Base/Media/PreciseEllipticArcHelper.cs | 2 +- .../Media/TextFormatting/Unicode/BinaryReaderExtensions.cs | 2 +- src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs | 2 +- src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreak.cs | 2 +- src/Avalonia.Base/Media/TextFormatting/Unicode/UnicodeTrie.cs | 2 +- .../TextFormatting/Unicode/UnicodeTrieBuilder.Constants.cs | 2 +- .../Media/TextFormatting/Unicode/UnicodeTrieBuilder.cs | 2 +- src/Avalonia.Base/Utilities/BinarySearchExtension.cs | 2 +- src/Avalonia.Controls/AutoCompleteBox.cs | 2 +- src/Avalonia.Controls/Calendar/Calendar.cs | 2 +- .../Calendar/CalendarBlackoutDatesCollection.cs | 2 +- src/Avalonia.Controls/Calendar/CalendarButton.cs | 2 +- src/Avalonia.Controls/Calendar/CalendarDateRange.cs | 2 +- src/Avalonia.Controls/Calendar/CalendarDayButton.cs | 2 +- src/Avalonia.Controls/Calendar/CalendarExtensions.cs | 2 +- src/Avalonia.Controls/Calendar/CalendarItem.cs | 2 +- src/Avalonia.Controls/Calendar/DateTimeHelper.cs | 2 +- src/Avalonia.Controls/Calendar/SelectedDatesCollection.cs | 2 +- src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs | 2 +- .../CalendarDatePickerDateValidationErrorEventArgs.cs | 2 +- .../CalendarDatePicker/CalendarDatePickerFormat.cs | 2 +- .../Primitives/PopupPositioning/IPopupPositioner.cs | 2 +- src/Avalonia.Controls/Utils/ISelectionAdapter.cs | 2 +- .../Utils/SelectingItemsControlSelectionAdapter.cs | 2 +- src/Avalonia.Native/IconLoader.cs | 2 +- src/Avalonia.Themes.Fluent/Controls/Calendar.xaml | 2 +- src/Avalonia.Themes.Fluent/Controls/CalendarButton.xaml | 2 +- src/Avalonia.Themes.Fluent/Controls/CalendarDayButton.xaml | 2 +- src/Avalonia.Themes.Fluent/Controls/CalendarItem.xaml | 2 +- src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml | 2 +- src/Avalonia.Themes.Fluent/Controls/DateTimePickerShared.xaml | 2 +- src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml | 2 +- src/Avalonia.Themes.Simple/Controls/CalendarItem.xaml | 2 +- src/Avalonia.Themes.Simple/Controls/DateTimePickerShared.xaml | 2 +- src/Avalonia.X11/X11Atoms.cs | 2 +- .../Avalonia.Web.Blazor/webapp/modules/Avalonia/CaretHelper.ts | 2 +- 37 files changed, 37 insertions(+), 37 deletions(-) diff --git a/src/Avalonia.Base/Input/Cursor.cs b/src/Avalonia.Base/Input/Cursor.cs index 98c4258a90..8e79206f93 100644 --- a/src/Avalonia.Base/Input/Cursor.cs +++ b/src/Avalonia.Base/Input/Cursor.cs @@ -33,7 +33,7 @@ namespace Avalonia.Input DragLink, None, - // Not available in GTK directly, see http://www.pixelbeat.org/programming/x_cursors/ + // Not available in GTK directly, see https://www.pixelbeat.org/programming/x_cursors/ // We might enable them later, preferably, by loading pixmax directly from theme with fallback image // SizeNorthWestSouthEast, // SizeNorthEastSouthWest, diff --git a/src/Avalonia.Base/Media/PreciseEllipticArcHelper.cs b/src/Avalonia.Base/Media/PreciseEllipticArcHelper.cs index 5dd647e8ca..49dfe3c7b3 100644 --- a/src/Avalonia.Base/Media/PreciseEllipticArcHelper.cs +++ b/src/Avalonia.Base/Media/PreciseEllipticArcHelper.cs @@ -1081,7 +1081,7 @@ namespace Avalonia.Media Point c = rest * (cs) + translation; - // See "http://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter" to understand + // See "https://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter" to understand // how the ellipse center is calculated diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BinaryReaderExtensions.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BinaryReaderExtensions.cs index 412007c6e0..ccbceb6a7b 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BinaryReaderExtensions.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BinaryReaderExtensions.cs @@ -5,7 +5,7 @@ // not use this product except in compliance with the License. You may obtain // a copy of the License at // -// http://www.apache.org/licenses/LICENSE-2.0 +// https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs index ab17263806..ce9cdde044 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/Codepoint.cs @@ -104,7 +104,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// Gets the canonical representation of a given codepoint. - /// + /// /// /// The code point to be mapped. /// The mapped canonical code point, or the passed . diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreak.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreak.cs index 34b14f008f..59c4df0a2e 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreak.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreak.cs @@ -5,7 +5,7 @@ // not use this product except in compliance with the License. You may obtain // a copy of the License at // -// http://www.apache.org/licenses/LICENSE-2.0 +// https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/UnicodeTrie.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/UnicodeTrie.cs index cf03ed7cd3..079f830ddc 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/UnicodeTrie.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/UnicodeTrie.cs @@ -5,7 +5,7 @@ // not use this product except in compliance with the License. You may obtain // a copy of the License at // -// http://www.apache.org/licenses/LICENSE-2.0 +// https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/UnicodeTrieBuilder.Constants.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/UnicodeTrieBuilder.Constants.cs index 29ee45acc2..de0304f4c9 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/UnicodeTrieBuilder.Constants.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/UnicodeTrieBuilder.Constants.cs @@ -5,7 +5,7 @@ // not use this product except in compliance with the License. You may obtain // a copy of the License at // -// http://www.apache.org/licenses/LICENSE-2.0 +// https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/UnicodeTrieBuilder.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/UnicodeTrieBuilder.cs index 87f96984c5..755d603539 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/UnicodeTrieBuilder.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/UnicodeTrieBuilder.cs @@ -5,7 +5,7 @@ // not use this product except in compliance with the License. You may obtain // a copy of the License at // -// http://www.apache.org/licenses/LICENSE-2.0 +// https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT diff --git a/src/Avalonia.Base/Utilities/BinarySearchExtension.cs b/src/Avalonia.Base/Utilities/BinarySearchExtension.cs index a4f6ae89c1..b7060d2e21 100644 --- a/src/Avalonia.Base/Utilities/BinarySearchExtension.cs +++ b/src/Avalonia.Base/Utilities/BinarySearchExtension.cs @@ -5,7 +5,7 @@ // not use this product except in compliance with the License. You may obtain // a copy of the License at // -// http://www.apache.org/licenses/LICENSE-2.0 +// https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index c675139831..8a8c4ead86 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -1,6 +1,6 @@ // (c) Copyright Microsoft Corporation. // This source is subject to the Microsoft Public License (Ms-PL). -// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// Please see https://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. using System; diff --git a/src/Avalonia.Controls/Calendar/Calendar.cs b/src/Avalonia.Controls/Calendar/Calendar.cs index 0de068a416..9c88bae5f6 100644 --- a/src/Avalonia.Controls/Calendar/Calendar.cs +++ b/src/Avalonia.Controls/Calendar/Calendar.cs @@ -1,6 +1,6 @@ // (c) Copyright Microsoft Corporation. // This source is subject to the Microsoft Public License (Ms-PL). -// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// Please see https://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. using System; diff --git a/src/Avalonia.Controls/Calendar/CalendarBlackoutDatesCollection.cs b/src/Avalonia.Controls/Calendar/CalendarBlackoutDatesCollection.cs index 5d883f2d14..a92feec509 100644 --- a/src/Avalonia.Controls/Calendar/CalendarBlackoutDatesCollection.cs +++ b/src/Avalonia.Controls/Calendar/CalendarBlackoutDatesCollection.cs @@ -1,6 +1,6 @@ // (c) Copyright Microsoft Corporation. // This source is subject to the Microsoft Public License (Ms-PL). -// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// Please see https://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. using System; diff --git a/src/Avalonia.Controls/Calendar/CalendarButton.cs b/src/Avalonia.Controls/Calendar/CalendarButton.cs index 0a8e4dfae8..d8672cbf18 100644 --- a/src/Avalonia.Controls/Calendar/CalendarButton.cs +++ b/src/Avalonia.Controls/Calendar/CalendarButton.cs @@ -1,6 +1,6 @@ // (c) Copyright Microsoft Corporation. // This source is subject to the Microsoft Public License (Ms-PL). -// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// Please see https://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. using Avalonia.Controls.Metadata; diff --git a/src/Avalonia.Controls/Calendar/CalendarDateRange.cs b/src/Avalonia.Controls/Calendar/CalendarDateRange.cs index 88bc5ed7bd..793ef7a2ee 100644 --- a/src/Avalonia.Controls/Calendar/CalendarDateRange.cs +++ b/src/Avalonia.Controls/Calendar/CalendarDateRange.cs @@ -1,6 +1,6 @@ // (c) Copyright Microsoft Corporation. // This source is subject to the Microsoft Public License (Ms-PL). -// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// Please see https://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. using System; diff --git a/src/Avalonia.Controls/Calendar/CalendarDayButton.cs b/src/Avalonia.Controls/Calendar/CalendarDayButton.cs index 2ba4e36260..3d0befdba7 100644 --- a/src/Avalonia.Controls/Calendar/CalendarDayButton.cs +++ b/src/Avalonia.Controls/Calendar/CalendarDayButton.cs @@ -1,6 +1,6 @@ // (c) Copyright Microsoft Corporation. // This source is subject to the Microsoft Public License (Ms-PL). -// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// Please see https://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. using System; diff --git a/src/Avalonia.Controls/Calendar/CalendarExtensions.cs b/src/Avalonia.Controls/Calendar/CalendarExtensions.cs index 00b5ce10bc..cb3ee06a9e 100644 --- a/src/Avalonia.Controls/Calendar/CalendarExtensions.cs +++ b/src/Avalonia.Controls/Calendar/CalendarExtensions.cs @@ -1,6 +1,6 @@ // (c) Copyright Microsoft Corporation. // This source is subject to the Microsoft Public License (Ms-PL). -// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// Please see https://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. using Avalonia.Input; diff --git a/src/Avalonia.Controls/Calendar/CalendarItem.cs b/src/Avalonia.Controls/Calendar/CalendarItem.cs index e2eabb5f28..eec3bdc9f2 100644 --- a/src/Avalonia.Controls/Calendar/CalendarItem.cs +++ b/src/Avalonia.Controls/Calendar/CalendarItem.cs @@ -1,6 +1,6 @@ // (c) Copyright Microsoft Corporation. // This source is subject to the Microsoft Public License (Ms-PL). -// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// Please see https://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. using System; diff --git a/src/Avalonia.Controls/Calendar/DateTimeHelper.cs b/src/Avalonia.Controls/Calendar/DateTimeHelper.cs index 7a5c74a51b..bfff03a926 100644 --- a/src/Avalonia.Controls/Calendar/DateTimeHelper.cs +++ b/src/Avalonia.Controls/Calendar/DateTimeHelper.cs @@ -1,6 +1,6 @@ // (c) Copyright Microsoft Corporation. // This source is subject to the Microsoft Public License (Ms-PL). -// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// Please see https://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. using System; diff --git a/src/Avalonia.Controls/Calendar/SelectedDatesCollection.cs b/src/Avalonia.Controls/Calendar/SelectedDatesCollection.cs index f4bc2528ba..211b5edb0d 100644 --- a/src/Avalonia.Controls/Calendar/SelectedDatesCollection.cs +++ b/src/Avalonia.Controls/Calendar/SelectedDatesCollection.cs @@ -1,6 +1,6 @@ // (c) Copyright Microsoft Corporation. // This source is subject to the Microsoft Public License (Ms-PL). -// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// Please see https://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. using Avalonia.Threading; diff --git a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs index 3d592e9ab5..ec1273ca98 100644 --- a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs +++ b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePicker.cs @@ -1,6 +1,6 @@ // (c) Copyright Microsoft Corporation. // This source is subject to the Microsoft Public License (Ms-PL). -// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// Please see https://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. using System; diff --git a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePickerDateValidationErrorEventArgs.cs b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePickerDateValidationErrorEventArgs.cs index 647910cb6b..b58b549030 100644 --- a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePickerDateValidationErrorEventArgs.cs +++ b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePickerDateValidationErrorEventArgs.cs @@ -1,6 +1,6 @@ // (c) Copyright Microsoft Corporation. // This source is subject to the Microsoft Public License (Ms-PL). -// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// Please see https://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. using System; diff --git a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePickerFormat.cs b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePickerFormat.cs index 4d96859d75..ffd1f6f594 100644 --- a/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePickerFormat.cs +++ b/src/Avalonia.Controls/CalendarDatePicker/CalendarDatePickerFormat.cs @@ -1,6 +1,6 @@ // (c) Copyright Microsoft Corporation. // This source is subject to the Microsoft Public License (Ms-PL). -// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// Please see https://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. namespace Avalonia.Controls diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs index d8de813d47..615eb69fe3 100644 --- a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs @@ -35,7 +35,7 @@ DEALINGS IN THE SOFTWARE. The above is the version of the MIT "Expat" License used by X.org: - http://cgit.freedesktop.org/xorg/xserver/tree/COPYING + https://cgit.freedesktop.org/xorg/xserver/tree/COPYING Adjustments for Avalonia needs: diff --git a/src/Avalonia.Controls/Utils/ISelectionAdapter.cs b/src/Avalonia.Controls/Utils/ISelectionAdapter.cs index c5fb12197f..3ede518ffa 100644 --- a/src/Avalonia.Controls/Utils/ISelectionAdapter.cs +++ b/src/Avalonia.Controls/Utils/ISelectionAdapter.cs @@ -1,6 +1,6 @@ // (c) Copyright Microsoft Corporation. // This source is subject to the Microsoft Public License (Ms-PL). -// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// Please see https://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. using System; diff --git a/src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs b/src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs index 0288f99dce..3c1b1262ae 100644 --- a/src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs +++ b/src/Avalonia.Controls/Utils/SelectingItemsControlSelectionAdapter.cs @@ -1,6 +1,6 @@ // (c) Copyright Microsoft Corporation. // This source is subject to the Microsoft Public License (Ms-PL). -// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// Please see https://go.microsoft.com/fwlink/?LinkID=131993 for details. // All other rights reserved. using System; diff --git a/src/Avalonia.Native/IconLoader.cs b/src/Avalonia.Native/IconLoader.cs index edb8b94e83..04779a43aa 100644 --- a/src/Avalonia.Native/IconLoader.cs +++ b/src/Avalonia.Native/IconLoader.cs @@ -6,7 +6,7 @@ namespace Avalonia.Native // OSX doesn't have a concept of *window* icon. // Icons in the title bar are only shown if there is // an opened file (on disk) associated with the current window - // see http://stackoverflow.com/a/7038671/2231814 + // see https://stackoverflow.com/a/7038671/2231814 class IconLoader : IPlatformIconLoader { class IconStub : IWindowIconImpl diff --git a/src/Avalonia.Themes.Fluent/Controls/Calendar.xaml b/src/Avalonia.Themes.Fluent/Controls/Calendar.xaml index 7042f51c71..9c66ea9b84 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Calendar.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Calendar.xaml @@ -1,7 +1,7 @@  diff --git a/src/Avalonia.Themes.Fluent/Controls/CalendarButton.xaml b/src/Avalonia.Themes.Fluent/Controls/CalendarButton.xaml index d1aee7ee9a..06b6cf30c2 100644 --- a/src/Avalonia.Themes.Fluent/Controls/CalendarButton.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/CalendarButton.xaml @@ -1,7 +1,7 @@  diff --git a/src/Avalonia.Themes.Fluent/Controls/DateTimePickerShared.xaml b/src/Avalonia.Themes.Fluent/Controls/DateTimePickerShared.xaml index be664b375d..7500ac7bca 100644 --- a/src/Avalonia.Themes.Fluent/Controls/DateTimePickerShared.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/DateTimePickerShared.xaml @@ -1,7 +1,7 @@ diff --git a/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml b/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml index fcd661a4b5..caf1251d25 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml @@ -1,7 +1,7 @@  diff --git a/src/Avalonia.Themes.Simple/Controls/CalendarItem.xaml b/src/Avalonia.Themes.Simple/Controls/CalendarItem.xaml index d9acd0d25a..2a9ae7cf8d 100644 --- a/src/Avalonia.Themes.Simple/Controls/CalendarItem.xaml +++ b/src/Avalonia.Themes.Simple/Controls/CalendarItem.xaml @@ -1,7 +1,7 @@ diff --git a/src/Avalonia.Themes.Simple/Controls/DateTimePickerShared.xaml b/src/Avalonia.Themes.Simple/Controls/DateTimePickerShared.xaml index 5909a1abbf..8639a2baa2 100644 --- a/src/Avalonia.Themes.Simple/Controls/DateTimePickerShared.xaml +++ b/src/Avalonia.Themes.Simple/Controls/DateTimePickerShared.xaml @@ -1,7 +1,7 @@ diff --git a/src/Avalonia.X11/X11Atoms.cs b/src/Avalonia.X11/X11Atoms.cs index 424db94e0a..b00879bd1d 100644 --- a/src/Avalonia.X11/X11Atoms.cs +++ b/src/Avalonia.X11/X11Atoms.cs @@ -17,7 +17,7 @@ // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // -// Copyright (c) 2006 Novell, Inc. (http://www.novell.com) +// Copyright (c) 2006 Novell, Inc. (https://www.novell.com) // // diff --git a/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/CaretHelper.ts b/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/CaretHelper.ts index 5709854087..60fd5a284e 100644 --- a/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/CaretHelper.ts +++ b/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/CaretHelper.ts @@ -75,7 +75,7 @@ export class CaretHelper { div.textContent = element.value.substring(0, position); // The second special handling for input type="text" vs textarea: - // spaces need to be replaced with non-breaking spaces - http://stackoverflow.com/a/13402035/1269037 + // spaces need to be replaced with non-breaking spaces - https://stackoverflow.com/a/13402035/1269037 if (isInput) div.textContent = div.textContent.replace(/\s/g, "\u00a0"); const span = document.createElement("span"); From 4da0ae8dab4f844a9d7a4ddd6748c55aa6cee09e Mon Sep 17 00:00:00 2001 From: Arhell Date: Wed, 5 Oct 2022 01:10:26 +0300 Subject: [PATCH 065/169] update test folder links --- NOTICE.md | 4 ++-- .../Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs | 2 +- tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/NOTICE.md b/NOTICE.md index 92fd725957..e97fc654c9 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -111,7 +111,7 @@ DEALINGS IN THE SOFTWARE. # Metsys.Bson -Copyright (c) 2010, Karl Seguin - http://www.openmymind.net/ +Copyright (c) 2010, Karl Seguin - https://www.openmymind.net/ All rights reserved. Redistribution and use in source and binary forms, with or without @@ -302,4 +302,4 @@ https://github.com/chromium/chromium // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs index 1f0b82b465..26a1ab88c7 100644 --- a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs +++ b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs @@ -235,7 +235,7 @@ namespace Avalonia.Base.UnitTests.VisualTree public bool FillContains(Point point) { - // Use the algorithm from http://www.blackpawn.com/texts/pointinpoly/default.html + // Use the algorithm from https://www.blackpawn.com/texts/pointinpoly/default.html // to determine if the point is in the geometry (since it will always be convex in this situation) for (int i = 0; i < points.Count; i++) { diff --git a/tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs b/tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs index 864e2efbaf..9d039a386e 100644 --- a/tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs +++ b/tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs @@ -148,7 +148,7 @@ namespace Avalonia.UnitTests public bool FillContains(Point point) { - // Use the algorithm from http://www.blackpawn.com/texts/pointinpoly/default.html + // Use the algorithm from https://www.blackpawn.com/texts/pointinpoly/default.html // to determine if the point is in the geometry (since it will always be convex in this situation) for (int i = 0; i < points.Count; i++) { From 240b644650045d4e572f291451b6731a25f91e85 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 5 Oct 2022 16:01:29 +0200 Subject: [PATCH 066/169] Next try --- .gitmodules | 3 + .nuke | 0 .nuke/build.schema.json | 148 ++++++++++++++++++ .nuke/parameters.json | 4 + azure-pipelines-integrationtests.yml | 4 +- azure-pipelines.yml | 36 +---- build.cmd | 7 + build.ps1 | 50 +++--- build.sh | 60 +++++-- dirs.proj | 2 +- global.json | 2 +- nukebuild/Build.cs | 4 +- nukebuild/BuildParameters.cs | 4 +- nukebuild/DotNetConfigHelper.cs | 4 +- nukebuild/_build.csproj | 33 ++-- nukebuild/il-repack | 1 + .../Avalonia.Build.Tasks.csproj | 4 +- 17 files changed, 270 insertions(+), 96 deletions(-) delete mode 100644 .nuke create mode 100644 .nuke/build.schema.json create mode 100644 .nuke/parameters.json create mode 100644 build.cmd create mode 160000 nukebuild/il-repack diff --git a/.gitmodules b/.gitmodules index 2d11fdfa9e..032bc879cc 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github"] path = src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github url = https://github.com/kekekeks/XamlX.git +[submodule "nukebuild/il-repack"] + path = nukebuild/il-repack + url = https://github.com/Gillibald/il-repack diff --git a/.nuke b/.nuke deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json new file mode 100644 index 0000000000..5bbc3d6915 --- /dev/null +++ b/.nuke/build.schema.json @@ -0,0 +1,148 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Build Schema", + "$ref": "#/definitions/build", + "definitions": { + "build": { + "type": "object", + "properties": { + "Configuration": { + "type": "string", + "description": "configuration" + }, + "Continue": { + "type": "boolean", + "description": "Indicates to continue a previously failed build attempt" + }, + "ForceNugetVersion": { + "type": "string", + "description": "force-nuget-version" + }, + "Help": { + "type": "boolean", + "description": "Shows the help text for this build assembly" + }, + "Host": { + "type": "string", + "description": "Host for execution. Default is 'automatic'", + "enum": [ + "AppVeyor", + "AzurePipelines", + "Bamboo", + "Bitbucket", + "Bitrise", + "GitHubActions", + "GitLab", + "Jenkins", + "Rider", + "SpaceAutomation", + "TeamCity", + "Terminal", + "TravisCI", + "VisualStudio", + "VSCode" + ] + }, + "NoLogo": { + "type": "boolean", + "description": "Disables displaying the NUKE logo" + }, + "Partition": { + "type": "string", + "description": "Partition to use on CI" + }, + "Plan": { + "type": "boolean", + "description": "Shows the execution plan (HTML)" + }, + "Profile": { + "type": "array", + "description": "Defines the profiles to load", + "items": { + "type": "string" + } + }, + "Root": { + "type": "string", + "description": "Root directory during build execution" + }, + "Skip": { + "type": "array", + "description": "List of targets to be skipped. Empty list skips all dependencies", + "items": { + "type": "string", + "enum": [ + "CiAzureLinux", + "CiAzureOSX", + "CiAzureWindows", + "Clean", + "Compile", + "CompileHtmlPreviewer", + "CompileNative", + "CreateIntermediateNugetPackages", + "CreateNugetPackages", + "GenerateCppHeaders", + "Package", + "RunCoreLibsTests", + "RunDesignerTests", + "RunHtmlPreviewerTests", + "RunLeakTests", + "RunRenderTests", + "RunTests", + "ZipFiles" + ] + } + }, + "SkipPreviewer": { + "type": "boolean", + "description": "skip-previewer" + }, + "SkipTests": { + "type": "boolean", + "description": "skip-tests" + }, + "Solution": { + "type": "string", + "description": "Path to a solution file that is automatically loaded. Default is Avalonia.sln" + }, + "Target": { + "type": "array", + "description": "List of targets to be invoked. Default is '{default_target}'", + "items": { + "type": "string", + "enum": [ + "CiAzureLinux", + "CiAzureOSX", + "CiAzureWindows", + "Clean", + "Compile", + "CompileHtmlPreviewer", + "CompileNative", + "CreateIntermediateNugetPackages", + "CreateNugetPackages", + "GenerateCppHeaders", + "Package", + "RunCoreLibsTests", + "RunDesignerTests", + "RunHtmlPreviewerTests", + "RunLeakTests", + "RunRenderTests", + "RunTests", + "ZipFiles" + ] + } + }, + "Verbosity": { + "type": "string", + "description": "Logging verbosity during build execution. Default is 'Normal'", + "enum": [ + "Minimal", + "Normal", + "Quiet", + "Verbose" + ] + } + } + } + } +} \ No newline at end of file diff --git a/.nuke/parameters.json b/.nuke/parameters.json new file mode 100644 index 0000000000..42521bb7dd --- /dev/null +++ b/.nuke/parameters.json @@ -0,0 +1,4 @@ +{ + "$schema": "./build.schema.json", + "Solution": "" +} \ No newline at end of file diff --git a/azure-pipelines-integrationtests.yml b/azure-pipelines-integrationtests.yml index 0b79758c76..99983b1758 100644 --- a/azure-pipelines-integrationtests.yml +++ b/azure-pipelines-integrationtests.yml @@ -41,9 +41,9 @@ jobs: steps: - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 6.0.202' + displayName: 'Use .NET Core SDK 6.0.401' inputs: - version: 6.0.202 + version: 6.0.401 - task: Windows Application Driver@0 inputs: diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 52fc8db53c..ba351b91a3 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -31,14 +31,9 @@ jobs: vmImage: 'ubuntu-20.04' steps: - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 3.1.418' + displayName: 'Use .NET Core SDK 6.0.401' inputs: - version: 3.1.418 - - - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 6.0.202' - inputs: - version: 6.0.202 + version: 6.0.401 - task: CmdLine@2 displayName: 'Run Build' @@ -62,22 +57,10 @@ jobs: vmImage: 'macos-12' steps: - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 3.1.418' + displayName: 'Use .NET Core SDK 6.0.401' inputs: - version: 3.1.418 - - - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 6.0.202' - inputs: - version: 6.0.202 + version: 6.0.401 - - task: CmdLine@2 - displayName: 'Install Mono 5.18' - inputs: - script: | - curl -o ./mono.pkg https://download.mono-project.com/archive/5.18.0/macos-10-universal/MonoFramework-MDK-5.18.0.225.macos10.xamarin.universal.pkg - sudo installer -verbose -pkg ./mono.pkg -target / - - task: CmdLine@2 displayName: 'Generate avalonia-native' inputs: @@ -134,14 +117,9 @@ jobs: SolutionDir: '$(Build.SourcesDirectory)' steps: - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 3.1.418' - inputs: - version: 3.1.418 - - - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 6.0.202' + displayName: 'Use .NET Core SDK 6.0.401' inputs: - version: 6.0.202 + version: 6.0.401 - task: CmdLine@2 displayName: 'Install Workloads' @@ -153,7 +131,7 @@ jobs: displayName: 'Install Nuke' inputs: script: | - dotnet tool install --global Nuke.GlobalTool --version 0.24.0 + dotnet tool install --global Nuke.GlobalTool --version 6.2.1 - task: CmdLine@2 displayName: 'Run Nuke' diff --git a/build.cmd b/build.cmd new file mode 100644 index 0000000000..b08cc590f4 --- /dev/null +++ b/build.cmd @@ -0,0 +1,7 @@ +:; set -eo pipefail +:; SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) +:; ${SCRIPT_DIR}/build.sh "$@" +:; exit $? + +@ECHO OFF +powershell -ExecutionPolicy ByPass -NoProfile -File "%~dp0build.ps1" %* diff --git a/build.ps1 b/build.ps1 index 985e8abcee..997e5b423f 100644 --- a/build.ps1 +++ b/build.ps1 @@ -1,13 +1,12 @@ [CmdletBinding()] Param( - #[switch]$CustomParam, [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)] [string[]]$BuildArguments ) -Write-Output "Windows PowerShell $($Host.Version)" +Write-Output "PowerShell $($PSVersionTable.PSEdition) version $($PSVersionTable.PSVersion)" -Set-StrictMode -Version 2.0; $ErrorActionPreference = "Stop"; $ConfirmPreference = "None"; trap { exit 1 } +Set-StrictMode -Version 2.0; $ErrorActionPreference = "Stop"; $ConfirmPreference = "None"; trap { Write-Error $_ -ErrorAction Continue; exit 1 } $PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent ########################################################################### @@ -15,15 +14,15 @@ $PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent ########################################################################### $BuildProjectFile = "$PSScriptRoot\nukebuild\_build.csproj" -$TempDirectory = "$PSScriptRoot\\.tmp" +$TempDirectory = "$PSScriptRoot\\.nuke\temp" $DotNetGlobalFile = "$PSScriptRoot\\global.json" -$DotNetInstallUrl = "https://raw.githubusercontent.com/dotnet/cli/master/scripts/obtain/dotnet-install.ps1" +$DotNetInstallUrl = "https://dot.net/v1/dotnet-install.ps1" $DotNetChannel = "Current" $env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = 1 $env:DOTNET_CLI_TELEMETRY_OPTOUT = 1 -$env:NUGET_XMLDOC_MODE = "skip" +$env:DOTNET_MULTILEVEL_LOOKUP = 0 ########################################################################### # EXECUTION @@ -34,38 +33,37 @@ function ExecSafe([scriptblock] $cmd) { if ($LASTEXITCODE) { exit $LASTEXITCODE } } -# If global.json exists, load expected version -if (Test-Path $DotNetGlobalFile) { - $DotNetGlobal = $(Get-Content $DotNetGlobalFile | Out-String | ConvertFrom-Json) - if ($DotNetGlobal.PSObject.Properties["sdk"] -and $DotNetGlobal.sdk.PSObject.Properties["version"]) { - $DotNetVersion = $DotNetGlobal.sdk.version - } -} - -# If dotnet is installed locally, and expected version is not set or installation matches the expected version +# If dotnet CLI is installed globally and it matches requested version, use for execution if ($null -ne (Get-Command "dotnet" -ErrorAction SilentlyContinue) -and ` - (!(Test-Path variable:DotNetVersion) -or $(& dotnet --version) -eq $DotNetVersion)) { + $(dotnet --version) -and $LASTEXITCODE -eq 0) { $env:DOTNET_EXE = (Get-Command "dotnet").Path } else { - $DotNetDirectory = "$TempDirectory\dotnet-win" - $env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe" - # Download install script $DotNetInstallFile = "$TempDirectory\dotnet-install.ps1" - mkdir -force $TempDirectory > $null + New-Item -ItemType Directory -Path $TempDirectory -Force | Out-Null + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 (New-Object System.Net.WebClient).DownloadFile($DotNetInstallUrl, $DotNetInstallFile) + # If global.json exists, load expected version + if (Test-Path $DotNetGlobalFile) { + $DotNetGlobal = $(Get-Content $DotNetGlobalFile | Out-String | ConvertFrom-Json) + if ($DotNetGlobal.PSObject.Properties["sdk"] -and $DotNetGlobal.sdk.PSObject.Properties["version"]) { + $DotNetVersion = $DotNetGlobal.sdk.version + } + } + # Install by channel or version + $DotNetDirectory = "$TempDirectory\dotnet-win" if (!(Test-Path variable:DotNetVersion)) { - ExecSafe { & $DotNetInstallFile -InstallDir $DotNetDirectory -Channel $DotNetChannel -NoPath } + ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Channel $DotNetChannel -NoPath } } else { - ExecSafe { & $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath } + ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath } } - - $env:PATH="$DotNetDirectory;$env:PATH" + $env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe" } -Write-Output "Microsoft (R) .NET Core SDK version $(& $env:DOTNET_EXE --version)" +Write-Output "Microsoft (R) .NET SDK version $(& $env:DOTNET_EXE --version)" -ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile -- $BuildArguments } +ExecSafe { & $env:DOTNET_EXE build $BuildProjectFile /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet } +ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile --no-build -- $BuildArguments } diff --git a/build.sh b/build.sh index 9532b4fbe0..76919a5351 100755 --- a/build.sh +++ b/build.sh @@ -1,16 +1,6 @@ #!/usr/bin/env bash -echo $(bash --version 2>&1 | head -n 1) - -#CUSTOMPARAM=0 -BUILD_ARGUMENTS=() -for i in "$@"; do - case $(echo $1 | awk '{print tolower($0)}') in - # -custom-param) CUSTOMPARAM=1;; - *) BUILD_ARGUMENTS+=("$1") ;; - esac - shift -done +bash --version 2>&1 | head -n 1 set -eo pipefail SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) @@ -20,11 +10,53 @@ SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) ########################################################################### BUILD_PROJECT_FILE="$SCRIPT_DIR/nukebuild/_build.csproj" +TEMP_DIRECTORY="$SCRIPT_DIR//.nuke/temp" + +DOTNET_GLOBAL_FILE="$SCRIPT_DIR//global.json" +DOTNET_INSTALL_URL="https://dot.net/v1/dotnet-install.sh" +DOTNET_CHANNEL="Current" export DOTNET_CLI_TELEMETRY_OPTOUT=1 export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 -export NUGET_XMLDOC_MODE="skip" +export DOTNET_MULTILEVEL_LOOKUP=0 -dotnet --info +########################################################################### +# EXECUTION +########################################################################### -dotnet run --project "$BUILD_PROJECT_FILE" -- ${BUILD_ARGUMENTS[@]} +function FirstJsonValue { + perl -nle 'print $1 if m{"'"$1"'": "([^"]+)",?}' <<< "${@:2}" +} + +# If dotnet CLI is installed globally and it matches requested version, use for execution +if [ -x "$(command -v dotnet)" ] && dotnet --version &>/dev/null; then + export DOTNET_EXE="$(command -v dotnet)" +else + # Download install script + DOTNET_INSTALL_FILE="$TEMP_DIRECTORY/dotnet-install.sh" + mkdir -p "$TEMP_DIRECTORY" + curl -Lsfo "$DOTNET_INSTALL_FILE" "$DOTNET_INSTALL_URL" + chmod +x "$DOTNET_INSTALL_FILE" + + # If global.json exists, load expected version + if [[ -f "$DOTNET_GLOBAL_FILE" ]]; then + DOTNET_VERSION=$(FirstJsonValue "version" "$(cat "$DOTNET_GLOBAL_FILE")") + if [[ "$DOTNET_VERSION" == "" ]]; then + unset DOTNET_VERSION + fi + fi + + # Install by channel or version + DOTNET_DIRECTORY="$TEMP_DIRECTORY/dotnet-unix" + if [[ -z ${DOTNET_VERSION+x} ]]; then + "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --channel "$DOTNET_CHANNEL" --no-path + else + "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version "$DOTNET_VERSION" --no-path + fi + export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet" +fi + +echo "Microsoft (R) .NET SDK version $("$DOTNET_EXE" --version)" + +"$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet +"$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@" diff --git a/dirs.proj b/dirs.proj index 47ad0dfd55..bbed92a3cc 100644 --- a/dirs.proj +++ b/dirs.proj @@ -29,6 +29,6 @@ - + diff --git a/global.json b/global.json index a6792b05c7..7c2daee25f 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "6.0.202", + "version": "6.0.401", "rollForward": "latestFeature" }, "msbuild-sdks": { diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index 4bbb667154..65d7817dba 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -163,7 +163,7 @@ partial class Build : NukeBuild .EnableNoBuild() .EnableNoRestore() .When(Parameters.PublishTestResults, _ => _ - .SetLogger("trx") + .SetLoggers("trx") .SetResultsDirectory(Parameters.TestResultsRoot))); } } @@ -229,7 +229,7 @@ partial class Build : NukeBuild $"{XunitPath.DoubleQuoteIfNeeded()} --propagate-exit-code -- {testAssembly}", timeout: 120_000); } - ControlFlow.ExecuteWithRetry(DoMemoryTest, waitInSeconds: 3); + ControlFlow.ExecuteWithRetry(DoMemoryTest, delay: TimeSpan.FromMilliseconds(3)); }); Target ZipFiles => _ => _ diff --git a/nukebuild/BuildParameters.cs b/nukebuild/BuildParameters.cs index 1826623674..dfa914d1db 100644 --- a/nukebuild/BuildParameters.cs +++ b/nukebuild/BuildParameters.cs @@ -74,11 +74,11 @@ public partial class Build MSBuildSolution = RootDirectory / "dirs.proj"; // PARAMETERS - IsLocalBuild = Host == HostType.Console; + IsLocalBuild = NukeBuild.IsLocalBuild; IsRunningOnUnix = Environment.OSVersion.Platform == PlatformID.Unix || Environment.OSVersion.Platform == PlatformID.MacOSX; IsRunningOnWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - IsRunningOnAzure = Host == HostType.AzurePipelines || + IsRunningOnAzure = Host is AzurePipelines || Environment.GetEnvironmentVariable("LOGNAME") == "vsts"; if (IsRunningOnAzure) diff --git a/nukebuild/DotNetConfigHelper.cs b/nukebuild/DotNetConfigHelper.cs index 932525288c..eca1e2684d 100644 --- a/nukebuild/DotNetConfigHelper.cs +++ b/nukebuild/DotNetConfigHelper.cs @@ -46,7 +46,7 @@ public class DotNetConfigHelper public DotNetConfigHelper SetVerbosity(DotNetVerbosity verbosity) { Build = Build?.SetVerbosity(verbosity); - Pack = Pack?.SetVerbostiy(verbosity); + Pack = Pack?.SetVerbosity(verbosity); Test = Test?.SetVerbosity(verbosity); return this; } @@ -54,4 +54,4 @@ public class DotNetConfigHelper public static implicit operator DotNetConfigHelper(DotNetBuildSettings s) => new DotNetConfigHelper(s); public static implicit operator DotNetConfigHelper(DotNetPackSettings s) => new DotNetConfigHelper(s); public static implicit operator DotNetConfigHelper(DotNetTestSettings s) => new DotNetConfigHelper(s); -} \ No newline at end of file +} diff --git a/nukebuild/_build.csproj b/nukebuild/_build.csproj index b2c58e2292..6fa901d3cb 100644 --- a/nukebuild/_build.csproj +++ b/nukebuild/_build.csproj @@ -2,40 +2,43 @@ Exe - netcoreapp3.1 + net6.0 false False CS0649;CS0169 + 1 - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - - + + + - - - - - - - - - - + + + + + + + + diff --git a/nukebuild/il-repack b/nukebuild/il-repack new file mode 160000 index 0000000000..892f079ea8 --- /dev/null +++ b/nukebuild/il-repack @@ -0,0 +1 @@ +Subproject commit 892f079ea8cb0c178f0a68f53a7a7eac13acdda9 diff --git a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj index 8387c62cad..c51a98e1c0 100644 --- a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj +++ b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj @@ -105,8 +105,8 @@ - - + + From 97ca9e3fe5f7b73905302a6ff9dbef25be34a8d5 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 5 Oct 2022 16:01:40 +0200 Subject: [PATCH 067/169] One more --- dirs.proj | 5 +---- nukebuild/Build.cs | 2 +- nukebuild/Shims.cs | 15 +++++++++++++++ nukebuild/_build.csproj | 12 ++++++------ 4 files changed, 23 insertions(+), 11 deletions(-) diff --git a/dirs.proj b/dirs.proj index bbed92a3cc..2dc5e7793c 100644 --- a/dirs.proj +++ b/dirs.proj @@ -23,10 +23,7 @@ - - - - + diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index 65d7817dba..f965080add 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -225,7 +225,7 @@ partial class Build : NukeBuild void DoMemoryTest() { var testAssembly = "tests\\Avalonia.LeakTests\\bin\\Release\\net461\\Avalonia.LeakTests.dll"; - DotMemoryUnit( + DotMemoryUnit( $"{XunitPath.DoubleQuoteIfNeeded()} --propagate-exit-code -- {testAssembly}", timeout: 120_000); } diff --git a/nukebuild/Shims.cs b/nukebuild/Shims.cs index 1ac14bf622..6f79972ad6 100644 --- a/nukebuild/Shims.cs +++ b/nukebuild/Shims.cs @@ -49,7 +49,11 @@ public partial class Build { if (fsEntry is FileInfo) { +#if NET6 var relPath = Path.GetRelativePath(rootPath, fsEntry.FullName); +#else + var relPath = GetRelativePath(rootPath, fsEntry.FullName); +#endif AddFile(fsEntry.FullName, relPath); } } @@ -78,6 +82,17 @@ public partial class Build } } + private static string GetRelativePath(string relativeTo, string path) + { + var uri = new Uri(relativeTo); + var rel = Uri.UnescapeDataString(uri.MakeRelativeUri(new Uri(path)).ToString()).Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); + if (rel.Contains(Path.DirectorySeparatorChar.ToString()) == false) + { + rel = $".{Path.DirectorySeparatorChar}{rel}"; + } + return rel; + } + class NumergeNukeLogger : INumergeLogger { public void Log(NumergeLogLevel level, string message) diff --git a/nukebuild/_build.csproj b/nukebuild/_build.csproj index 6fa901d3cb..5db9579876 100644 --- a/nukebuild/_build.csproj +++ b/nukebuild/_build.csproj @@ -1,27 +1,27 @@  - Exe - net6.0 false False CS0649;CS0169 1 + net6.0 + - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + From 32a4ddbcefefe50bd96067cf44078d25f1391412 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 5 Oct 2022 16:04:43 +0200 Subject: [PATCH 068/169] Build mobile --- dirs.proj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dirs.proj b/dirs.proj index 2dc5e7793c..a2544ef951 100644 --- a/dirs.proj +++ b/dirs.proj @@ -24,6 +24,10 @@ + + + + From c3da4df93a357a1565d0ed60f0f0d43b3a918975 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 5 Oct 2022 16:56:21 +0200 Subject: [PATCH 069/169] Fix leaktests --- nukebuild/Build.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index f965080add..358549db55 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -215,8 +215,6 @@ partial class Build : NukeBuild RunCoreTest("Avalonia.DesignerSupport.Tests"); }); - [PackageExecutable("JetBrains.dotMemoryUnit", "dotMemoryUnit.exe")] readonly Tool DotMemoryUnit; - Target RunLeakTests => _ => _ .OnlyWhenStatic(() => !Parameters.SkipTests && Parameters.IsRunningOnWindows) .DependsOn(Compile) @@ -224,10 +222,7 @@ partial class Build : NukeBuild { void DoMemoryTest() { - var testAssembly = "tests\\Avalonia.LeakTests\\bin\\Release\\net461\\Avalonia.LeakTests.dll"; - DotMemoryUnit( - $"{XunitPath.DoubleQuoteIfNeeded()} --propagate-exit-code -- {testAssembly}", - timeout: 120_000); + RunCoreTest("Avalonia.LeakTests"); } ControlFlow.ExecuteWithRetry(DoMemoryTest, delay: TimeSpan.FromMilliseconds(3)); }); From 3b4b9682b223dd92d9c7dc0d7e38fad6aafbc758 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 5 Oct 2022 17:00:41 +0200 Subject: [PATCH 070/169] Use older msbuild --- src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj index c51a98e1c0..22c410198f 100644 --- a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj +++ b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj @@ -106,7 +106,7 @@ - + From 6be88c5dcbe5d5afb99b584d73e819f8ba867fd9 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 6 Oct 2022 02:38:46 +0100 Subject: [PATCH 071/169] use net 7 --- azure-pipelines.yml | 27 +++++++++++++++++++++++++++ nukebuild/_build.csproj | 2 +- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 8804bc02e1..33b2dc670a 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -34,6 +34,17 @@ jobs: inputs: version: 6.0.401 + - task: UseDotNet@2 + displayName: 'Use .NET Core SDK 7.0.100-rc.1.22431.12' + inputs: + version: 7.0.100-rc.1.22431.12 + + - task: CmdLine@2 + displayName: 'Install Workloads' + inputs: + script: | + dotnet workload install wasm-tools wasm-experimental + - task: CmdLine@2 displayName: 'Run Build' inputs: @@ -59,6 +70,17 @@ jobs: displayName: 'Use .NET Core SDK 6.0.401' inputs: version: 6.0.401 + + - task: UseDotNet@2 + displayName: 'Use .NET Core SDK 7.0.100-rc.1.22431.12' + inputs: + version: 7.0.100-rc.1.22431.12 + + - task: CmdLine@2 + displayName: 'Install Workloads' + inputs: + script: | + dotnet workload install wasm-tools wasm-experimental - task: CmdLine@2 displayName: 'Generate avalonia-native' @@ -120,6 +142,11 @@ jobs: inputs: version: 6.0.401 + - task: UseDotNet@2 + displayName: 'Use .NET Core SDK 7.0.100-rc.1.22431.12' + inputs: + version: 7.0.100-rc.1.22431.12 + - task: CmdLine@2 displayName: 'Install Workloads' inputs: diff --git a/nukebuild/_build.csproj b/nukebuild/_build.csproj index 5db9579876..0112697846 100644 --- a/nukebuild/_build.csproj +++ b/nukebuild/_build.csproj @@ -6,7 +6,7 @@ False CS0649;CS0169 1 - net6.0 + net7.0 From 3e4b5bb6155e32d23ceffff17bc3635f0b5f2d0c Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 6 Oct 2022 02:41:15 +0100 Subject: [PATCH 072/169] remove sdk restriction --- global.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/global.json b/global.json index 7c2daee25f..44d4e10dbf 100644 --- a/global.json +++ b/global.json @@ -1,8 +1,4 @@ { - "sdk": { - "version": "6.0.401", - "rollForward": "latestFeature" - }, "msbuild-sdks": { "Microsoft.Build.Traversal": "1.0.43", "MSBuild.Sdk.Extras": "3.0.22", From 7423441ee02555d41176cc8a8066b8307f7a1706 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 6 Oct 2022 02:47:11 +0100 Subject: [PATCH 073/169] turn of warning as errors for nuke build. --- nukebuild/_build.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/nukebuild/_build.csproj b/nukebuild/_build.csproj index 0112697846..401673030f 100644 --- a/nukebuild/_build.csproj +++ b/nukebuild/_build.csproj @@ -7,6 +7,7 @@ CS0649;CS0169 1 net7.0 + false From 6e6102e110fe6979ff9fc11d4bca2c78988be254 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 6 Oct 2022 02:50:11 +0100 Subject: [PATCH 074/169] build with net 6 --- nukebuild/_build.csproj | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/nukebuild/_build.csproj b/nukebuild/_build.csproj index 401673030f..5db9579876 100644 --- a/nukebuild/_build.csproj +++ b/nukebuild/_build.csproj @@ -6,8 +6,7 @@ False CS0649;CS0169 1 - net7.0 - false + net6.0 From 79cc37e372789cc4f59a016c9016caa169bbb5fb Mon Sep 17 00:00:00 2001 From: rabbitism Date: Mon, 3 Oct 2022 23:05:36 +0800 Subject: [PATCH 075/169] feat: add :dropdownopen and :pressed pseudoclass for combobox. simplify fluent theme combobox default template. update unit test. --- src/Avalonia.Controls/ComboBox.cs | 28 ++++++++++++++++++- .../Controls/ComboBox.xaml | 19 +------------ .../ComboBoxTests.cs | 25 +++++++++++++++++ 3 files changed, 53 insertions(+), 19 deletions(-) diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index 05be5ad00d..54196bdf1a 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -21,8 +21,11 @@ namespace Avalonia.Controls /// A drop-down list control. /// [TemplatePart("PART_Popup", typeof(Popup))] + [PseudoClasses(pcDropdownOpen, pcPressed)] public class ComboBox : SelectingItemsControl { + public const string pcDropdownOpen = ":dropdownopen"; + public const string pcPressed = ":pressed"; /// /// The default value for the property. /// @@ -95,6 +98,7 @@ namespace Avalonia.Controls SelectedItemProperty.Changed.AddClassHandler((x, e) => x.SelectedItemChanged(e)); KeyDownEvent.AddClassHandler((x, e) => x.OnKeyDown(e), Interactivity.RoutingStrategies.Tunnel); IsTextSearchEnabledProperty.OverrideDefaultValue(true); + IsDropDownOpenProperty.Changed.AddClassHandler((x, e) => x.DropdownChanged(e)); } /// @@ -267,6 +271,20 @@ namespace Avalonia.Controls } } + /// + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + base.OnPointerPressed(e); + if(!e.Handled && e.Source is IVisual source) + { + if (_popup?.IsInsidePopup(source) == true) + { + return; + } + } + PseudoClasses.Set(pcPressed, true); + } + /// protected override void OnPointerReleased(PointerReleasedEventArgs e) { @@ -286,10 +304,12 @@ namespace Avalonia.Controls e.Handled = true; } } - + PseudoClasses.Set(pcPressed, false); base.OnPointerReleased(e); + } + /// protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { @@ -470,5 +490,11 @@ namespace Avalonia.Controls MoveSelection(NavigationDirection.Previous, WrapSelection); } } + + private void DropdownChanged(AvaloniaPropertyChangedEventArgs e) + { + bool newValue = e.GetNewValue(); + PseudoClasses.Set(pcDropdownOpen, newValue); + } } } diff --git a/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml b/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml index af3b5bc51e..4aa0ddc23a 100644 --- a/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/ComboBox.xaml @@ -57,17 +57,8 @@ - - + - diff --git a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs index aa32af7e51..21438543d5 100644 --- a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs @@ -29,11 +29,36 @@ namespace Avalonia.Controls.UnitTests _helper.Down(target); _helper.Up(target); Assert.True(target.IsDropDownOpen); + Assert.True(target.Classes.Contains(ComboBox.pcDropdownOpen)); _helper.Down(target); _helper.Up(target); Assert.False(target.IsDropDownOpen); + Assert.True(!target.Classes.Contains(ComboBox.pcDropdownOpen)); + } + + [Fact] + public void Clicking_On_Control_PseudoClass() + { + var target = new ComboBox + { + Items = new[] { "Foo", "Bar" }, + }; + + _helper.Down(target); + Assert.True(target.Classes.Contains(ComboBox.pcPressed)); + _helper.Up(target); + Assert.True(!target.Classes.Contains(ComboBox.pcPressed)); + Assert.True(target.Classes.Contains(ComboBox.pcDropdownOpen)); + + _helper.Down(target); + Assert.True(target.Classes.Contains(ComboBox.pcPressed)); + _helper.Up(target); + Assert.True(!target.Classes.Contains(ComboBox.pcPressed)); + + Assert.False(target.IsDropDownOpen); + Assert.True(!target.Classes.Contains(ComboBox.pcDropdownOpen)); } [Fact] From 8aa64528e8663dc33bf4db0984d71559753b44a5 Mon Sep 17 00:00:00 2001 From: Stebner Date: Thu, 6 Oct 2022 00:49:48 -0700 Subject: [PATCH 076/169] Merge --- Avalonia.sln | 2 -- nukebuild/_build.csproj | 8 ++++++-- src/iOS/Avalonia.iOS/Avalonia.iOS.csproj | 6 ++++++ src/iOS/Avalonia.iOS/CombinedSpan3.cs | 8 ++++---- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/Avalonia.sln b/Avalonia.sln index c1760cd6b4..81a9b43890 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -411,9 +411,7 @@ Global {BF28998D-072C-439A-AFBB-2FE5021241E0}.Release|Any CPU.ActiveCfg = Release|Any CPU {BF28998D-072C-439A-AFBB-2FE5021241E0}.Release|Any CPU.Build.0 = Release|Any CPU {3F00BC43-5095-477F-93D8-E65B08179A00}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3F00BC43-5095-477F-93D8-E65B08179A00}.Debug|Any CPU.Build.0 = Debug|Any CPU {3F00BC43-5095-477F-93D8-E65B08179A00}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3F00BC43-5095-477F-93D8-E65B08179A00}.Release|Any CPU.Build.0 = Release|Any CPU {41B02319-965D-4945-8005-C1A3D1224165}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {41B02319-965D-4945-8005-C1A3D1224165}.Debug|Any CPU.Build.0 = Debug|Any CPU {41B02319-965D-4945-8005-C1A3D1224165}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/nukebuild/_build.csproj b/nukebuild/_build.csproj index 5db9579876..8c0d824298 100644 --- a/nukebuild/_build.csproj +++ b/nukebuild/_build.csproj @@ -4,9 +4,9 @@ false False - CS0649;CS0169 + CS0649;CS0169;SYSLIB0011 1 - net6.0 + net7.0 @@ -40,5 +40,9 @@ + + + + diff --git a/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj b/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj index e05566c454..99d4fd2a27 100644 --- a/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj +++ b/src/iOS/Avalonia.iOS/Avalonia.iOS.csproj @@ -4,6 +4,12 @@ 13.0 true + + + + + + diff --git a/src/iOS/Avalonia.iOS/CombinedSpan3.cs b/src/iOS/Avalonia.iOS/CombinedSpan3.cs index c73fb8de96..e9f44b7d58 100644 --- a/src/iOS/Avalonia.iOS/CombinedSpan3.cs +++ b/src/iOS/Avalonia.iOS/CombinedSpan3.cs @@ -16,7 +16,7 @@ internal ref struct CombinedSpan3 public int Length => Span1.Length + Span2.Length + Span3.Length; - void CopyFromSpan(ReadOnlySpan from, ref int offset, ref Span to) + void CopyFromSpan(ReadOnlySpan from, int offset, ref Span to) { if(to.Length == 0) return; @@ -33,8 +33,8 @@ internal ref struct CombinedSpan3 public void CopyTo(Span to, int offset) { - //CopyFromSpan(Span1, ref offset, ref to); - //CopyFromSpan(Span2, ref offset, ref to); - //CopyFromSpan(Span3, ref offset, ref to); + CopyFromSpan(Span1, offset, ref to); + CopyFromSpan(Span2, offset, ref to); + CopyFromSpan(Span3, offset, ref to); } } From 5abe64b8cfbca212e07f0bea81ba7a36488a3d72 Mon Sep 17 00:00:00 2001 From: Stebner Date: Thu, 6 Oct 2022 01:13:50 -0700 Subject: [PATCH 077/169] fix module import --- src/Web/Avalonia.Web/webapp/modules/avalonia.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia.ts index 6992921f5b..98c912f940 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia.ts @@ -4,7 +4,7 @@ import { InputHelper } from "./avalonia/input"; import { AvaloniaDOM } from "./avalonia/dom"; import { Caniuse } from "./avalonia/caniuse"; import { StreamHelper } from "./avalonia/stream"; -import { NativeControlHost } from "./avalonia/NativeControlHost"; +import { NativeControlHost } from "./avalonia/nativeControlHost"; export async function createAvaloniaRuntime(api: RuntimeAPI): Promise { api.setModuleImports("avalonia.ts", { From 397066663b96a176315aec22031d40f026398f0a Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Thu, 6 Oct 2022 14:47:03 +0200 Subject: [PATCH 078/169] feat(LogArea): Add missing Platform Added missing LogArea Platform: - LinuxFramebuffer - FreeDesktopPlatform - macOSPlatform --- src/Avalonia.Base/Logging/LogArea.cs | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/Logging/LogArea.cs b/src/Avalonia.Base/Logging/LogArea.cs index 98ef6d2530..50a5ead18b 100644 --- a/src/Avalonia.Base/Logging/LogArea.cs +++ b/src/Avalonia.Base/Logging/LogArea.cs @@ -36,23 +36,38 @@ namespace Avalonia.Logging public const string Control = "Control"; /// - /// The log event comes from Win32Platform. + /// The log event comes from Win32 Platform. /// public const string Win32Platform = nameof(Win32Platform); /// - /// The log event comes from X11Platform. + /// The log event comes from X11 Platform. /// public const string X11Platform = nameof(X11Platform); /// - /// The log event comes from AndroidPlatform. + /// The log event comes from Android Platform. /// public const string AndroidPlatform = nameof(AndroidPlatform); /// - /// The log event comes from IOSPlatform. + /// The log event comes from iOS Platform. /// public const string IOSPlatform = nameof(IOSPlatform); + + /// + /// The log event comes from LinuxFramebuffer Platform + /// + public const string LinuxFramebufferPlatform = nameof(LinuxFramebufferPlatform); + + /// + /// The log event comes from FreeDesktop Platform + /// + public const string FreeDesktopPlatform = nameof(FreeDesktopPlatform); + + /// + /// The log event comes from macOS Platform + /// + public const string macOSPlatform = nameof(macOSPlatform); } } From d92edc8d62b5bc1e82764a2abcbac1030ded90e6 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Thu, 6 Oct 2022 14:48:57 +0200 Subject: [PATCH 079/169] fix(LogArea): Replace string with nameof --- src/Avalonia.Base/Logging/LogArea.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Base/Logging/LogArea.cs b/src/Avalonia.Base/Logging/LogArea.cs index 50a5ead18b..972a9a1e9d 100644 --- a/src/Avalonia.Base/Logging/LogArea.cs +++ b/src/Avalonia.Base/Logging/LogArea.cs @@ -8,32 +8,32 @@ namespace Avalonia.Logging /// /// The log event comes from the property system. /// - public const string Property = "Property"; + public const string Property = nameof(Property); /// /// The log event comes from the binding system. /// - public const string Binding = "Binding"; + public const string Binding = nameof(Binding); /// /// The log event comes from the animations system. /// - public const string Animations = "Animations"; + public const string Animations = nameof(Animations); /// /// The log event comes from the visual system. /// - public const string Visual = "Visual"; + public const string Visual = nameof(Visual); /// /// The log event comes from the layout system. /// - public const string Layout = "Layout"; + public const string Layout = nameof(Layout); /// /// The log event comes from the control system. /// - public const string Control = "Control"; + public const string Control = nameof(Control); /// /// The log event comes from Win32 Platform. From e423d86b63b1efba2b9fe8d6a51a9cb0ab0e6544 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 6 Oct 2022 14:05:36 +0100 Subject: [PATCH 080/169] deploy interop.js and remove emscripten clib. --- src/Web/Avalonia.Web/Avalonia.Web.csproj | 4 +++ src/Web/Avalonia.Web/Avalonia.Web.targets | 1 - src/Web/Avalonia.Web/Interop/Emscripten.cs | 31 -------------------- src/Web/Avalonia.Web/Interop/libEmscripten.c | 9 ------ 4 files changed, 4 insertions(+), 41 deletions(-) delete mode 100644 src/Web/Avalonia.Web/Interop/Emscripten.cs delete mode 100644 src/Web/Avalonia.Web/Interop/libEmscripten.c diff --git a/src/Web/Avalonia.Web/Avalonia.Web.csproj b/src/Web/Avalonia.Web/Avalonia.Web.csproj index 04b31b1a2f..a9752762ac 100644 --- a/src/Web/Avalonia.Web/Avalonia.Web.csproj +++ b/src/Web/Avalonia.Web/Avalonia.Web.csproj @@ -29,6 +29,10 @@ true build\;buildTransitive\ + + true + buildTransitive/net7.0/interop.js + diff --git a/src/Web/Avalonia.Web/Avalonia.Web.targets b/src/Web/Avalonia.Web/Avalonia.Web.targets index 537d746ab9..d1bec2aa93 100644 --- a/src/Web/Avalonia.Web/Avalonia.Web.targets +++ b/src/Web/Avalonia.Web/Avalonia.Web.targets @@ -1,6 +1,5 @@ - diff --git a/src/Web/Avalonia.Web/Interop/Emscripten.cs b/src/Web/Avalonia.Web/Interop/Emscripten.cs deleted file mode 100644 index bfb42fa356..0000000000 --- a/src/Web/Avalonia.Web/Interop/Emscripten.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using System.Runtime.InteropServices; - -namespace Avalonia.Web -{ - public static partial class Emscripten - { - const string Prefix = "av_"; - const string Library = "libEmscripten"; - - [LibraryImport(Library, EntryPoint = Prefix + "log", StringMarshalling = StringMarshalling.Utf8)] - public static partial void Log(EM_LOG flags, string format); - - [LibraryImport(Library, EntryPoint = Prefix + "debugger")] - public static partial void Debugger(); - } - - [Flags] - public enum EM_LOG : int - { - CONSOLE = 1, - WARN = 2, - ERROR = 4, - C_STACK = 8, - JS_STACK = 16, - NO_PATHS = 64, - FUNC_PARAMS = 128, - DEBUG = 256, - INFO = 512, - } -} diff --git a/src/Web/Avalonia.Web/Interop/libEmscripten.c b/src/Web/Avalonia.Web/Interop/libEmscripten.c deleted file mode 100644 index 708be1abc8..0000000000 --- a/src/Web/Avalonia.Web/Interop/libEmscripten.c +++ /dev/null @@ -1,9 +0,0 @@ -#include - -void av_log(int flags, const char* format) { - emscripten_log(flags, format); -} - -void av_debugger() { - emscripten_debugger(); -} From 1ae178f52fee5638bed71d989368e5db63199e1d Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 6 Oct 2022 14:15:17 +0100 Subject: [PATCH 081/169] package interop.js in correct location. --- src/Web/Avalonia.Web/Avalonia.Web.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Web/Avalonia.Web/Avalonia.Web.csproj b/src/Web/Avalonia.Web/Avalonia.Web.csproj index a9752762ac..c927a4998b 100644 --- a/src/Web/Avalonia.Web/Avalonia.Web.csproj +++ b/src/Web/Avalonia.Web/Avalonia.Web.csproj @@ -31,7 +31,7 @@ true - buildTransitive/net7.0/interop.js + build/net7.0/interop.js From 1a994036bfe005e1106e5c4e76d0427244eb3ade Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 6 Oct 2022 16:23:43 +0100 Subject: [PATCH 082/169] fix avalonia.web package. --- src/Web/Avalonia.Web.Sample/Program.cs | 4 ---- src/Web/Avalonia.Web/Avalonia.Web.csproj | 6 +++++- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Web/Avalonia.Web.Sample/Program.cs b/src/Web/Avalonia.Web.Sample/Program.cs index 464c9925dd..52acabb0fa 100644 --- a/src/Web/Avalonia.Web.Sample/Program.cs +++ b/src/Web/Avalonia.Web.Sample/Program.cs @@ -7,15 +7,11 @@ internal partial class Program { private static void Main(string[] args) { - Emscripten.Log(EM_LOG.INFO, "Call from Main"); - BuildAvaloniaApp() .AfterSetup(_ => { ControlCatalog.Pages.EmbedSample.Implementation = new EmbedSampleWeb(); }).SetupBrowserApp("out"); - - BuildAvaloniaApp().SetupBrowserApp("out"); } public static AppBuilder BuildAvaloniaApp() diff --git a/src/Web/Avalonia.Web/Avalonia.Web.csproj b/src/Web/Avalonia.Web/Avalonia.Web.csproj index c927a4998b..a4756a5e2b 100644 --- a/src/Web/Avalonia.Web/Avalonia.Web.csproj +++ b/src/Web/Avalonia.Web/Avalonia.Web.csproj @@ -31,7 +31,11 @@ true - build/net7.0/interop.js + build/interop.js;buildTransitive/interop.js + + + true + build\wwwroot;buildTransitive\wwwroot From 9714462521930e6846679de3dfbbe53ea4738ad2 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 8 Oct 2022 12:16:00 +0100 Subject: [PATCH 083/169] try fix nuke --- nukebuild/Build.cs | 9 +++++++++ nukebuild/MicroComGen.cs | 14 -------------- 2 files changed, 9 insertions(+), 14 deletions(-) delete mode 100644 nukebuild/MicroComGen.cs diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index 358549db55..7425c344c3 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -23,6 +23,7 @@ using static Nuke.Common.Tools.MSBuild.MSBuildTasks; using static Nuke.Common.Tools.DotNet.DotNetTasks; using static Nuke.Common.Tools.Xunit.XunitTasks; using static Nuke.Common.Tools.VSWhere.VSWhereTasks; +using MicroCom.CodeGenerator; /* Before editing this file, install support plugin for your IDE, @@ -278,6 +279,14 @@ partial class Build : NukeBuild .DependsOn(Package) .DependsOn(ZipFiles); + Target GenerateCppHeaders => _ => _.Executes(() => + { + var file = MicroComCodeGenerator.Parse( + File.ReadAllText(RootDirectory / "src" / "Avalonia.Native" / "avn.idl")); + File.WriteAllText(RootDirectory / "native" / "Avalonia.Native" / "inc" / "avalonia-native.h", + file.GenerateCppHeader()); + }); + public static int Main() => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) diff --git a/nukebuild/MicroComGen.cs b/nukebuild/MicroComGen.cs deleted file mode 100644 index b1e546cb97..0000000000 --- a/nukebuild/MicroComGen.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.IO; -using MicroCom.CodeGenerator; -using Nuke.Common; - -partial class Build : NukeBuild -{ - Target GenerateCppHeaders => _ => _.Executes(() => - { - var file = MicroComCodeGenerator.Parse( - File.ReadAllText(RootDirectory / "src" / "Avalonia.Native" / "avn.idl")); - File.WriteAllText(RootDirectory / "native" / "Avalonia.Native" / "inc" / "avalonia-native.h", - file.GenerateCppHeader()); - }); -} \ No newline at end of file From 929f1e6b92896d7afe2f0fefd1b8987df45c78e4 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 8 Oct 2022 13:15:50 +0100 Subject: [PATCH 084/169] retry integration tests up to 3 times. --- azure-pipelines-integrationtests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/azure-pipelines-integrationtests.yml b/azure-pipelines-integrationtests.yml index 99983b1758..ee8abb75c1 100644 --- a/azure-pipelines-integrationtests.yml +++ b/azure-pipelines-integrationtests.yml @@ -57,6 +57,7 @@ jobs: projects: 'samples/IntegrationTestApp/IntegrationTestApp.csproj' - task: DotNetCoreCLI@2 + retryCountOnTaskFailure: 3 inputs: command: 'test' projects: 'tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj' From 61e15b3a8c5e4785ae0fced4be245d61f223a772 Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 8 Oct 2022 12:58:15 -0400 Subject: [PATCH 085/169] Create MaterialColorPalette.cs --- .../ColorPalettes/MaterialColorPalette.cs | 362 ++++++++++++++++++ 1 file changed, 362 insertions(+) create mode 100644 src/Avalonia.Controls.ColorPicker/ColorPalettes/MaterialColorPalette.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorPalettes/MaterialColorPalette.cs b/src/Avalonia.Controls.ColorPicker/ColorPalettes/MaterialColorPalette.cs new file mode 100644 index 0000000000..4df1448f7a --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/ColorPalettes/MaterialColorPalette.cs @@ -0,0 +1,362 @@ +using Avalonia.Media; +using Avalonia.Utilities; + +namespace Avalonia.Controls +{ + /// + /// Implements a reduced version of the 2014 Material Design color palette. + /// + /// + /// This palette is based on the one outlined here: + /// + /// https://material.io/design/color/the-color-system.html#tools-for-picking-colors + /// + /// In order to make the palette uniform and rectangular the following + /// alterations were made: + /// + /// 1. The A100-A700 shades of each color are excluded. + /// These shades do not exist for all colors (brown/gray). + /// 2. Black/White are stand-alone and are also excluded. + /// + /// + public class MaterialColorPalette : IColorPalette + { + // See: https://material.io/design/color/the-color-system.html#tools-for-picking-colors + // This is a reduced palette for uniformity + private static Color[,]? _colorChart = null; + private static int _colorChartColorCount = 0; + private static int _colorChartShadeCount = 0; + private static object _colorChartMutex = new object(); + + /// + /// Initializes all color chart colors. + /// + /// + /// This is pulled out separately to lazy load for performance. + /// If no material color palette is ever used, no colors will be created. + /// + private void InitColorChart() + { + lock (_colorChartMutex) + { + _colorChart = new Color[,] + { + // Red + { + Color.FromArgb(0xFF, 0xFF, 0xEB, 0xEE), + Color.FromArgb(0xFF, 0xFF, 0xCD, 0xD2), + Color.FromArgb(0xFF, 0xEF, 0x9A, 0x9A), + Color.FromArgb(0xFF, 0xE5, 0x73, 0x73), + Color.FromArgb(0xFF, 0xEF, 0x53, 0x50), + Color.FromArgb(0xFF, 0xF4, 0x43, 0x36), + Color.FromArgb(0xFF, 0xE5, 0x39, 0x35), + Color.FromArgb(0xFF, 0xD3, 0x2F, 0x2F), + Color.FromArgb(0xFF, 0xC6, 0x28, 0x28), + Color.FromArgb(0xFF, 0xB7, 0x1C, 0x1C), + }, + + // Pink + { + Color.FromArgb(0xFF, 0xFC, 0xE4, 0xEC), + Color.FromArgb(0xFF, 0xF8, 0xBB, 0xD0), + Color.FromArgb(0xFF, 0xF4, 0x8F, 0xB1), + Color.FromArgb(0xFF, 0xF0, 0x62, 0x92), + Color.FromArgb(0xFF, 0xEC, 0x40, 0x7A), + Color.FromArgb(0xFF, 0xE9, 0x1E, 0x63), + Color.FromArgb(0xFF, 0xD8, 0x1B, 0x60), + Color.FromArgb(0xFF, 0xC2, 0x18, 0x5B), + Color.FromArgb(0xFF, 0xAD, 0x14, 0x57), + Color.FromArgb(0xFF, 0x88, 0x0E, 0x4F), + }, + + // Purple + { + Color.FromArgb(0xFF, 0xF3, 0xE5, 0xF5), + Color.FromArgb(0xFF, 0xE1, 0xBE, 0xE7), + Color.FromArgb(0xFF, 0xCE, 0x93, 0xD8), + Color.FromArgb(0xFF, 0xBA, 0x68, 0xC8), + Color.FromArgb(0xFF, 0xAB, 0x47, 0xBC), + Color.FromArgb(0xFF, 0x9C, 0x27, 0xB0), + Color.FromArgb(0xFF, 0x8E, 0x24, 0xAA), + Color.FromArgb(0xFF, 0x7B, 0x1F, 0xA2), + Color.FromArgb(0xFF, 0x6A, 0x1B, 0x9A), + Color.FromArgb(0xFF, 0x4A, 0x14, 0x8C), + }, + + // Deep Purple + { + Color.FromArgb(0xFF, 0xED, 0xE7, 0xF6), + Color.FromArgb(0xFF, 0xD1, 0xC4, 0xE9), + Color.FromArgb(0xFF, 0xB3, 0x9D, 0xDB), + Color.FromArgb(0xFF, 0x95, 0x75, 0xCD), + Color.FromArgb(0xFF, 0x7E, 0x57, 0xC2), + Color.FromArgb(0xFF, 0x67, 0x3A, 0xB7), + Color.FromArgb(0xFF, 0x5E, 0x35, 0xB1), + Color.FromArgb(0xFF, 0x51, 0x2D, 0xA8), + Color.FromArgb(0xFF, 0x45, 0x27, 0xA0), + Color.FromArgb(0xFF, 0x31, 0x1B, 0x92), + }, + + // Indigo + { + Color.FromArgb(0xFF, 0xE8, 0xEA, 0xF6), + Color.FromArgb(0xFF, 0xC5, 0xCA, 0xE9), + Color.FromArgb(0xFF, 0x9F, 0xA8, 0xDA), + Color.FromArgb(0xFF, 0x79, 0x86, 0xCB), + Color.FromArgb(0xFF, 0x5C, 0x6B, 0xC0), + Color.FromArgb(0xFF, 0x3F, 0x51, 0xB5), + Color.FromArgb(0xFF, 0x39, 0x49, 0xAB), + Color.FromArgb(0xFF, 0x30, 0x3F, 0x9F), + Color.FromArgb(0xFF, 0x28, 0x35, 0x93), + Color.FromArgb(0xFF, 0x1A, 0x23, 0x7E), + }, + + // Blue + { + Color.FromArgb(0xFF, 0xE3, 0xF2, 0xFD), + Color.FromArgb(0xFF, 0xBB, 0xDE, 0xFB), + Color.FromArgb(0xFF, 0x90, 0xCA, 0xF9), + Color.FromArgb(0xFF, 0x64, 0xB5, 0xF6), + Color.FromArgb(0xFF, 0x42, 0xA5, 0xF5), + Color.FromArgb(0xFF, 0x21, 0x96, 0xF3), + Color.FromArgb(0xFF, 0x1E, 0x88, 0xE5), + Color.FromArgb(0xFF, 0x19, 0x76, 0xD2), + Color.FromArgb(0xFF, 0x15, 0x65, 0xC0), + Color.FromArgb(0xFF, 0x0D, 0x47, 0xA1), + }, + + // Light Blue + { + Color.FromArgb(0xFF, 0xE1, 0xF5, 0xFE), + Color.FromArgb(0xFF, 0xB3, 0xE5, 0xFC), + Color.FromArgb(0xFF, 0x81, 0xD4, 0xFA), + Color.FromArgb(0xFF, 0x4F, 0xC3, 0xF7), + Color.FromArgb(0xFF, 0x29, 0xB6, 0xF6), + Color.FromArgb(0xFF, 0x03, 0xA9, 0xF4), + Color.FromArgb(0xFF, 0x03, 0x9B, 0xE5), + Color.FromArgb(0xFF, 0x02, 0x88, 0xD1), + Color.FromArgb(0xFF, 0x02, 0x77, 0xBD), + Color.FromArgb(0xFF, 0x01, 0x57, 0x9B), + }, + + // Cyan + { + Color.FromArgb(0xFF, 0xE0, 0xF7, 0xFA), + Color.FromArgb(0xFF, 0xB2, 0xEB, 0xF2), + Color.FromArgb(0xFF, 0x80, 0xDE, 0xEA), + Color.FromArgb(0xFF, 0x4D, 0xD0, 0xE1), + Color.FromArgb(0xFF, 0x26, 0xC6, 0xDA), + Color.FromArgb(0xFF, 0x00, 0xBC, 0xD4), + Color.FromArgb(0xFF, 0x00, 0xAC, 0xC1), + Color.FromArgb(0xFF, 0x00, 0x97, 0xA7), + Color.FromArgb(0xFF, 0x00, 0x83, 0x8F), + Color.FromArgb(0xFF, 0x00, 0x60, 0x64), + }, + + // Teal + { + Color.FromArgb(0xFF, 0xE0, 0xF2, 0xF1), + Color.FromArgb(0xFF, 0xB2, 0xDF, 0xDB), + Color.FromArgb(0xFF, 0x80, 0xCB, 0xC4), + Color.FromArgb(0xFF, 0x4D, 0xB6, 0xAC), + Color.FromArgb(0xFF, 0x26, 0xA6, 0x9A), + Color.FromArgb(0xFF, 0x00, 0x96, 0x88), + Color.FromArgb(0xFF, 0x00, 0x89, 0x7B), + Color.FromArgb(0xFF, 0x00, 0x79, 0x6B), + Color.FromArgb(0xFF, 0x00, 0x69, 0x5C), + Color.FromArgb(0xFF, 0x00, 0x4D, 0x40), + }, + + // Green + { + Color.FromArgb(0xFF, 0xE8, 0xF5, 0xE9), + Color.FromArgb(0xFF, 0xC8, 0xE6, 0xC9), + Color.FromArgb(0xFF, 0xA5, 0xD6, 0xA7), + Color.FromArgb(0xFF, 0x81, 0xC7, 0x84), + Color.FromArgb(0xFF, 0x66, 0xBB, 0x6A), + Color.FromArgb(0xFF, 0x4C, 0xAF, 0x50), + Color.FromArgb(0xFF, 0x43, 0xA0, 0x47), + Color.FromArgb(0xFF, 0x38, 0x8E, 0x3C), + Color.FromArgb(0xFF, 0x2E, 0x7D, 0x32), + Color.FromArgb(0xFF, 0x1B, 0x5E, 0x20), + }, + + // Light Green + { + Color.FromArgb(0xFF, 0xF1, 0xF8, 0xE9), + Color.FromArgb(0xFF, 0xDC, 0xED, 0xC8), + Color.FromArgb(0xFF, 0xC5, 0xE1, 0xA5), + Color.FromArgb(0xFF, 0xAE, 0xD5, 0x81), + Color.FromArgb(0xFF, 0x9C, 0xCC, 0x65), + Color.FromArgb(0xFF, 0x8B, 0xC3, 0x4A), + Color.FromArgb(0xFF, 0x7C, 0xB3, 0x42), + Color.FromArgb(0xFF, 0x68, 0x9F, 0x38), + Color.FromArgb(0xFF, 0x55, 0x8B, 0x2F), + Color.FromArgb(0xFF, 0x33, 0x69, 0x1E), + }, + + // Lime + { + Color.FromArgb(0xFF, 0xF9, 0xFB, 0xE7), + Color.FromArgb(0xFF, 0xF0, 0xF4, 0xC3), + Color.FromArgb(0xFF, 0xE6, 0xEE, 0x9C), + Color.FromArgb(0xFF, 0xDC, 0xE7, 0x75), + Color.FromArgb(0xFF, 0xD4, 0xE1, 0x57), + Color.FromArgb(0xFF, 0xCD, 0xDC, 0x39), + Color.FromArgb(0xFF, 0xC0, 0xCA, 0x33), + Color.FromArgb(0xFF, 0xAF, 0xB4, 0x2B), + Color.FromArgb(0xFF, 0x9E, 0x9D, 0x24), + Color.FromArgb(0xFF, 0x82, 0x77, 0x17), + }, + + // Yellow + { + Color.FromArgb(0xFF, 0xFF, 0xFD, 0xE7), + Color.FromArgb(0xFF, 0xFF, 0xF9, 0xC4), + Color.FromArgb(0xFF, 0xFF, 0xF5, 0x9D), + Color.FromArgb(0xFF, 0xFF, 0xF1, 0x76), + Color.FromArgb(0xFF, 0xFF, 0xEE, 0x58), + Color.FromArgb(0xFF, 0xFF, 0xEB, 0x3B), + Color.FromArgb(0xFF, 0xFD, 0xD8, 0x35), + Color.FromArgb(0xFF, 0xFB, 0xC0, 0x2D), + Color.FromArgb(0xFF, 0xF9, 0xA8, 0x25), + Color.FromArgb(0xFF, 0xF5, 0x7F, 0x17), + }, + + // Amber + { + Color.FromArgb(0xFF, 0xFF, 0xF8, 0xE1), + Color.FromArgb(0xFF, 0xFF, 0xEC, 0xB3), + Color.FromArgb(0xFF, 0xFF, 0xE0, 0x82), + Color.FromArgb(0xFF, 0xFF, 0xD5, 0x4F), + Color.FromArgb(0xFF, 0xFF, 0xCA, 0x28), + Color.FromArgb(0xFF, 0xFF, 0xC1, 0x07), + Color.FromArgb(0xFF, 0xFF, 0xB3, 0x00), + Color.FromArgb(0xFF, 0xFF, 0xA0, 0x00), + Color.FromArgb(0xFF, 0xFF, 0x8F, 0x00), + Color.FromArgb(0xFF, 0xFF, 0x6F, 0x00), + }, + + // Orange + { + Color.FromArgb(0xFF, 0xFF, 0xF3, 0xE0), + Color.FromArgb(0xFF, 0xFF, 0xE0, 0xB2), + Color.FromArgb(0xFF, 0xFF, 0xCC, 0x80), + Color.FromArgb(0xFF, 0xFF, 0xB7, 0x4D), + Color.FromArgb(0xFF, 0xFF, 0xA7, 0x26), + Color.FromArgb(0xFF, 0xFF, 0x98, 0x00), + Color.FromArgb(0xFF, 0xFB, 0x8C, 0x00), + Color.FromArgb(0xFF, 0xF5, 0x7C, 0x00), + Color.FromArgb(0xFF, 0xEF, 0x6C, 0x00), + Color.FromArgb(0xFF, 0xE6, 0x51, 0x00), + }, + + // Deep Orange + { + Color.FromArgb(0xFF, 0xFB, 0xE9, 0xE7), + Color.FromArgb(0xFF, 0xFF, 0xCC, 0xBC), + Color.FromArgb(0xFF, 0xFF, 0xAB, 0x91), + Color.FromArgb(0xFF, 0xFF, 0x8A, 0x65), + Color.FromArgb(0xFF, 0xFF, 0x70, 0x43), + Color.FromArgb(0xFF, 0xFF, 0x57, 0x22), + Color.FromArgb(0xFF, 0xF4, 0x51, 0x1E), + Color.FromArgb(0xFF, 0xE6, 0x4A, 0x19), + Color.FromArgb(0xFF, 0xD8, 0x43, 0x15), + Color.FromArgb(0xFF, 0xBF, 0x36, 0x0C), + }, + + // Brown + { + Color.FromArgb(0xFF, 0xEF, 0xEB, 0xE9), + Color.FromArgb(0xFF, 0xD7, 0xCC, 0xC8), + Color.FromArgb(0xFF, 0xBC, 0xAA, 0xA4), + Color.FromArgb(0xFF, 0xA1, 0x88, 0x7F), + Color.FromArgb(0xFF, 0x8D, 0x6E, 0x63), + Color.FromArgb(0xFF, 0x79, 0x55, 0x48), + Color.FromArgb(0xFF, 0x6D, 0x4C, 0x41), + Color.FromArgb(0xFF, 0x5D, 0x40, 0x37), + Color.FromArgb(0xFF, 0x4E, 0x34, 0x2E), + Color.FromArgb(0xFF, 0x3E, 0x27, 0x23), + }, + + // Gray + { + Color.FromArgb(0xFF, 0xFA, 0xFA, 0xFA), + Color.FromArgb(0xFF, 0xF5, 0xF5, 0xF5), + Color.FromArgb(0xFF, 0xEE, 0xEE, 0xEE), + Color.FromArgb(0xFF, 0xE0, 0xE0, 0xE0), + Color.FromArgb(0xFF, 0xBD, 0xBD, 0xBD), + Color.FromArgb(0xFF, 0x9E, 0x9E, 0x9E), + Color.FromArgb(0xFF, 0x75, 0x75, 0x75), + Color.FromArgb(0xFF, 0x61, 0x61, 0x61), + Color.FromArgb(0xFF, 0x42, 0x42, 0x42), + Color.FromArgb(0xFF, 0x21, 0x21, 0x21), + }, + + // Blue Gray + { + Color.FromArgb(0xFF, 0xEC, 0xEF, 0xF1), + Color.FromArgb(0xFF, 0xCF, 0xD8, 0xDC), + Color.FromArgb(0xFF, 0xB0, 0xBE, 0xC5), + Color.FromArgb(0xFF, 0x90, 0xA4, 0xAE), + Color.FromArgb(0xFF, 0x78, 0x90, 0x9C), + Color.FromArgb(0xFF, 0x60, 0x7D, 0x8B), + Color.FromArgb(0xFF, 0x54, 0x6E, 0x7A), + Color.FromArgb(0xFF, 0x45, 0x5A, 0x64), + Color.FromArgb(0xFF, 0x37, 0x47, 0x4F), + Color.FromArgb(0xFF, 0x26, 0x32, 0x38), + }, + }; + + _colorChartColorCount = _colorChart.GetLength(0); + _colorChartShadeCount = _colorChart.GetLength(1); + } + + return; + } + + /// + public int ColorCount + { + // Table is transposed compared to the reference chart + get + { + if (_colorChart == null) + { + InitColorChart(); + } + + return _colorChartColorCount; + } + } + + /// + public int ShadeCount + { + // Table is transposed compared to the reference chart + get + { + if (_colorChart == null) + { + InitColorChart(); + } + + return _colorChartShadeCount; + } + } + + /// + public Color GetColor(int colorIndex, int shadeIndex) + { + if (_colorChart == null) + { + InitColorChart(); + } + + // Table is transposed compared to the reference chart + return _colorChart![ + MathUtilities.Clamp(colorIndex, 0, _colorChartColorCount - 1), + MathUtilities.Clamp(shadeIndex, 0, _colorChartShadeCount - 1)]; + } + } +} From ea6c90b8fcd82487a5fafa8b66c0b9ac840072cd Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 8 Oct 2022 13:04:15 -0400 Subject: [PATCH 086/169] Fix comments --- .../ColorPalettes/MaterialColorPalette.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorPalettes/MaterialColorPalette.cs b/src/Avalonia.Controls.ColorPicker/ColorPalettes/MaterialColorPalette.cs index 4df1448f7a..5cf5662ede 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorPalettes/MaterialColorPalette.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorPalettes/MaterialColorPalette.cs @@ -318,7 +318,6 @@ namespace Avalonia.Controls /// public int ColorCount { - // Table is transposed compared to the reference chart get { if (_colorChart == null) @@ -333,7 +332,6 @@ namespace Avalonia.Controls /// public int ShadeCount { - // Table is transposed compared to the reference chart get { if (_colorChart == null) @@ -353,7 +351,6 @@ namespace Avalonia.Controls InitColorChart(); } - // Table is transposed compared to the reference chart return _colorChart![ MathUtilities.Clamp(colorIndex, 0, _colorChartColorCount - 1), MathUtilities.Clamp(shadeIndex, 0, _colorChartShadeCount - 1)]; From 9250ccb18afb5d538b6e300a05c606da2d38d3a6 Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 8 Oct 2022 13:35:13 -0400 Subject: [PATCH 087/169] Add missing base constructor call --- .../ColorSpectrum/ColorSpectrum.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs index bd44161a42..200a652d3b 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -95,7 +95,7 @@ namespace Avalonia.Controls.Primitives /// /// Initializes a new instance of the class. /// - public ColorSpectrum() + public ColorSpectrum() : base() { _shapeFromLastBitmapCreation = Shape; _componentsFromLastBitmapCreation = Components; From 5c3a56a9533eab9060a216831df2a9a8c5e08e12 Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 8 Oct 2022 14:25:02 -0400 Subject: [PATCH 088/169] Fix the color spectrum selection ellipse position Specifically, when updated and not part of the visual tree --- .../ColorSpectrum/ColorSpectrum.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs index 200a652d3b..9261e5892d 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -171,6 +171,18 @@ namespace Avalonia.Controls.Primitives { base.OnAttachedToVisualTree(e); + // If the color was updated while this ColorSpectrum was not part of the visual tree, + // the selection ellipse may be in an incorrect position. This is because the spectrum + // renders based on layout scaling to avoid color banding; however, layout scale is only + // available when the control is attached to the visual tree. The ColorSpectrum's color + // may be updated from code-behind or from binding with another control when it's not + // part of the visual tree. + // + // See discussion: https://github.com/AvaloniaUI/Avalonia/discussions/9077 + // + // To work-around this issue the selection ellipse is refreshed here. + UpdateEllipse(); + // OnAttachedToVisualTree is called after OnApplyTemplate so events cannot be connected here } @@ -832,6 +844,8 @@ namespace Avalonia.Controls.Primitives } // Remember the bitmap size follows physical device pixels + // Warning: LayoutHelper.GetLayoutScale() doesn't work unless the control is visible + // This will not be true in all cases if the color is updated from another control or code-behind var scale = LayoutHelper.GetLayoutScale(this); Canvas.SetLeft(_selectionEllipsePanel, (xPosition / scale) - (_selectionEllipsePanel.Width / 2)); Canvas.SetTop(_selectionEllipsePanel, (yPosition / scale) - (_selectionEllipsePanel.Height / 2)); From 55e27213f958304679d449ef4aeb25ca9e8743ea Mon Sep 17 00:00:00 2001 From: robloo Date: Sat, 8 Oct 2022 14:25:26 -0400 Subject: [PATCH 089/169] Comment additional ColorSpectrum methods --- .../ColorSpectrum/ColorSpectrum.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs index 9261e5892d..f272ce850e 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -600,6 +600,10 @@ namespace Avalonia.Controls.Primitives RaiseColorChanged(); } + /// + /// Updates the selected and based on a point within the color spectrum. + /// + /// The point on the spectrum representing the color. private void UpdateColorFromPoint(PointerPoint point) { // If we haven't initialized our HSV value array yet, then we should just ignore any user input - @@ -676,6 +680,9 @@ namespace Avalonia.Controls.Primitives UpdateColor(hsvAtPoint); } + /// + /// Updates the position of the selection ellipse on the spectrum which indicates the selected color. + /// private void UpdateEllipse() { if (_selectionEllipsePanel == null) From 8b070919398878acda991efc0140f7a02cd2a8b7 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 9 Oct 2022 12:21:08 -0400 Subject: [PATCH 090/169] Fix formatting --- .../Themes/Fluent/ColorView.xaml | 30 +++++++++---------- .../Themes/Simple/ColorView.xaml | 30 +++++++++---------- 2 files changed, 28 insertions(+), 32 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml index 16bc2acdd1..cf836aff4a 100644 --- a/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml +++ b/src/Avalonia.Controls.ColorPicker/Themes/Fluent/ColorView.xaml @@ -241,23 +241,21 @@ - + - + - + - + Date: Sun, 9 Oct 2022 13:51:06 -0400 Subject: [PATCH 091/169] Fix some template part names in selectors --- src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml | 6 +++--- src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml | 4 ++-- src/Avalonia.Themes.Simple/Controls/DatePicker.xaml | 2 +- src/Avalonia.Themes.Simple/Controls/TimePicker.xaml | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml b/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml index 0cfd491b21..1c652ddc62 100644 --- a/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/DatePicker.xaml @@ -153,11 +153,11 @@ - - - diff --git a/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml b/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml index fcd661a4b5..ba6dcb17ca 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml @@ -181,11 +181,11 @@ - - diff --git a/src/Avalonia.Themes.Simple/Controls/DatePicker.xaml b/src/Avalonia.Themes.Simple/Controls/DatePicker.xaml index 317a27435a..b4cf6313a6 100644 --- a/src/Avalonia.Themes.Simple/Controls/DatePicker.xaml +++ b/src/Avalonia.Themes.Simple/Controls/DatePicker.xaml @@ -170,7 +170,7 @@ - diff --git a/src/Avalonia.Themes.Simple/Controls/TimePicker.xaml b/src/Avalonia.Themes.Simple/Controls/TimePicker.xaml index 5aa717e9ee..e02e3d8194 100644 --- a/src/Avalonia.Themes.Simple/Controls/TimePicker.xaml +++ b/src/Avalonia.Themes.Simple/Controls/TimePicker.xaml @@ -189,7 +189,7 @@ - From 4b43cb3662e5f8b0f9e7b648dd2b445c405869c7 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 2 Oct 2022 11:51:25 -0400 Subject: [PATCH 092/169] Implement WriteableBitmap caching/reuse and disposal in ColorSlider --- .../ColorSlider/ColorSlider.cs | 42 ++++++++++++++++++- .../Helpers/ColorPickerHelpers.cs | 19 ++++++++- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs index b662d20223..4e907f610d 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs @@ -2,6 +2,7 @@ using Avalonia.Controls.Metadata; using Avalonia.Layout; using Avalonia.Media; +using Avalonia.Media.Imaging; using Avalonia.Utilities; namespace Avalonia.Controls.Primitives @@ -31,6 +32,8 @@ namespace Avalonia.Controls.Primitives protected bool ignorePropertyChanged = false; + private WriteableBitmap? _backgroundBitmap; + /// /// Initializes a new instance of the class. /// @@ -38,6 +41,26 @@ namespace Avalonia.Controls.Primitives { } + /// + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + // Bitmaps were released when detached from the visual tree so they must be re-built + UpdateBackground(); + } + + /// + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + + // Clean-up all bitmaps + // https://github.com/AvaloniaUI/Avalonia/issues/9051 + _backgroundBitmap?.Dispose(); + _backgroundBitmap = null; + } + /// /// Updates the visual state of the control by applying latest PseudoClasses. /// @@ -110,7 +133,19 @@ namespace Avalonia.Controls.Primitives if (bitmap != null) { - Background = new ImageBrush(ColorPickerHelpers.CreateBitmapFromPixelData(bitmap, pixelWidth, pixelHeight)); + if (_backgroundBitmap != null) + { + // Re-use the existing WriteableBitmap + // This assumes the height, width and byte counts are the same and must be set to null + // elsewhere if that assumption is ever not true. + ColorPickerHelpers.UpdateBitmapFromPixelData(_backgroundBitmap, bitmap); + } + else + { + _backgroundBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bitmap, pixelWidth, pixelHeight); + } + + Background = new ImageBrush(_backgroundBitmap); } } } @@ -399,6 +434,11 @@ namespace Avalonia.Controls.Primitives } else if (change.Property == BoundsProperty) { + // If the control's overall dimensions have changed the background bitmap size also needs to change. + // This means the existing bitmap must be released to be recreated correctly in UpdateBackground(). + _backgroundBitmap?.Dispose(); + _backgroundBitmap = null; + UpdateBackground(); } else if (change.Property == ValueProperty || diff --git a/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs b/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs index c1904a3c30..f2683e4677 100644 --- a/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs +++ b/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs @@ -617,7 +617,6 @@ namespace Avalonia.Controls.Primitives PixelFormat.Bgra8888, AlphaFormat.Premul); - // Warning: This is highly questionable using (var frameBuffer = bitmap.Lock()) { Marshal.Copy(bgraPixelData.ToArray(), 0, frameBuffer.Address, bgraPixelData.Count); @@ -625,5 +624,23 @@ namespace Avalonia.Controls.Primitives return bitmap; } + + /// + /// Updates the given with new, raw BGRA pre-multiplied alpha pixel data. + /// WARNING: The bitmap's width, height and byte count MUST not have changed and MUST be enforced externally. + /// + /// The existing to update. + /// The bitmap (in raw BGRA pre-multiplied alpha pixels). + public static void UpdateBitmapFromPixelData( + WriteableBitmap bitmap, + IList bgraPixelData) + { + using (var frameBuffer = bitmap.Lock()) + { + Marshal.Copy(bgraPixelData.ToArray(), 0, frameBuffer.Address, bgraPixelData.Count); + } + + return; + } } } From ca7543f1d613f9d7b9f2cfed978f8e23379d6684 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 2 Oct 2022 14:24:10 -0400 Subject: [PATCH 093/169] Add new ArrayList to avoid an extra copy when creating spectrum bitmaps --- .../ColorSlider/ColorSlider.cs | 8 +-- .../ColorSpectrum/ColorSpectrum.cs | 61 +++++++++------- .../Helpers/ArrayList.cs | 71 +++++++++++++++++++ .../Helpers/ColorPickerHelpers.cs | 18 ++--- 4 files changed, 118 insertions(+), 40 deletions(-) create mode 100644 src/Avalonia.Controls.ColorPicker/Helpers/ArrayList.cs diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs index 4e907f610d..8bcdef5558 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs @@ -121,7 +121,7 @@ namespace Avalonia.Controls.Primitives if (pixelWidth != 0 && pixelHeight != 0) { - var bitmap = await ColorPickerHelpers.CreateComponentBitmapAsync( + ArrayList bgraPixelData = await ColorPickerHelpers.CreateComponentBitmapAsync( pixelWidth, pixelHeight, Orientation, @@ -131,18 +131,18 @@ namespace Avalonia.Controls.Primitives IsAlphaMaxForced, IsSaturationValueMaxForced); - if (bitmap != null) + if (bgraPixelData != null) { if (_backgroundBitmap != null) { // Re-use the existing WriteableBitmap // This assumes the height, width and byte counts are the same and must be set to null // elsewhere if that assumption is ever not true. - ColorPickerHelpers.UpdateBitmapFromPixelData(_backgroundBitmap, bitmap); + ColorPickerHelpers.UpdateBitmapFromPixelData(_backgroundBitmap, bgraPixelData); } else { - _backgroundBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bitmap, pixelWidth, pixelHeight); + _backgroundBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraPixelData, pixelWidth, pixelHeight); } Background = new ImageBrush(_backgroundBitmap); diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs index f272ce850e..3604894833 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -994,13 +994,13 @@ namespace Avalonia.Controls.Primitives // The middle 4 are only needed and used in the case of hue as the third dimension. // Saturation and luminosity need only a min and max. - List bgraMinPixelData = new List(); - List bgraMiddle1PixelData = new List(); - List bgraMiddle2PixelData = new List(); - List bgraMiddle3PixelData = new List(); - List bgraMiddle4PixelData = new List(); - List bgraMaxPixelData = new List(); - List newHsvValues = new List(); + ArrayList bgraMinPixelData; + ArrayList bgraMiddle1PixelData; + ArrayList bgraMiddle2PixelData; + ArrayList bgraMiddle3PixelData; + ArrayList bgraMiddle4PixelData; + ArrayList bgraMaxPixelData; + List newHsvValues; // In Avalonia, Bounds returns the actual device-independent pixel size of a control. // However, this is not necessarily the size of the control rendered on a display. @@ -1011,20 +1011,27 @@ namespace Avalonia.Controls.Primitives int pixelDimension = (int)Math.Round(minDimension * scale); var pixelCount = pixelDimension * pixelDimension; var pixelDataSize = pixelCount * 4; - bgraMinPixelData.Capacity = pixelDataSize; + + bgraMinPixelData = new ArrayList(pixelDataSize); + bgraMaxPixelData = new ArrayList(pixelDataSize); + newHsvValues = new List(pixelCount); // We'll only save pixel data for the middle bitmaps if our third dimension is hue. if (components == ColorSpectrumComponents.ValueSaturation || components == ColorSpectrumComponents.SaturationValue) { - bgraMiddle1PixelData.Capacity = pixelDataSize; - bgraMiddle2PixelData.Capacity = pixelDataSize; - bgraMiddle3PixelData.Capacity = pixelDataSize; - bgraMiddle4PixelData.Capacity = pixelDataSize; + bgraMiddle1PixelData = new ArrayList(pixelDataSize); + bgraMiddle2PixelData = new ArrayList(pixelDataSize); + bgraMiddle3PixelData = new ArrayList(pixelDataSize); + bgraMiddle4PixelData = new ArrayList(pixelDataSize); + } + else + { + bgraMiddle1PixelData = new ArrayList(0); + bgraMiddle2PixelData = new ArrayList(0); + bgraMiddle3PixelData = new ArrayList(0); + bgraMiddle4PixelData = new ArrayList(0); } - - bgraMaxPixelData.Capacity = pixelDataSize; - newHsvValues.Capacity = pixelCount; await Task.Run(() => { @@ -1132,12 +1139,12 @@ namespace Avalonia.Controls.Primitives double maxSaturation, double minValue, double maxValue, - List bgraMinPixelData, - List bgraMiddle1PixelData, - List bgraMiddle2PixelData, - List bgraMiddle3PixelData, - List bgraMiddle4PixelData, - List bgraMaxPixelData, + ArrayList bgraMinPixelData, + ArrayList bgraMiddle1PixelData, + ArrayList bgraMiddle2PixelData, + ArrayList bgraMiddle3PixelData, + ArrayList bgraMiddle4PixelData, + ArrayList bgraMaxPixelData, List newHsvValues) { double hMin = minHue; @@ -1292,12 +1299,12 @@ namespace Avalonia.Controls.Primitives double maxSaturation, double minValue, double maxValue, - List bgraMinPixelData, - List bgraMiddle1PixelData, - List bgraMiddle2PixelData, - List bgraMiddle3PixelData, - List bgraMiddle4PixelData, - List bgraMaxPixelData, + ArrayList bgraMinPixelData, + ArrayList bgraMiddle1PixelData, + ArrayList bgraMiddle2PixelData, + ArrayList bgraMiddle3PixelData, + ArrayList bgraMiddle4PixelData, + ArrayList bgraMaxPixelData, List newHsvValues) { double hMin = minHue; diff --git a/src/Avalonia.Controls.ColorPicker/Helpers/ArrayList.cs b/src/Avalonia.Controls.ColorPicker/Helpers/ArrayList.cs new file mode 100644 index 0000000000..0b4c2f8579 --- /dev/null +++ b/src/Avalonia.Controls.ColorPicker/Helpers/ArrayList.cs @@ -0,0 +1,71 @@ +namespace Avalonia.Controls.Primitives +{ + /// + /// A thin wrapper over an that allows some additional list-like functionality. + /// + /// + /// This is only for internal ColorPicker-related functionality and should not be used elsewhere. + /// It is added for performance to enjoy the simplicity of the IList.Add() method without requiring + /// an additional copy to turn a list into an array for bitmaps. + /// + /// The type of items in the array. + internal class ArrayList + { + private int _nextIndex = 0; + + /// + /// Initializes a new instance of the class. + /// + public ArrayList(int capacity) + { + Capacity = capacity; + Array = new T[capacity]; + } + + /// + /// Provides access to the underlying array by index. + /// This exists for simplification and the property + /// may also be used. + /// + /// The index of the item to get or set. + /// The item at the given index. + public T this[int i] + { + get => Array[i]; + set => Array[i] = value; + } + + /// + /// Gets the underlying array. + /// + public T[] Array { get; private set; } + + /// + /// Gets the fixed capacity/size of the array. + /// This must be set during construction. + /// + public int Capacity { get; private set; } + + /// + /// Adds the given item to the array at the next available index. + /// WARNING: This must be used carefully and only once, in sequence. + /// + /// The item to add. + public void Add(T item) + { + if (_nextIndex >= 0 && + _nextIndex < Capacity) + { + Array[_nextIndex] = item; + _nextIndex++; + } + else + { + // If necessary an exception could be thrown here + // throw new IndexOutOfRangeException(); + } + + return; + } + } +} diff --git a/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs b/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs index f2683e4677..2378813f52 100644 --- a/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs +++ b/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs @@ -37,7 +37,7 @@ namespace Avalonia.Controls.Primitives /// during calculation with the HSVA color model. /// This will ensure colors are always discernible regardless of saturation/value. /// A new bitmap representing a gradient of color component values. - public static async Task CreateComponentBitmapAsync( + public static async Task> CreateComponentBitmapAsync( int width, int height, Orientation orientation, @@ -49,14 +49,14 @@ namespace Avalonia.Controls.Primitives { if (width == 0 || height == 0) { - return new byte[0]; + return new ArrayList(0); } - var bitmap = await Task.Run(() => + var bitmap = await Task.Run>(() => { int pixelDataIndex = 0; double componentStep; - byte[] bgraPixelData; + ArrayList bgraPixelData; Color baseRgbColor = Colors.White; Color rgbColor; int bgraPixelDataHeight; @@ -64,7 +64,7 @@ namespace Avalonia.Controls.Primitives // Allocate the buffer // BGRA formatted color components 1 byte each (4 bytes in a pixel) - bgraPixelData = new byte[width * height * 4]; + bgraPixelData = new ArrayList(width * height * 4); bgraPixelDataHeight = height * 4; bgraPixelDataWidth = width * 4; @@ -604,7 +604,7 @@ namespace Avalonia.Controls.Primitives /// The pixel height of the bitmap. /// A new . public static WriteableBitmap CreateBitmapFromPixelData( - IList bgraPixelData, + ArrayList bgraPixelData, int pixelWidth, int pixelHeight) { @@ -619,7 +619,7 @@ namespace Avalonia.Controls.Primitives using (var frameBuffer = bitmap.Lock()) { - Marshal.Copy(bgraPixelData.ToArray(), 0, frameBuffer.Address, bgraPixelData.Count); + Marshal.Copy(bgraPixelData.Array, 0, frameBuffer.Address, bgraPixelData.Array.Length); } return bitmap; @@ -633,11 +633,11 @@ namespace Avalonia.Controls.Primitives /// The bitmap (in raw BGRA pre-multiplied alpha pixels). public static void UpdateBitmapFromPixelData( WriteableBitmap bitmap, - IList bgraPixelData) + ArrayList bgraPixelData) { using (var frameBuffer = bitmap.Lock()) { - Marshal.Copy(bgraPixelData.ToArray(), 0, frameBuffer.Address, bgraPixelData.Count); + Marshal.Copy(bgraPixelData.Array, 0, frameBuffer.Address, bgraPixelData.Array.Length); } return; From 98a58217ce22d55f9343216fd4fbc727618c7306 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 2 Oct 2022 14:25:24 -0400 Subject: [PATCH 094/169] Fix ColorSlider update/refresh for all property changes --- .../ColorSlider/ColorSlider.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs index 8bcdef5558..6f20238b70 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs @@ -385,11 +385,11 @@ namespace Avalonia.Controls.Primitives return; } - // Always keep the two color properties in sync if (change.Property == ColorProperty) { ignorePropertyChanged = true; + // Always keep the two color properties in sync HsvColor = Color.ToHsv(); SetColorToSliderValues(); @@ -402,7 +402,10 @@ namespace Avalonia.Controls.Primitives ignorePropertyChanged = false; } - else if (change.Property == ColorModelProperty) + else if (change.Property == ColorComponentProperty || + change.Property == ColorModelProperty || + change.Property == IsAlphaMaxForcedProperty || + change.Property == IsSaturationValueMaxForcedProperty) { ignorePropertyChanged = true; @@ -416,6 +419,7 @@ namespace Avalonia.Controls.Primitives { ignorePropertyChanged = true; + // Always keep the two color properties in sync Color = HsvColor.ToRgb(); SetColorToSliderValues(); @@ -440,6 +444,7 @@ namespace Avalonia.Controls.Primitives _backgroundBitmap = null; UpdateBackground(); + UpdatePseudoClasses(); } else if (change.Property == ValueProperty || change.Property == MinimumProperty || From 4f1d315b4b11cf84ca7c3b4ffc65c41d35e24436 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 2 Oct 2022 14:26:28 -0400 Subject: [PATCH 095/169] Make all ColorSpectrum bitmaps globally accessible by the control for future disposal --- .../ColorSpectrum/ColorSpectrum.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs index 3604894833..6b06e1eac1 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSpectrum/ColorSpectrum.cs @@ -73,6 +73,8 @@ namespace Avalonia.Controls.Primitives private WriteableBitmap? _saturationMaximumBitmap; private WriteableBitmap? _valueBitmap; + private WriteableBitmap? _minBitmap; + private WriteableBitmap? _maxBitmap; // Fields used by UpdateEllipse() to ensure that it's using the data // associated with the last call to CreateBitmapsAndColorMap(), @@ -1084,28 +1086,28 @@ namespace Avalonia.Controls.Primitives ColorSpectrumComponents components2 = Components; - WriteableBitmap minBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraMinPixelData, pixelWidth, pixelHeight); - WriteableBitmap maxBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraMaxPixelData, pixelWidth, pixelHeight); + _minBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraMinPixelData, pixelWidth, pixelHeight); + _maxBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraMaxPixelData, pixelWidth, pixelHeight); switch (components2) { case ColorSpectrumComponents.HueValue: case ColorSpectrumComponents.ValueHue: - _saturationMinimumBitmap = minBitmap; - _saturationMaximumBitmap = maxBitmap; + _saturationMinimumBitmap = _minBitmap; + _saturationMaximumBitmap = _maxBitmap; break; case ColorSpectrumComponents.HueSaturation: case ColorSpectrumComponents.SaturationHue: - _valueBitmap = maxBitmap; + _valueBitmap = _maxBitmap; break; case ColorSpectrumComponents.ValueSaturation: case ColorSpectrumComponents.SaturationValue: - _hueRedBitmap = minBitmap; + _hueRedBitmap = _minBitmap; _hueYellowBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraMiddle1PixelData, pixelWidth, pixelHeight); _hueGreenBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraMiddle2PixelData, pixelWidth, pixelHeight); _hueCyanBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraMiddle3PixelData, pixelWidth, pixelHeight); _hueBlueBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraMiddle4PixelData, pixelWidth, pixelHeight); - _huePurpleBitmap = maxBitmap; + _huePurpleBitmap = _maxBitmap; break; } From 667b02d2c0d6e713980f0ad05c843d96e0c5a6f4 Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 2 Oct 2022 14:33:24 -0400 Subject: [PATCH 096/169] Disable WriteableBitmap re-use in ColorSlider due to crashes --- .../ColorSlider/ColorSlider.cs | 8 +++++++- .../Helpers/ColorPickerHelpers.cs | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs index 6f20238b70..8b624958dd 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs @@ -135,10 +135,16 @@ namespace Avalonia.Controls.Primitives { if (_backgroundBitmap != null) { + // CURRENTLY DISABLED DUE TO INTERMITTENT CRASHES IN SKIA/RENDERER + // // Re-use the existing WriteableBitmap // This assumes the height, width and byte counts are the same and must be set to null // elsewhere if that assumption is ever not true. - ColorPickerHelpers.UpdateBitmapFromPixelData(_backgroundBitmap, bgraPixelData); + // ColorPickerHelpers.UpdateBitmapFromPixelData(_backgroundBitmap, bgraPixelData); + + // ALSO DISABLED DISPOSE DUE TO INTERMITTENT CRASHES + //_backgroundBitmap?.Dispose(); + _backgroundBitmap = ColorPickerHelpers.CreateBitmapFromPixelData(bgraPixelData, pixelWidth, pixelHeight); } else { diff --git a/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs b/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs index 2378813f52..01e5a0868f 100644 --- a/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs +++ b/src/Avalonia.Controls.ColorPicker/Helpers/ColorPickerHelpers.cs @@ -627,6 +627,7 @@ namespace Avalonia.Controls.Primitives /// /// Updates the given with new, raw BGRA pre-multiplied alpha pixel data. + /// WARNING: THIS METHOD IS CURRENTLY PROVIDED AS REFERENCE BUT CAUSES INTERMITTENT CRASHES IF USED. /// WARNING: The bitmap's width, height and byte count MUST not have changed and MUST be enforced externally. /// /// The existing to update. From bc927d03129fa201248d2bcab78143f7a1c8f93f Mon Sep 17 00:00:00 2001 From: robloo Date: Sun, 9 Oct 2022 21:07:58 -0400 Subject: [PATCH 097/169] Remove ColorSlider background disposal on attach/detach from visual tree --- .../ColorSlider/ColorSlider.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs index 8b624958dd..0b9f7f9146 100644 --- a/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs +++ b/src/Avalonia.Controls.ColorPicker/ColorSlider/ColorSlider.cs @@ -45,20 +45,12 @@ namespace Avalonia.Controls.Primitives protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); - - // Bitmaps were released when detached from the visual tree so they must be re-built - UpdateBackground(); } /// protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { base.OnDetachedFromVisualTree(e); - - // Clean-up all bitmaps - // https://github.com/AvaloniaUI/Avalonia/issues/9051 - _backgroundBitmap?.Dispose(); - _backgroundBitmap = null; } /// From 0d05c40ca25381cec24a1c6456891113794d7b6a Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 10 Oct 2022 04:05:14 -0400 Subject: [PATCH 098/169] Remove old blazor backend. Keep blazor components with new WASM implementation. --- samples/ControlCatalog.Web/App.razor.cs | 4 - .../ControlCatalog.Web.csproj | 40 +- .../ControlCatalog.Web/EmbedSample.Browser.cs | 34 -- ...valonia.Web.Blazor.CompilationTuning.props | 7 - .../Avalonia.Web.Blazor.csproj | 53 +- .../Avalonia.Web.Blazor.props | 4 - .../Avalonia.Web.Blazor.targets | 6 - .../AvaloniaBlazorAppBuilder.cs | 20 - src/Web/Avalonia.Web.Blazor/AvaloniaView.cs | 43 ++ .../Avalonia.Web.Blazor/AvaloniaView.razor | 67 --- .../Avalonia.Web.Blazor/AvaloniaView.razor.cs | 500 ------------------ .../BlazorSingleViewLifetime.cs | 35 +- src/Web/Avalonia.Web.Blazor/BlazorSkiaGpu.cs | 25 - .../BlazorSkiaGpuRenderSession.cs | 37 -- .../BlazorSkiaGpuRenderTarget.cs | 39 -- .../BlazorSkiaRasterSurface.cs | 87 --- .../Avalonia.Web.Blazor/BlazorSkiaSurface.cs | 30 -- src/Web/Avalonia.Web.Blazor/ClipboardImpl.cs | 34 -- src/Web/Avalonia.Web.Blazor/Cursor.cs | 93 ---- .../Avalonia.Web.Blazor/IBlazorSkiaSurface.cs | 9 - .../Interop/ActionHelper.cs | 81 --- .../Interop/AvaloniaModule.cs | 18 - .../Interop/DpiWatcherInterop.cs | 72 --- .../Interop/FocusHelperInterop.cs | 22 - .../Interop/InputHelperInterop.cs | 130 ----- .../Interop/JSModuleInterop.cs | 46 -- .../Interop/NativeControlHostImpl.cs | 144 ----- .../Interop/SKHtmlCanvasInterop.cs | 76 --- .../Interop/SizeWatcherInterop.cs | 50 -- .../Interop/Storage/StorageProviderInterop.cs | 225 -------- .../Interop/Storage/WriteableStream.cs | 124 ----- .../JSObjectControlHandle.cs | 35 -- src/Web/Avalonia.Web.Blazor/Keycodes.cs | 127 ----- .../ManualTriggerRenderTimer.cs | 17 - .../RazorViewTopLevelImpl.cs | 222 -------- src/Web/Avalonia.Web.Blazor/WinStubs.cs | 50 -- .../Avalonia.Web.Blazor/WindowingPlatform.cs | 106 ---- src/Web/Avalonia.Web.Blazor/_Imports.razor | 1 - src/Web/Avalonia.Web.Blazor/webapp/build.js | 16 - .../webapp/modules/Avalonia.ts | 7 - .../webapp/modules/Avalonia/CaretHelper.ts | 149 ------ .../webapp/modules/Avalonia/DpiWatcher.ts | 40 -- .../webapp/modules/Avalonia/FocusHelper.ts | 9 - .../webapp/modules/Avalonia/InputHelper.ts | 86 --- .../modules/Avalonia/NativeControlHost.ts | 61 --- .../webapp/modules/Avalonia/SKHtmlCanvas.ts | 255 --------- .../webapp/modules/Avalonia/SizeWatcher.ts | 67 --- .../webapp/modules/Storage.ts | 1 - .../modules/Storage/IndexedDbWrapper.ts | 79 --- .../webapp/modules/Storage/StorageProvider.ts | 204 ------- .../Avalonia.Web.Blazor/webapp/package.json | 13 - .../Avalonia.Web.Blazor/webapp/tsconfig.json | 18 - .../webapp/types/dotnet/index.d.ts | 56 -- src/Web/Avalonia.Web.Sample/main.js | 4 +- src/Web/Avalonia.Web/AvaloniaView.cs | 19 +- .../Avalonia.Web/BrowserSingleViewLifetime.cs | 22 +- src/Web/Avalonia.Web/Cursor.cs | 2 +- src/Web/Avalonia.Web/Interop/CanvasHelper.cs | 6 +- src/Web/Avalonia.Web/Interop/DomHelper.cs | 10 +- src/Web/Avalonia.Web/Interop/InputHelper.cs | 24 +- .../Interop/NativeControlHostHelper.cs | 14 +- src/Web/Avalonia.Web/Interop/StorageHelper.cs | 30 +- src/Web/Avalonia.Web/Interop/StreamHelper.cs | 14 +- .../Avalonia.Web/ManualTriggerRenderTimer.cs | 2 +- .../Storage/BrowserStorageProvider.cs | 2 +- src/Web/Avalonia.Web/WindowingPlatform.cs | 2 +- .../Avalonia.Web/webapp/modules/avalonia.ts | 26 +- .../webapp/modules/avalonia/canvas.ts | 4 +- 68 files changed, 182 insertions(+), 3773 deletions(-) delete mode 100644 samples/ControlCatalog.Web/EmbedSample.Browser.cs delete mode 100644 src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.CompilationTuning.props delete mode 100644 src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.props delete mode 100644 src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.targets delete mode 100644 src/Web/Avalonia.Web.Blazor/AvaloniaBlazorAppBuilder.cs create mode 100644 src/Web/Avalonia.Web.Blazor/AvaloniaView.cs delete mode 100644 src/Web/Avalonia.Web.Blazor/AvaloniaView.razor delete mode 100644 src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs delete mode 100644 src/Web/Avalonia.Web.Blazor/BlazorSkiaGpu.cs delete mode 100644 src/Web/Avalonia.Web.Blazor/BlazorSkiaGpuRenderSession.cs delete mode 100644 src/Web/Avalonia.Web.Blazor/BlazorSkiaGpuRenderTarget.cs delete mode 100644 src/Web/Avalonia.Web.Blazor/BlazorSkiaRasterSurface.cs delete mode 100644 src/Web/Avalonia.Web.Blazor/BlazorSkiaSurface.cs delete mode 100644 src/Web/Avalonia.Web.Blazor/ClipboardImpl.cs delete mode 100644 src/Web/Avalonia.Web.Blazor/Cursor.cs delete mode 100644 src/Web/Avalonia.Web.Blazor/IBlazorSkiaSurface.cs delete mode 100644 src/Web/Avalonia.Web.Blazor/Interop/ActionHelper.cs delete mode 100644 src/Web/Avalonia.Web.Blazor/Interop/AvaloniaModule.cs delete mode 100644 src/Web/Avalonia.Web.Blazor/Interop/DpiWatcherInterop.cs delete mode 100644 src/Web/Avalonia.Web.Blazor/Interop/FocusHelperInterop.cs delete mode 100644 src/Web/Avalonia.Web.Blazor/Interop/InputHelperInterop.cs delete mode 100644 src/Web/Avalonia.Web.Blazor/Interop/JSModuleInterop.cs delete mode 100644 src/Web/Avalonia.Web.Blazor/Interop/NativeControlHostImpl.cs delete mode 100644 src/Web/Avalonia.Web.Blazor/Interop/SKHtmlCanvasInterop.cs delete mode 100644 src/Web/Avalonia.Web.Blazor/Interop/SizeWatcherInterop.cs delete mode 100644 src/Web/Avalonia.Web.Blazor/Interop/Storage/StorageProviderInterop.cs delete mode 100644 src/Web/Avalonia.Web.Blazor/Interop/Storage/WriteableStream.cs delete mode 100644 src/Web/Avalonia.Web.Blazor/JSObjectControlHandle.cs delete mode 100644 src/Web/Avalonia.Web.Blazor/Keycodes.cs delete mode 100644 src/Web/Avalonia.Web.Blazor/ManualTriggerRenderTimer.cs delete mode 100644 src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs delete mode 100644 src/Web/Avalonia.Web.Blazor/WinStubs.cs delete mode 100644 src/Web/Avalonia.Web.Blazor/WindowingPlatform.cs delete mode 100644 src/Web/Avalonia.Web.Blazor/_Imports.razor delete mode 100644 src/Web/Avalonia.Web.Blazor/webapp/build.js delete mode 100644 src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia.ts delete mode 100644 src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/CaretHelper.ts delete mode 100644 src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/DpiWatcher.ts delete mode 100644 src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/FocusHelper.ts delete mode 100644 src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/InputHelper.ts delete mode 100644 src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/NativeControlHost.ts delete mode 100644 src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/SKHtmlCanvas.ts delete mode 100644 src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/SizeWatcher.ts delete mode 100644 src/Web/Avalonia.Web.Blazor/webapp/modules/Storage.ts delete mode 100644 src/Web/Avalonia.Web.Blazor/webapp/modules/Storage/IndexedDbWrapper.ts delete mode 100644 src/Web/Avalonia.Web.Blazor/webapp/modules/Storage/StorageProvider.ts delete mode 100644 src/Web/Avalonia.Web.Blazor/webapp/package.json delete mode 100644 src/Web/Avalonia.Web.Blazor/webapp/tsconfig.json delete mode 100644 src/Web/Avalonia.Web.Blazor/webapp/types/dotnet/index.d.ts diff --git a/samples/ControlCatalog.Web/App.razor.cs b/samples/ControlCatalog.Web/App.razor.cs index bcd2a6fefc..ca3d3604b1 100644 --- a/samples/ControlCatalog.Web/App.razor.cs +++ b/samples/ControlCatalog.Web/App.razor.cs @@ -8,10 +8,6 @@ public partial class App protected override void OnParametersSet() { WebAppBuilder.Configure() - .AfterSetup(_ => - { - ControlCatalog.Pages.EmbedSample.Implementation = new EmbedSampleWeb(); - }) .With(new SkiaOptions { CustomGpuFactory = null }) // uncomment to disable GPU/GL rendering .SetupWithSingleViewLifetime(); diff --git a/samples/ControlCatalog.Web/ControlCatalog.Web.csproj b/samples/ControlCatalog.Web/ControlCatalog.Web.csproj index b2c9ec72eb..03fb31f0d3 100644 --- a/samples/ControlCatalog.Web/ControlCatalog.Web.csproj +++ b/samples/ControlCatalog.Web/ControlCatalog.Web.csproj @@ -1,44 +1,16 @@  - net6.0 + net7.0 + browser-wasm enable - - true 16777216 false false - - - false - -O1 - false - - - - true - true - -O3 - -O3 - false - false - false - false - false - false - true - false - true - true - true - link - true - - - - + + @@ -50,8 +22,8 @@ - - + + diff --git a/samples/ControlCatalog.Web/EmbedSample.Browser.cs b/samples/ControlCatalog.Web/EmbedSample.Browser.cs deleted file mode 100644 index 5fe14409de..0000000000 --- a/samples/ControlCatalog.Web/EmbedSample.Browser.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; - -using Avalonia; -using Avalonia.Platform; -using Avalonia.Web.Blazor; - -using ControlCatalog.Pages; - -using Microsoft.JSInterop; - -namespace ControlCatalog.Web; - -public class EmbedSampleWeb : INativeDemoControl -{ - public IPlatformHandle CreateControl(bool isSecond, IPlatformHandle parent, Func createDefault) - { - var runtime = AvaloniaLocator.Current.GetRequiredService(); - - if (isSecond) - { - var iframe = runtime.Invoke("document.createElement", "iframe"); - iframe.InvokeVoid("setAttribute", "src", "https://www.youtube.com/embed/kZCIporjJ70"); - - return new JSObjectControlHandle(iframe); - } - else - { - // window.createAppButton source is defined in "app.js" file. - var button = runtime.Invoke("window.createAppButton"); - - return new JSObjectControlHandle(button); - } - } -} diff --git a/src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.CompilationTuning.props b/src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.CompilationTuning.props deleted file mode 100644 index eb5e5dd733..0000000000 --- a/src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.CompilationTuning.props +++ /dev/null @@ -1,7 +0,0 @@ - - - 16777216 - false - false - - diff --git a/src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.csproj b/src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.csproj index 693a6a1462..1c31e0eb5d 100644 --- a/src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.csproj +++ b/src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.csproj @@ -1,53 +1,40 @@  - net6.0 - enable + net7.0 Avalonia.Web.Blazor - preview - false - true + _IncludeGeneratedAvaloniaStaticFiles;$(ResolveStaticWebAssetsInputsDependsOn) - - - - - true - build\ - - - true - build\;buildTransitive\ - + - - - - - - - - - - + - - - - - - - + + + <_AvaloniaWebAssets Include="$(MSBuildThisFileDirectory)../Avalonia.Web/wwwroot/**/*.*" /> + + + + diff --git a/src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.props b/src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.props deleted file mode 100644 index dd96a60c6a..0000000000 --- a/src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.props +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.targets b/src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.targets deleted file mode 100644 index e9052fda88..0000000000 --- a/src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.targets +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/src/Web/Avalonia.Web.Blazor/AvaloniaBlazorAppBuilder.cs b/src/Web/Avalonia.Web.Blazor/AvaloniaBlazorAppBuilder.cs deleted file mode 100644 index 11d9bcc98f..0000000000 --- a/src/Web/Avalonia.Web.Blazor/AvaloniaBlazorAppBuilder.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Avalonia.Controls; -using Avalonia.Platform; - -namespace Avalonia.Web.Blazor -{ - public class AvaloniaBlazorAppBuilder : AppBuilderBase - { - public AvaloniaBlazorAppBuilder(IRuntimePlatform platform, Action platformServices) - : base(platform, platformServices) - { - } - - public AvaloniaBlazorAppBuilder() - : base(new StandardRuntimePlatform(), - builder => StandardRuntimePlatformServices.Register(builder.ApplicationType!.Assembly)) - { - UseWindowingSubsystem(BlazorWindowingPlatform.Register); - } - } -} diff --git a/src/Web/Avalonia.Web.Blazor/AvaloniaView.cs b/src/Web/Avalonia.Web.Blazor/AvaloniaView.cs new file mode 100644 index 0000000000..ae294151a6 --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/AvaloniaView.cs @@ -0,0 +1,43 @@ +using System.Runtime.InteropServices.JavaScript; +using System.Runtime.Versioning; +using System.Threading.Tasks; +using System; +using Avalonia.Controls.ApplicationLifetimes; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Rendering; +using BrowserView = Avalonia.Web.AvaloniaView; + +namespace Avalonia.Web.Blazor; + +[SupportedOSPlatform("browser")] +public class AvaloniaView : ComponentBase +{ + private BrowserView? _browserView; + private readonly string _containerId; + + public AvaloniaView() + { + _containerId = "av_container_" + Guid.NewGuid(); + } + + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + builder.OpenElement(0, "div"); + builder.AddAttribute(1, "id", _containerId); + builder.CloseElement(); + } + + protected override async Task OnInitializedAsync() + { + if (OperatingSystem.IsBrowser()) + { + _ = await JSHost.ImportAsync("avalonia", "/_content/Avalonia.Web.Blazor/avalonia.js"); + + _browserView = new BrowserView(_containerId); + if (Application.Current?.ApplicationLifetime is ISingleViewApplicationLifetime lifetime) + { + _browserView.Content = lifetime.MainView; + } + } + } +} diff --git a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor deleted file mode 100644 index 5a3b9d5f71..0000000000 --- a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor +++ /dev/null @@ -1,67 +0,0 @@ -
- - - -
- - -
- - diff --git a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs deleted file mode 100644 index 0e64e98f1e..0000000000 --- a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs +++ /dev/null @@ -1,500 +0,0 @@ -using System.Diagnostics; -using Avalonia.Controls.ApplicationLifetimes; -using Avalonia.Controls.Embedding; -using Avalonia.Controls.Platform; -using Avalonia.Input; -using Avalonia.Input.Raw; -using Avalonia.Input.TextInput; -using Avalonia.Platform.Storage; -using Avalonia.Rendering; -using Avalonia.Rendering.Composition; -using Avalonia.Web.Blazor.Interop; -using Avalonia.Web.Blazor.Interop.Storage; - -using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.Components.Forms; -using Microsoft.AspNetCore.Components.Web; -using Microsoft.JSInterop; - -using SkiaSharp; -using HTMLPointerEventArgs = Microsoft.AspNetCore.Components.Web.PointerEventArgs; - -namespace Avalonia.Web.Blazor -{ - public partial class AvaloniaView : ITextInputMethodImpl - { - private readonly RazorViewTopLevelImpl _topLevelImpl; - private EmbeddableControlRoot _topLevel; - - // Interop - private SKHtmlCanvasInterop? _interop = null; - private SizeWatcherInterop? _sizeWatcher = null; - private DpiWatcherInterop? _dpiWatcher = null; - private SKHtmlCanvasInterop.GLInfo? _jsGlInfo = null; - private AvaloniaModule? _avaloniaModule = null; - private InputHelperInterop? _inputHelper = null; - private FocusHelperInterop? _canvasHelper = null; - private FocusHelperInterop? _containerHelper = null; - private NativeControlHostInterop? _nativeControlHost = null; - private StorageProviderInterop? _storageProvider = null; - private ElementReference _htmlCanvas; - private ElementReference _inputElement; - private ElementReference _containerElement; - private ElementReference _nativeControlsContainer; - private double _dpi = 1; - private SKSize _canvasSize = new (100, 100); - - private GRContext? _context; - private GRGlInterface? _glInterface; - private const SKColorType ColorType = SKColorType.Rgba8888; - - private bool _useGL; - private bool _inputElementFocused; - - private ITextInputMethodClient? _client; - - - [Inject] private IJSRuntime Js { get; set; } = null!; - - public AvaloniaView() - { - _topLevelImpl = new RazorViewTopLevelImpl(this); - - _topLevel = new EmbeddableControlRoot(_topLevelImpl); - - if (Application.Current?.ApplicationLifetime is ISingleViewApplicationLifetime lifetime) - { - _topLevel.Content = lifetime.MainView; - } - } - - public bool KeyPreventDefault { get; set; } - - public ITextInputMethodClient? Client => _client; - - public bool IsActive => _client != null; - - public bool IsComposing { get; private set; } - - internal INativeControlHostImpl GetNativeControlHostImpl() - { - return _nativeControlHost ?? throw new InvalidOperationException("Blazor View wasn't initialized yet"); - } - - internal IStorageProvider GetStorageProvider() - { - return _storageProvider ?? throw new InvalidOperationException("Blazor View wasn't initialized yet"); - } - - private void OnPointerCancel(HTMLPointerEventArgs e) - { - if (e.PointerType == "touch") - { - _topLevelImpl.RawPointerEvent(RawPointerEventType.TouchCancel, e.PointerType, GetPointFromEventArgs(e), - GetModifiers(e), e.PointerId); - } - } - - private void OnPointerMove(HTMLPointerEventArgs e) - { - var type = e.PointerType switch - { - "touch" => RawPointerEventType.TouchUpdate, - _ => RawPointerEventType.Move - }; - - _topLevelImpl.RawPointerEvent(type, e.PointerType, GetPointFromEventArgs(e), GetModifiers(e), e.PointerId); - } - - private void OnPointerUp(HTMLPointerEventArgs e) - { - var type = e.PointerType switch - { - "touch" => RawPointerEventType.TouchEnd, - _ => e.Button switch - { - 0 => RawPointerEventType.LeftButtonUp, - 1 => RawPointerEventType.MiddleButtonUp, - 2 => RawPointerEventType.RightButtonUp, - 3 => RawPointerEventType.XButton1Up, - 4 => RawPointerEventType.XButton2Up, - // 5 => Pen eraser button, - _ => RawPointerEventType.Move - } - }; - - _topLevelImpl.RawPointerEvent(type, e.PointerType, GetPointFromEventArgs(e), GetModifiers(e), e.PointerId); - } - - private void OnPointerDown(HTMLPointerEventArgs e) - { - var type = e.PointerType switch - { - "touch" => RawPointerEventType.TouchBegin, - _ => e.Button switch - { - 0 => RawPointerEventType.LeftButtonDown, - 1 => RawPointerEventType.MiddleButtonDown, - 2 => RawPointerEventType.RightButtonDown, - 3 => RawPointerEventType.XButton1Down, - 4 => RawPointerEventType.XButton2Down, - // 5 => Pen eraser button, - _ => RawPointerEventType.Move - } - }; - - _topLevelImpl.RawPointerEvent(type, e.PointerType, GetPointFromEventArgs(e), GetModifiers(e), e.PointerId); - } - - private static RawPointerPoint GetPointFromEventArgs(HTMLPointerEventArgs args) - { - return new RawPointerPoint - { - Position = new Point(args.ClientX, args.ClientY), - Pressure = args.Pressure, - XTilt = args.TiltX, - YTilt = args.TiltY - // Twist = args.Twist - read from JS code directly when - }; - } - - private void OnWheel(WheelEventArgs e) - { - _topLevelImpl.RawMouseWheelEvent( new Point(e.ClientX, e.ClientY), - new Vector(-(e.DeltaX / 50), -(e.DeltaY / 50)), GetModifiers(e)); - } - - private static RawInputModifiers GetModifiers(MouseEventArgs e) - { - var modifiers = RawInputModifiers.None; - - if (e.CtrlKey) - modifiers |= RawInputModifiers.Control; - if (e.AltKey) - modifiers |= RawInputModifiers.Alt; - if (e.ShiftKey) - modifiers |= RawInputModifiers.Shift; - if (e.MetaKey) - modifiers |= RawInputModifiers.Meta; - - if ((e.Buttons & 1L) == 1) - modifiers |= RawInputModifiers.LeftMouseButton; - - if ((e.Buttons & 2L) == 2) - modifiers |= e.Type == "pen" ? RawInputModifiers.PenBarrelButton : RawInputModifiers.RightMouseButton; - - if ((e.Buttons & 4L) == 4) - modifiers |= RawInputModifiers.MiddleMouseButton; - - if ((e.Buttons & 8L) == 8) - modifiers |= RawInputModifiers.XButton1MouseButton; - - if ((e.Buttons & 16L) == 16) - modifiers |= RawInputModifiers.XButton2MouseButton; - - if ((e.Buttons & 32L) == 32) - modifiers |= RawInputModifiers.PenEraser; - - return modifiers; - } - - private static RawInputModifiers GetModifiers(KeyboardEventArgs e) - { - var modifiers = RawInputModifiers.None; - - if (e.CtrlKey) - modifiers |= RawInputModifiers.Control; - if (e.AltKey) - modifiers |= RawInputModifiers.Alt; - if (e.ShiftKey) - modifiers |= RawInputModifiers.Shift; - if (e.MetaKey) - modifiers |= RawInputModifiers.Meta; - - return modifiers; - } - - private void OnKeyDown(KeyboardEventArgs e) - { - KeyPreventDefault = _topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyDown, e.Code, e.Key, GetModifiers(e)); - } - - private void OnKeyUp(KeyboardEventArgs e) - { - KeyPreventDefault = _topLevelImpl.RawKeyboardEvent(RawKeyEventType.KeyUp, e.Code, e.Key, GetModifiers(e)); - } - - private void OnFocus(FocusEventArgs e) - { - // if focus has unexpectedly moved from the input element to the container element, - // shift it back to the input element - if (_inputElementFocused && _inputHelper is not null) - { - _inputHelper.Focus(); - } - } - - [Parameter(CaptureUnmatchedValues = true)] - public IReadOnlyDictionary? AdditionalAttributes { get; set; } - - protected override async Task OnAfterRenderAsync(bool firstRender) - { - if (firstRender) - { - AvaloniaLocator.CurrentMutable.Bind().ToConstant((IJSInProcessRuntime)Js); - - _avaloniaModule = await AvaloniaModule.ImportAsync(Js); - - _canvasHelper = new FocusHelperInterop(_avaloniaModule, _htmlCanvas); - _containerHelper = new FocusHelperInterop(_avaloniaModule, _containerElement); - _inputHelper = new InputHelperInterop(_avaloniaModule, _inputElement); - - _inputHelper.CompositionEvent += InputHelperOnCompositionEvent; - _inputHelper.InputEvent += InputHelperOnInputEvent; - - HideIme(); - _canvasHelper.SetCursor("default"); - _topLevelImpl.SetCssCursor = x => - { - _inputHelper.SetCursor(x); //macOS - _canvasHelper.SetCursor(x); //windows - }; - - _nativeControlHost = new NativeControlHostInterop(_avaloniaModule, _nativeControlsContainer); - _storageProvider = await StorageProviderInterop.ImportAsync(Js); - - Console.WriteLine("starting html canvas setup"); - _interop = new SKHtmlCanvasInterop(_avaloniaModule, _htmlCanvas, OnRenderFrame); - - Console.WriteLine("Interop created"); - - var skiaOptions = AvaloniaLocator.Current.GetService(); - _useGL = skiaOptions?.CustomGpuFactory != null; - - if (_useGL) - { - _jsGlInfo = _interop.InitGL(); - Console.WriteLine("jsglinfo created - init gl"); - } - else - { - var rasterInitialized = _interop.InitRaster(); - Console.WriteLine("raster initialized: {0}", rasterInitialized); - } - - if (_useGL) - { - // create the SkiaSharp context - if (_context == null) - { - _glInterface = GRGlInterface.Create(); - _context = GRContext.CreateGl(_glInterface); - - - // bump the default resource cache limit - _context.SetResourceCacheLimit(skiaOptions?.MaxGpuResourceSizeBytes ?? 32 * 1024 * 1024); - Console.WriteLine("glcontext created and resource limit set"); - } - - _topLevelImpl.SetSurface(_context, _jsGlInfo!, ColorType, - new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi); - } - else - { - _topLevelImpl.SetSurface(ColorType, - new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi, _interop.PutImageData); - } - - _interop.SetCanvasSize((int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); - - Threading.Dispatcher.UIThread.Post(async () => - { - _interop.RequestAnimationFrame(true); - - _sizeWatcher = new SizeWatcherInterop(_avaloniaModule, _htmlCanvas, OnSizeChanged); - _dpiWatcher = new DpiWatcherInterop(_avaloniaModule, OnDpiChanged); - - _sizeWatcher.Start(); - _topLevel.Prepare(); - - _topLevel.Renderer.Start(); - }); - } - } - - private void InputHelperOnInputEvent(object? sender, WebInputEventArgs e) - { - if (IsComposing) - { - return; - } - - _topLevelImpl.RawTextEvent(e.Data); - - e.Handled = true; - } - - private void InputHelperOnCompositionEvent(object? sender, WebCompositionEventArgs e) - { - if(_client == null) - { - return; - } - - switch (e.Type) - { - case WebCompositionEventArgs.WebCompositionEventType.Start: - _client.SetPreeditText(null); - IsComposing = true; - break; - case WebCompositionEventArgs.WebCompositionEventType.Update: - _client.SetPreeditText(e.Data); - break; - case WebCompositionEventArgs.WebCompositionEventType.End: - IsComposing = false; - _client.SetPreeditText(null); - _topLevelImpl.RawTextEvent(e.Data); - break; - } - } - - private void OnRenderFrame() - { - if (_useGL && (_jsGlInfo == null)) - { - Console.WriteLine("nothing to render"); - return; - } - if (_canvasSize.Width <= 0 || _canvasSize.Height <= 0 || _dpi <= 0) - { - Console.WriteLine("nothing to render"); - return; - } - - ManualTriggerRenderTimer.Instance.RaiseTick(); - } - - public void Dispose() - { - _dpiWatcher?.Unsubscribe(OnDpiChanged); - _sizeWatcher?.Dispose(); - _interop?.Dispose(); - } - - private void ForceBlit() - { - // Note: this is technically a hack, but it's a kinda unique use case when - // we want to blit the previous frame - // renderer doesn't have much control over the render target - // we render on the UI thread - // We also don't want to have it as a meaningful public API. - // Therefore we have InternalsVisibleTo hack here. - - if (_topLevel.Renderer is CompositingRenderer dr) - { - dr.CompositionTarget.ImmediateUIThreadRender(); - } - } - - private void OnDpiChanged(double newDpi) - { - if (Math.Abs(_dpi - newDpi) > 0.0001) - { - _dpi = newDpi; - - _interop!.SetCanvasSize((int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); - - _topLevelImpl.SetClientSize(_canvasSize, _dpi); - - ForceBlit(); - } - } - - private void OnSizeChanged(SKSize newSize) - { - if (_canvasSize != newSize) - { - _canvasSize = newSize; - - _interop!.SetCanvasSize((int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); - - _topLevelImpl.SetClientSize(_canvasSize, _dpi); - - ForceBlit(); - } - } - - private void HideIme() - { - _inputHelper?.Hide(); - _containerHelper?.Focus(); - } - - public void SetClient(ITextInputMethodClient? client) - { - if (_inputHelper is null) - { - return; - } - - if(_client != null) - { - _client.SurroundingTextChanged -= SurroundingTextChanged; - } - - if(client != null) - { - client.SurroundingTextChanged += SurroundingTextChanged; - } - - _inputHelper.Clear(); - - _client = client; - - if (IsActive && _client != null) - { - _inputHelper.Show(); - _inputElementFocused = true; - _inputHelper.Focus(); - - var surroundingText = _client.SurroundingText; - - _inputHelper?.SetSurroundingText(surroundingText.Text, surroundingText.AnchorOffset, surroundingText.CursorOffset); - } - else - { - _inputElementFocused = false; - HideIme(); - } - } - - private void SurroundingTextChanged(object? sender, EventArgs e) - { - if(_client != null) - { - var surroundingText = _client.SurroundingText; - - _inputHelper?.SetSurroundingText(surroundingText.Text, surroundingText.AnchorOffset, surroundingText.CursorOffset); - } - } - - public void SetCursorRect(Rect rect) - { - _inputHelper?.Focus(); - var bounds = new PixelRect((int)rect.X, (int) rect.Y, (int) rect.Width, (int) rect.Height); - - _inputHelper?.SetBounds(bounds, _client?.SurroundingText.CursorOffset ?? 0); - _inputHelper?.Focus(); - } - - public void SetOptions(TextInputOptions options) - { - } - - public void Reset() - { - _inputHelper?.Clear(); - _inputHelper?.SetSurroundingText("", 0, 0); - } - } -} diff --git a/src/Web/Avalonia.Web.Blazor/BlazorSingleViewLifetime.cs b/src/Web/Avalonia.Web.Blazor/BlazorSingleViewLifetime.cs index 7970f09a58..26b4b15863 100644 --- a/src/Web/Avalonia.Web.Blazor/BlazorSingleViewLifetime.cs +++ b/src/Web/Avalonia.Web.Blazor/BlazorSingleViewLifetime.cs @@ -1,31 +1,28 @@ using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; -using Avalonia.Media; -namespace Avalonia.Web.Blazor +namespace Avalonia.Web.Blazor; + +public static class WebAppBuilder { - public class BlazorSingleViewLifetime : ISingleViewApplicationLifetime + public static T SetupWithSingleViewLifetime( + this T builder) + where T : AppBuilderBase, new() { - public Control? MainView { get; set; } + return builder.SetupWithLifetime(new BlazorSingleViewLifetime()); } - public static class WebAppBuilder + public static AppBuilder Configure() + where TApp : Application, new() { - public static T SetupWithSingleViewLifetime( - this T builder) - where T : AppBuilderBase, new() - { - return builder.SetupWithLifetime(new BlazorSingleViewLifetime()); - } + var builder = AppBuilder.Configure() + .UseBrowser(); - public static AvaloniaBlazorAppBuilder Configure() - where TApp : Application, new() - { - var builder = AvaloniaBlazorAppBuilder.Configure() - .UseSkia() - .With(new SkiaOptions { CustomGpuFactory = () => new BlazorSkiaGpu() }); + return builder; + } - return builder; - } + internal class BlazorSingleViewLifetime : ISingleViewApplicationLifetime + { + public Control? MainView { get; set; } } } diff --git a/src/Web/Avalonia.Web.Blazor/BlazorSkiaGpu.cs b/src/Web/Avalonia.Web.Blazor/BlazorSkiaGpu.cs deleted file mode 100644 index 6fa7bf0bde..0000000000 --- a/src/Web/Avalonia.Web.Blazor/BlazorSkiaGpu.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Avalonia.Skia; - -namespace Avalonia.Web.Blazor -{ - public class BlazorSkiaGpu : ISkiaGpu - { - public ISkiaGpuRenderTarget? TryCreateRenderTarget(IEnumerable surfaces) - { - foreach (var surface in surfaces) - { - if (surface is BlazorSkiaSurface blazorSkiaSurface) - { - return new BlazorSkiaGpuRenderTarget(blazorSkiaSurface); - } - } - - return null; - } - - public ISkiaSurface? TryCreateSurface(PixelSize size, ISkiaGpuRenderSession session) - { - return null; - } - } -} diff --git a/src/Web/Avalonia.Web.Blazor/BlazorSkiaGpuRenderSession.cs b/src/Web/Avalonia.Web.Blazor/BlazorSkiaGpuRenderSession.cs deleted file mode 100644 index 0c53825131..0000000000 --- a/src/Web/Avalonia.Web.Blazor/BlazorSkiaGpuRenderSession.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Avalonia.Skia; -using SkiaSharp; - -namespace Avalonia.Web.Blazor -{ - internal class BlazorSkiaGpuRenderSession : ISkiaGpuRenderSession - { - private readonly SKSurface _surface; - - - public BlazorSkiaGpuRenderSession(BlazorSkiaSurface blazorSkiaSurface, GRBackendRenderTarget renderTarget) - { - _surface = SKSurface.Create(blazorSkiaSurface.Context, renderTarget, blazorSkiaSurface.Origin, blazorSkiaSurface.ColorType); - - GrContext = blazorSkiaSurface.Context; - - ScaleFactor = blazorSkiaSurface.Scaling; - - SurfaceOrigin = blazorSkiaSurface.Origin; - } - - public void Dispose() - { - _surface.Flush(); - - _surface.Dispose(); - } - - public GRContext GrContext { get; } - - public SKSurface SkSurface => _surface; - - public double ScaleFactor { get; } - - public GRSurfaceOrigin SurfaceOrigin { get; } - } -} diff --git a/src/Web/Avalonia.Web.Blazor/BlazorSkiaGpuRenderTarget.cs b/src/Web/Avalonia.Web.Blazor/BlazorSkiaGpuRenderTarget.cs deleted file mode 100644 index fa6a39f210..0000000000 --- a/src/Web/Avalonia.Web.Blazor/BlazorSkiaGpuRenderTarget.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Avalonia.Skia; -using SkiaSharp; - -namespace Avalonia.Web.Blazor -{ - internal class BlazorSkiaGpuRenderTarget : ISkiaGpuRenderTarget - { - private readonly GRBackendRenderTarget _renderTarget; - private readonly BlazorSkiaSurface _blazorSkiaSurface; - private readonly PixelSize _size; - - public BlazorSkiaGpuRenderTarget(BlazorSkiaSurface blazorSkiaSurface) - { - _size = blazorSkiaSurface.Size; - - var glFbInfo = new GRGlFramebufferInfo(blazorSkiaSurface.GlInfo.FboId, blazorSkiaSurface.ColorType.ToGlSizedFormat()); - { - _blazorSkiaSurface = blazorSkiaSurface; - _renderTarget = new GRBackendRenderTarget( - (int)(blazorSkiaSurface.Size.Width * blazorSkiaSurface.Scaling), - (int)(blazorSkiaSurface.Size.Height * blazorSkiaSurface.Scaling), - blazorSkiaSurface.GlInfo.Samples, - blazorSkiaSurface.GlInfo.Stencils, glFbInfo); - } - } - - public void Dispose() - { - _renderTarget.Dispose(); - } - - public ISkiaGpuRenderSession BeginRenderingSession() - { - return new BlazorSkiaGpuRenderSession(_blazorSkiaSurface, _renderTarget); - } - - public bool IsCorrupted => _blazorSkiaSurface.Size != _size; - } -} diff --git a/src/Web/Avalonia.Web.Blazor/BlazorSkiaRasterSurface.cs b/src/Web/Avalonia.Web.Blazor/BlazorSkiaRasterSurface.cs deleted file mode 100644 index 603a792de3..0000000000 --- a/src/Web/Avalonia.Web.Blazor/BlazorSkiaRasterSurface.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System.Runtime.InteropServices; -using Avalonia.Controls.Platform.Surfaces; -using Avalonia.Platform; -using Avalonia.Skia; -using SkiaSharp; - -namespace Avalonia.Web.Blazor -{ - internal class BlazorSkiaRasterSurface : IBlazorSkiaSurface, IFramebufferPlatformSurface, IDisposable - { - public SKColorType ColorType { get; set; } - - public PixelSize Size { get; set; } - - public double Scaling { get; set; } - - private FramebufferData? _fbData; - private readonly Action _blitCallback; - private readonly Action _onDisposeAction; - - public BlazorSkiaRasterSurface( - SKColorType colorType, PixelSize size, double scaling, Action blitCallback) - { - ColorType = colorType; - Size = size; - Scaling = scaling; - _blitCallback = blitCallback; - _onDisposeAction = Blit; - } - - public void Dispose() - { - _fbData?.Dispose(); - _fbData = null; - } - - public ILockedFramebuffer Lock() - { - var bytesPerPixel = 4; // TODO: derive from ColorType - var dpi = Scaling * 96.0; - var width = (int)(Size.Width * Scaling); - var height = (int)(Size.Height * Scaling); - - if (_fbData is null || _fbData?.Size.Width != width || _fbData?.Size.Height != height) - { - _fbData?.Dispose(); - _fbData = new FramebufferData(width, height, bytesPerPixel); - } - - var pixelFormat = ColorType.ToPixelFormat(); - var data = _fbData.Value; - return new LockedFramebuffer( - data.Address, data.Size, data.RowBytes, - new Vector(dpi, dpi), pixelFormat, _onDisposeAction); - } - - private void Blit() - { - if (_fbData != null) - { - var data = _fbData.Value; - _blitCallback(data.Address, new SKSizeI(data.Size.Width, data.Size.Height)); - } - } - - private readonly struct FramebufferData - { - public PixelSize Size { get; } - - public int RowBytes { get; } - - public IntPtr Address { get; } - - public FramebufferData(int width, int height, int bytesPerPixel) - { - Size = new PixelSize(width, height); - RowBytes = width * bytesPerPixel; - Address = Marshal.AllocHGlobal(width * height * bytesPerPixel); - } - - public void Dispose() - { - Marshal.FreeHGlobal(Address); - } - } - } -} diff --git a/src/Web/Avalonia.Web.Blazor/BlazorSkiaSurface.cs b/src/Web/Avalonia.Web.Blazor/BlazorSkiaSurface.cs deleted file mode 100644 index fb49df338b..0000000000 --- a/src/Web/Avalonia.Web.Blazor/BlazorSkiaSurface.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Avalonia.Web.Blazor.Interop; -using SkiaSharp; - -namespace Avalonia.Web.Blazor -{ - internal class BlazorSkiaSurface : IBlazorSkiaSurface - { - public BlazorSkiaSurface(GRContext context, SKHtmlCanvasInterop.GLInfo glInfo, SKColorType colorType, PixelSize size, double scaling, GRSurfaceOrigin origin) - { - Context = context; - GlInfo = glInfo; - ColorType = colorType; - Size = size; - Scaling = scaling; - Origin = origin; - } - - public SKColorType ColorType { get; set; } - - public PixelSize Size { get; set; } - - public GRContext Context { get; set; } - - public GRSurfaceOrigin Origin { get; set; } - - public double Scaling { get; set; } - - public SKHtmlCanvasInterop.GLInfo GlInfo { get; set; } - } -} diff --git a/src/Web/Avalonia.Web.Blazor/ClipboardImpl.cs b/src/Web/Avalonia.Web.Blazor/ClipboardImpl.cs deleted file mode 100644 index bafc07ca15..0000000000 --- a/src/Web/Avalonia.Web.Blazor/ClipboardImpl.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Avalonia.Input; -using Avalonia.Input.Platform; -using Microsoft.JSInterop; - -namespace Avalonia.Web.Blazor -{ - internal class ClipboardImpl : IClipboard - { - public async Task GetTextAsync() - { - return await AvaloniaLocator.Current.GetRequiredService(). - InvokeAsync("navigator.clipboard.readText"); - } - - public async Task SetTextAsync(string text) - { - await AvaloniaLocator.Current.GetRequiredService(). - InvokeAsync("navigator.clipboard.writeText",text); - } - - public async Task ClearAsync() => await SetTextAsync(""); - - public Task SetDataObjectAsync(IDataObject data) => Task.CompletedTask; - - public Task GetFormatsAsync() => Task.FromResult(Array.Empty()); - - public Task GetDataAsync(string format) => Task.FromResult(new()); - } -} diff --git a/src/Web/Avalonia.Web.Blazor/Cursor.cs b/src/Web/Avalonia.Web.Blazor/Cursor.cs deleted file mode 100644 index d921b2fa6c..0000000000 --- a/src/Web/Avalonia.Web.Blazor/Cursor.cs +++ /dev/null @@ -1,93 +0,0 @@ -using Avalonia.Input; -using Avalonia.Platform; - -namespace Avalonia.Web.Blazor -{ - public class CssCursor : ICursorImpl - { - public const string Default = "default"; - public string? Value { get; set; } - - public CssCursor(StandardCursorType type) - { - Value = ToKeyword(type); - } - - /// - /// Create a cursor from base64 image - /// - public CssCursor(string base64, string format, PixelPoint hotspot, StandardCursorType fallback) - { - Value = $"url(\"data:image/{format};base64,{base64}\") {hotspot.X} {hotspot.Y}, {ToKeyword(fallback)}"; - } - - /// - /// Create a cursor from url to *.cur file. - /// - public CssCursor(string url, StandardCursorType fallback) - { - Value = $"url('{url}'), {ToKeyword(fallback)}"; - } - - /// - /// Create a cursor from png/svg and hotspot position - /// - public CssCursor(string url, PixelPoint hotSpot, StandardCursorType fallback) - { - Value = $"url('{url}') {hotSpot.X} {hotSpot.Y}, {ToKeyword(fallback)}"; - } - - private static string ToKeyword(StandardCursorType type) => type switch - { - StandardCursorType.Hand => "pointer", - StandardCursorType.Cross => "crosshair", - StandardCursorType.Help => "help", - StandardCursorType.Ibeam => "text", - StandardCursorType.No => "not-allowed", - StandardCursorType.None => "none", - StandardCursorType.Wait => "progress", - StandardCursorType.AppStarting => "wait", - - StandardCursorType.DragMove => "move", - StandardCursorType.DragCopy => "copy", - StandardCursorType.DragLink => "alias", - - StandardCursorType.UpArrow => "default",/*not found matching one*/ - StandardCursorType.SizeWestEast => "ew-resize", - StandardCursorType.SizeNorthSouth => "ns-resize", - StandardCursorType.SizeAll => "move", - - StandardCursorType.TopSide => "n-resize", - StandardCursorType.BottomSide => "s-resize", - StandardCursorType.LeftSide => "w-resize", - StandardCursorType.RightSide => "e-resize", - StandardCursorType.TopLeftCorner => "nw-resize", - StandardCursorType.TopRightCorner => "ne-resize", - StandardCursorType.BottomLeftCorner => "sw-resize", - StandardCursorType.BottomRightCorner => "se-resize", - - _ => Default, - }; - - public void Dispose() {} - } - - internal class CssCursorFactory : ICursorFactory - { - public ICursorImpl CreateCursor(IBitmapImpl cursor, PixelPoint hotSpot) - { - using var imageStream = new MemoryStream(); - cursor.Save(imageStream); - - //not memory optimized because CryptoStream with ToBase64Transform is not supported in the browser. - var base64String = Convert.ToBase64String(imageStream.ToArray()); - return new CssCursor(base64String, "png", hotSpot, StandardCursorType.Arrow); - } - - ICursorImpl ICursorFactory.GetCursor(StandardCursorType cursorType) - { - return new CssCursor(cursorType); - } - } -} - diff --git a/src/Web/Avalonia.Web.Blazor/IBlazorSkiaSurface.cs b/src/Web/Avalonia.Web.Blazor/IBlazorSkiaSurface.cs deleted file mode 100644 index 5463893e27..0000000000 --- a/src/Web/Avalonia.Web.Blazor/IBlazorSkiaSurface.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Avalonia.Web.Blazor -{ - internal interface IBlazorSkiaSurface - { - public PixelSize Size { get; set; } - - public double Scaling { get; set; } - } -} diff --git a/src/Web/Avalonia.Web.Blazor/Interop/ActionHelper.cs b/src/Web/Avalonia.Web.Blazor/Interop/ActionHelper.cs deleted file mode 100644 index 8bb266a942..0000000000 --- a/src/Web/Avalonia.Web.Blazor/Interop/ActionHelper.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System; -using System.ComponentModel; -using Microsoft.JSInterop; - -namespace Avalonia.Web.Blazor.Interop -{ - [EditorBrowsable(EditorBrowsableState.Never)] - public class ActionHelper - { - private readonly Action action; - - public ActionHelper(Action action) - { - this.action = action; - } - - [JSInvokable] - public void Invoke() => action?.Invoke(); - } - - [EditorBrowsable(EditorBrowsableState.Never)] - public class ActionHelper - { - private readonly Action action; - - public ActionHelper(Action action) - { - this.action = action; - } - - [JSInvokable] - public void Invoke(T param1) => action?.Invoke(param1); - } - - [EditorBrowsable(EditorBrowsableState.Never)] - public class ActionHelper - { - private readonly Action action; - - public ActionHelper(Action action) - { - this.action = action; - } - - [JSInvokable] - public void Invoke(T1 p1, T2 p2) => action?.Invoke(p1, p2); - } - - [EditorBrowsable(EditorBrowsableState.Never)] - public class ActionHelper - { - private readonly Action action; - - public ActionHelper(Action action) - { - this.action = action; - } - - [JSInvokable] - public void Invoke(T1 p1, T2 p2, T3 p3) => action?.Invoke(p1, p2, p3); - } - - [EditorBrowsable(EditorBrowsableState.Never)] - public class ActionHelper - { - private readonly Action action; - - public ActionHelper(Action action) - { - this.action = action; - } - - [JSInvokable] - public void Invoke(T1 p1, T2 p2, T3 p3, T4 p4) => action?.Invoke(p1, p2, p3, p4); - } - - - - - -} diff --git a/src/Web/Avalonia.Web.Blazor/Interop/AvaloniaModule.cs b/src/Web/Avalonia.Web.Blazor/Interop/AvaloniaModule.cs deleted file mode 100644 index ff13e95aa7..0000000000 --- a/src/Web/Avalonia.Web.Blazor/Interop/AvaloniaModule.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Microsoft.JSInterop; - -namespace Avalonia.Web.Blazor.Interop -{ - internal class AvaloniaModule : JSModuleInterop - { - private AvaloniaModule(IJSRuntime js) : base(js, "./_content/Avalonia.Web.Blazor/Avalonia.js") - { - } - - public static async Task ImportAsync(IJSRuntime js) - { - var interop = new AvaloniaModule(js); - await interop.ImportAsync(); - return interop; - } - } -} diff --git a/src/Web/Avalonia.Web.Blazor/Interop/DpiWatcherInterop.cs b/src/Web/Avalonia.Web.Blazor/Interop/DpiWatcherInterop.cs deleted file mode 100644 index c86c72f29c..0000000000 --- a/src/Web/Avalonia.Web.Blazor/Interop/DpiWatcherInterop.cs +++ /dev/null @@ -1,72 +0,0 @@ -using Microsoft.JSInterop; - -namespace Avalonia.Web.Blazor.Interop -{ - internal class DpiWatcherInterop : IDisposable - { - private const string StartSymbol = "DpiWatcher.start"; - private const string StopSymbol = "DpiWatcher.stop"; - private const string GetDpiSymbol = "DpiWatcher.getDpi"; - - private event Action? callbacksEvent; - private readonly ActionHelper _callbackHelper; - private readonly AvaloniaModule _module; - - private DotNetObjectReference>? callbackReference; - - public DpiWatcherInterop(AvaloniaModule module, Action? callback = null) - { - _module = module; - _callbackHelper = new ActionHelper((o, n) => callbacksEvent?.Invoke(n)); - - if (callback != null) - Subscribe(callback); - } - - public void Dispose() => Stop(); - - public void Subscribe(Action callback) - { - var shouldStart = callbacksEvent == null; - - callbacksEvent += callback; - - var dpi = shouldStart - ? Start() - : GetDpi(); - - callback(dpi); - } - - public void Unsubscribe(Action callback) - { - callbacksEvent -= callback; - - if (callbacksEvent == null) - Stop(); - } - - private double Start() - { - if (callbackReference != null) - return GetDpi(); - - callbackReference = DotNetObjectReference.Create(_callbackHelper); - - return _module.Invoke(StartSymbol, callbackReference); - } - - private void Stop() - { - if (callbackReference == null) - return; - - _module.Invoke(StopSymbol); - - callbackReference?.Dispose(); - callbackReference = null; - } - - public double GetDpi() => _module.Invoke(GetDpiSymbol); - } -} diff --git a/src/Web/Avalonia.Web.Blazor/Interop/FocusHelperInterop.cs b/src/Web/Avalonia.Web.Blazor/Interop/FocusHelperInterop.cs deleted file mode 100644 index 090909f98f..0000000000 --- a/src/Web/Avalonia.Web.Blazor/Interop/FocusHelperInterop.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Microsoft.AspNetCore.Components; - -namespace Avalonia.Web.Blazor.Interop; - -internal class FocusHelperInterop -{ - private const string FocusSymbol = "FocusHelper.focus"; - private const string SetCursorSymbol = "FocusHelper.setCursor"; - - private readonly AvaloniaModule _module; - private readonly ElementReference _inputElement; - - public FocusHelperInterop(AvaloniaModule module, ElementReference inputElement) - { - _module = module; - _inputElement = inputElement; - } - - public void Focus() => _module.Invoke(FocusSymbol, _inputElement); - - public void SetCursor(string kind) => _module.Invoke(SetCursorSymbol, _inputElement, kind); -} diff --git a/src/Web/Avalonia.Web.Blazor/Interop/InputHelperInterop.cs b/src/Web/Avalonia.Web.Blazor/Interop/InputHelperInterop.cs deleted file mode 100644 index 8872339f91..0000000000 --- a/src/Web/Avalonia.Web.Blazor/Interop/InputHelperInterop.cs +++ /dev/null @@ -1,130 +0,0 @@ -using Microsoft.AspNetCore.Components; -using Microsoft.JSInterop; - -namespace Avalonia.Web.Blazor.Interop -{ - internal class WebCompositionEventArgs : EventArgs - { - public enum WebCompositionEventType - { - Start, - Update, - End - } - - public WebCompositionEventArgs(string type, string data) - { - Type = type switch - { - "compositionstart" => WebCompositionEventType.Start, - "compositionupdate" => WebCompositionEventType.Update, - "compositionend" => WebCompositionEventType.End, - _ => Type - }; - - Data = data; - } - - public WebCompositionEventType Type { get; } - - public string Data { get; } - } - - internal class WebInputEventArgs - { - public WebInputEventArgs(string type, string data) - { - Type = type; - Data = data; - } - - public string Type { get; } - - public string Data { get; } - - public bool Handled { get; set; } - } - - internal class InputHelperInterop - { - private const string ClearSymbol = "InputHelper.clear"; - private const string FocusSymbol = "InputHelper.focus"; - private const string SetCursorSymbol = "InputHelper.setCursor"; - private const string HideSymbol = "InputHelper.hide"; - private const string ShowSymbol = "InputHelper.show"; - private const string StartSymbol = "InputHelper.start"; - private const string SetSurroundingTextSymbol = "InputHelper.setSurroundingText"; - private const string SetBoundsSymbol = "InputHelper.setBounds"; - - private readonly AvaloniaModule _module; - private readonly ElementReference _inputElement; - private readonly ActionHelper _compositionAction; - private readonly ActionHelper _inputAction; - - private DotNetObjectReference>? compositionActionReference; - private DotNetObjectReference>? inputActionReference; - - public InputHelperInterop(AvaloniaModule module, ElementReference inputElement) - { - _module = module; - _inputElement = inputElement; - - _compositionAction = new ActionHelper(OnCompositionEvent); - _inputAction = new ActionHelper(OnInputEvent); - - Start(); - } - - public event EventHandler? CompositionEvent; - public event EventHandler? InputEvent; - - private void OnCompositionEvent(string type, string data) - { - Console.WriteLine($"CompositionEvent Handler Helper {CompositionEvent == null} "); - CompositionEvent?.Invoke(this, new WebCompositionEventArgs(type, data)); - } - - private void OnInputEvent(string type, string data) - { - Console.WriteLine($"InputEvent Handler Helper {InputEvent == null} "); - - var args = new WebInputEventArgs(type, data); - - InputEvent?.Invoke(this, args); - } - - public void Clear() => _module.Invoke(ClearSymbol, _inputElement); - - public void Focus() => _module.Invoke(FocusSymbol, _inputElement); - - public void SetCursor(string kind) => _module.Invoke(SetCursorSymbol, _inputElement, kind); - - public void Hide() => _module.Invoke(HideSymbol, _inputElement); - - public void Show() => _module.Invoke(ShowSymbol, _inputElement); - - private void Start() - { - if(compositionActionReference != null) - { - return; - } - - compositionActionReference = DotNetObjectReference.Create(_compositionAction); - - inputActionReference = DotNetObjectReference.Create(_inputAction); - - _module.Invoke(StartSymbol, _inputElement, compositionActionReference, inputActionReference); - } - - public void SetSurroundingText(string text, int start, int end) - { - _module.Invoke(SetSurroundingTextSymbol, _inputElement, text, start, end); - } - - public void SetBounds(PixelRect bounds, int caret) - { - _module.Invoke(SetBoundsSymbol, _inputElement, bounds.X, bounds.Y, bounds.Width, bounds.Height, caret); - } - } -} diff --git a/src/Web/Avalonia.Web.Blazor/Interop/JSModuleInterop.cs b/src/Web/Avalonia.Web.Blazor/Interop/JSModuleInterop.cs deleted file mode 100644 index dca1b53650..0000000000 --- a/src/Web/Avalonia.Web.Blazor/Interop/JSModuleInterop.cs +++ /dev/null @@ -1,46 +0,0 @@ -using Microsoft.JSInterop; - -namespace Avalonia.Web.Blazor.Interop -{ - internal class JSModuleInterop : IDisposable - { - private readonly Task moduleTask; - private IJSUnmarshalledObjectReference? module; - - public JSModuleInterop(IJSRuntime js, string filename) - { - if (js is not IJSInProcessRuntime) - throw new NotSupportedException("SkiaSharp currently only works on Web Assembly."); - - moduleTask = js.InvokeAsync("import", filename).AsTask(); - } - - public async Task ImportAsync() - { - module = await moduleTask; - } - - public void Dispose() - { - OnDisposingModule(); - Module.Dispose(); - } - - protected IJSUnmarshalledObjectReference Module => - module ?? throw new InvalidOperationException("Make sure to run ImportAsync() first."); - - internal void Invoke(string identifier, params object?[]? args) => - Module.InvokeVoid(identifier, args); - - internal TValue Invoke(string identifier, params object?[]? args) => - Module.Invoke(identifier, args); - - internal ValueTask InvokeAsync(string identifier, params object?[]? args) => - Module.InvokeVoidAsync(identifier, args); - - internal ValueTask InvokeAsync(string identifier, params object?[]? args) => - Module.InvokeAsync(identifier, args); - - protected virtual void OnDisposingModule() { } - } -} diff --git a/src/Web/Avalonia.Web.Blazor/Interop/NativeControlHostImpl.cs b/src/Web/Avalonia.Web.Blazor/Interop/NativeControlHostImpl.cs deleted file mode 100644 index b824fcae46..0000000000 --- a/src/Web/Avalonia.Web.Blazor/Interop/NativeControlHostImpl.cs +++ /dev/null @@ -1,144 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -using Avalonia.Controls.Platform; -using Avalonia.Platform; - -using Microsoft.AspNetCore.Components; -using Microsoft.JSInterop; - -namespace Avalonia.Web.Blazor.Interop -{ - - internal class NativeControlHostInterop : INativeControlHostImpl - { - private const string CreateDefaultChildSymbol = "NativeControlHost.CreateDefaultChild"; - private const string CreateAttachmentSymbol = "NativeControlHost.CreateAttachment"; - private const string GetReferenceSymbol = "NativeControlHost.GetReference"; - - private readonly AvaloniaModule _module; - private readonly ElementReference _hostElement; - - public NativeControlHostInterop(AvaloniaModule module, ElementReference element) - { - _module = module; - _hostElement = element; - } - - public INativeControlHostDestroyableControlHandle CreateDefaultChild(IPlatformHandle parent) - { - var element = _module.Invoke(CreateDefaultChildSymbol); - return new JSObjectControlHandle(element); - } - - public INativeControlHostControlTopLevelAttachment CreateNewAttachment(Func create) - { - Attachment? a = null; - try - { - using var hostElementJsReference = _module.Invoke(GetReferenceSymbol, _hostElement); - var child = create(new JSObjectControlHandle(hostElementJsReference)); - var attachmenetReference = _module.Invoke(CreateAttachmentSymbol); - // It has to be assigned to the variable before property setter is called so we dispose it on exception -#pragma warning disable IDE0017 // Simplify object initialization - a = new Attachment(attachmenetReference, child); -#pragma warning restore IDE0017 // Simplify object initialization - a.AttachedTo = this; - return a; - } - catch - { - a?.Dispose(); - throw; - } - } - - public INativeControlHostControlTopLevelAttachment CreateNewAttachment(IPlatformHandle handle) - { - var attachmenetReference = _module.Invoke(CreateAttachmentSymbol); - var a = new Attachment(attachmenetReference, handle); - a.AttachedTo = this; - return a; - } - - public bool IsCompatibleWith(IPlatformHandle handle) => handle is JSObjectControlHandle; - - class Attachment : INativeControlHostControlTopLevelAttachment - { - private const string InitializeWithChildHandleSymbol = "InitializeWithChildHandle"; - private const string AttachToSymbol = "AttachTo"; - private const string ShowInBoundsSymbol = "ShowInBounds"; - private const string HideWithSizeSymbol = "HideWithSize"; - private const string ReleaseChildSymbol = "ReleaseChild"; - - private IJSInProcessObjectReference? _native; - private NativeControlHostInterop? _attachedTo; - - public Attachment(IJSInProcessObjectReference native, IPlatformHandle handle) - { - _native = native; - _native.InvokeVoid(InitializeWithChildHandleSymbol, ((JSObjectControlHandle)handle).Object); - } - - public void Dispose() - { - if (_native != null) - { - _native.InvokeVoid(ReleaseChildSymbol); - _native.Dispose(); - _native = null; - } - } - - public INativeControlHostImpl? AttachedTo - { - get => _attachedTo!; - set - { - CheckDisposed(); - - var host = (NativeControlHostInterop?)value; - if (host == null) - { - _native.InvokeVoid(AttachToSymbol); - } - else - { - _native.InvokeVoid(AttachToSymbol, host._hostElement); - } - _attachedTo = host; - } - } - - public bool IsCompatibleWith(INativeControlHostImpl host) => host is NativeControlHostInterop; - - public void HideWithSize(Size size) - { - CheckDisposed(); - if (_attachedTo == null) - return; - - _native.InvokeVoid(HideWithSizeSymbol, Math.Max(1, (float)size.Width), Math.Max(1, (float)size.Height)); - } - - public void ShowInBounds(Rect bounds) - { - CheckDisposed(); - - if (_attachedTo == null) - throw new InvalidOperationException("Native control isn't attached to a toplevel"); - - bounds = new Rect(bounds.X, bounds.Y, Math.Max(1, bounds.Width), - Math.Max(1, bounds.Height)); - - _native.InvokeVoid(ShowInBoundsSymbol, (float)bounds.X, (float)bounds.Y, (float)bounds.Width, (float)bounds.Height); - } - - [MemberNotNull(nameof(_native))] - private void CheckDisposed() - { - if (_native == null) - throw new ObjectDisposedException(nameof(Attachment)); - } - } - } -} diff --git a/src/Web/Avalonia.Web.Blazor/Interop/SKHtmlCanvasInterop.cs b/src/Web/Avalonia.Web.Blazor/Interop/SKHtmlCanvasInterop.cs deleted file mode 100644 index cf9350fb62..0000000000 --- a/src/Web/Avalonia.Web.Blazor/Interop/SKHtmlCanvasInterop.cs +++ /dev/null @@ -1,76 +0,0 @@ -using Microsoft.AspNetCore.Components; -using Microsoft.JSInterop; -using SkiaSharp; - -namespace Avalonia.Web.Blazor.Interop -{ - internal class SKHtmlCanvasInterop : IDisposable - { - private const string JsFilename = "./_content/Avalonia.Web.Blazor/SKHtmlCanvas.js"; - private const string InitGLSymbol = "SKHtmlCanvas.initGL"; - private const string InitRasterSymbol = "SKHtmlCanvas.initRaster"; - private const string DeinitSymbol = "SKHtmlCanvas.deinit"; - private const string RequestAnimationFrameSymbol = "SKHtmlCanvas.requestAnimationFrame"; - private const string SetCanvasSizeSymbol = "SKHtmlCanvas.setCanvasSize"; - private const string PutImageDataSymbol = "SKHtmlCanvas.putImageData"; - - private readonly AvaloniaModule _module; - private readonly ElementReference _htmlCanvas; - private readonly string _htmlElementId; - private readonly ActionHelper _callbackHelper; - - private DotNetObjectReference? callbackReference; - - public SKHtmlCanvasInterop(AvaloniaModule module, ElementReference element, Action renderFrameCallback) - { - _module = module; - _htmlCanvas = element; - _htmlElementId = element.Id; - - _callbackHelper = new ActionHelper(renderFrameCallback); - } - - public void Dispose() => Deinit(); - - public GLInfo InitGL() - { - if (callbackReference != null) - throw new InvalidOperationException("Unable to initialize the same canvas more than once."); - - callbackReference = DotNetObjectReference.Create(_callbackHelper); - - return _module.Invoke(InitGLSymbol, _htmlCanvas, _htmlElementId, callbackReference); - } - - public bool InitRaster() - { - if (callbackReference != null) - throw new InvalidOperationException("Unable to initialize the same canvas more than once."); - - callbackReference = DotNetObjectReference.Create(_callbackHelper); - - return _module.Invoke(InitRasterSymbol, _htmlCanvas, _htmlElementId, callbackReference); - } - - public void Deinit() - { - if (callbackReference == null) - return; - - _module.Invoke(DeinitSymbol, _htmlElementId); - - callbackReference?.Dispose(); - } - - public void RequestAnimationFrame(bool enableRenderLoop) => - _module.Invoke(RequestAnimationFrameSymbol, _htmlCanvas, enableRenderLoop); - - public void SetCanvasSize(int rawWidth, int rawHeight) => - _module.Invoke(SetCanvasSizeSymbol, _htmlCanvas, rawWidth, rawHeight); - - public void PutImageData(IntPtr intPtr, SKSizeI rawSize) => - _module.Invoke(PutImageDataSymbol, _htmlCanvas, intPtr.ToInt64(), rawSize.Width, rawSize.Height); - - public record GLInfo(int ContextId, uint FboId, int Stencils, int Samples, int Depth); - } -} diff --git a/src/Web/Avalonia.Web.Blazor/Interop/SizeWatcherInterop.cs b/src/Web/Avalonia.Web.Blazor/Interop/SizeWatcherInterop.cs deleted file mode 100644 index e21ae837d0..0000000000 --- a/src/Web/Avalonia.Web.Blazor/Interop/SizeWatcherInterop.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Microsoft.AspNetCore.Components; -using Microsoft.JSInterop; -using SkiaSharp; - -namespace Avalonia.Web.Blazor.Interop -{ - internal class SizeWatcherInterop : IDisposable - { - private const string ObserveSymbol = "SizeWatcher.observe"; - private const string UnobserveSymbol = "SizeWatcher.unobserve"; - - private readonly AvaloniaModule _module; - private readonly ElementReference _htmlElement; - private readonly string _htmlElementId; - private readonly ActionHelper _callbackHelper; - - private DotNetObjectReference>? callbackReference; - - public SizeWatcherInterop(AvaloniaModule module, ElementReference element, Action callback) - { - _module = module; - _htmlElement = element; - _htmlElementId = element.Id; - _callbackHelper = new ActionHelper((x, y) => callback(new SKSize(x, y))); - } - - public void Dispose() => Stop(); - - public void Start() - { - if (callbackReference != null) - return; - - callbackReference = DotNetObjectReference.Create(_callbackHelper); - - _module.Invoke(ObserveSymbol, _htmlElement, _htmlElementId, callbackReference); - } - - public void Stop() - { - if (callbackReference == null) - return; - - _module.Invoke(UnobserveSymbol, _htmlElementId); - - callbackReference?.Dispose(); - callbackReference = null; - } - } -} diff --git a/src/Web/Avalonia.Web.Blazor/Interop/Storage/StorageProviderInterop.cs b/src/Web/Avalonia.Web.Blazor/Interop/Storage/StorageProviderInterop.cs deleted file mode 100644 index 690d9683df..0000000000 --- a/src/Web/Avalonia.Web.Blazor/Interop/Storage/StorageProviderInterop.cs +++ /dev/null @@ -1,225 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -using Avalonia.Platform.Storage; - -using Microsoft.JSInterop; - -namespace Avalonia.Web.Blazor.Interop.Storage -{ - internal record FilePickerAcceptType(string Description, IReadOnlyDictionary> Accept); - - internal record FileProperties(ulong Size, long LastModified, string? Type); - - internal class StorageProviderInterop : JSModuleInterop, IStorageProvider - { - private const string JsFilename = "./_content/Avalonia.Web.Blazor/Storage.js"; - private const string PickerCancelMessage = "The user aborted a request"; - - public static async Task ImportAsync(IJSRuntime js) - { - var interop = new StorageProviderInterop(js); - await interop.ImportAsync(); - return interop; - } - - public StorageProviderInterop(IJSRuntime js) - : base(js, JsFilename) - { - } - - public bool CanOpen => Invoke("StorageProvider.canOpen"); - public bool CanSave => Invoke("StorageProvider.canSave"); - public bool CanPickFolder => Invoke("StorageProvider.canPickFolder"); - - public async Task> OpenFilePickerAsync(FilePickerOpenOptions options) - { - try - { - var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle; - - var (types, exludeAll) = ConvertFileTypes(options.FileTypeFilter); - var items = await InvokeAsync("StorageProvider.openFileDialog", startIn, options.AllowMultiple, types, exludeAll); - var count = items.Invoke("count"); - - return Enumerable.Range(0, count) - .Select(index => new JSStorageFile(items.Invoke("at", index))) - .ToArray(); - } - catch (JSException ex) when (ex.Message.Contains(PickerCancelMessage, StringComparison.Ordinal)) - { - return Array.Empty(); - } - } - - public async Task SaveFilePickerAsync(FilePickerSaveOptions options) - { - try - { - var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle; - - var (types, exludeAll) = ConvertFileTypes(options.FileTypeChoices); - var item = await InvokeAsync("StorageProvider.saveFileDialog", startIn, options.SuggestedFileName, types, exludeAll); - - return item is not null ? new JSStorageFile(item) : null; - } - catch (JSException ex) when (ex.Message.Contains(PickerCancelMessage, StringComparison.Ordinal)) - { - return null; - } - } - - public async Task> OpenFolderPickerAsync(FolderPickerOpenOptions options) - { - try - { - var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle; - - var item = await InvokeAsync("StorageProvider.selectFolderDialog", startIn); - - return item is not null ? new[] { new JSStorageFolder(item) } : Array.Empty(); - } - catch (JSException ex) when (ex.Message.Contains(PickerCancelMessage, StringComparison.Ordinal)) - { - return Array.Empty(); - } - } - - public async Task OpenFileBookmarkAsync(string bookmark) - { - var item = await InvokeAsync("StorageProvider.openBookmark", bookmark); - return item is not null ? new JSStorageFile(item) : null; - } - - public async Task OpenFolderBookmarkAsync(string bookmark) - { - var item = await InvokeAsync("StorageProvider.openBookmark", bookmark); - return item is not null ? new JSStorageFolder(item) : null; - } - - private static (FilePickerAcceptType[]? types, bool excludeAllOption) ConvertFileTypes(IEnumerable? input) - { - var types = input? - .Where(t => t.MimeTypes?.Any() == true && t != FilePickerFileTypes.All) - .Select(t => new FilePickerAcceptType(t.Name, t.MimeTypes! - .ToDictionary(m => m, _ => (IReadOnlyList)Array.Empty()))) - .ToArray(); - if (types?.Length == 0) - { - types = null; - } - - var inlcudeAll = input?.Contains(FilePickerFileTypes.All) == true || types is null; - - return (types, !inlcudeAll); - } - } - - internal abstract class JSStorageItem : IStorageBookmarkItem - { - internal IJSInProcessObjectReference? _fileHandle; - - protected JSStorageItem(IJSInProcessObjectReference fileHandle) - { - _fileHandle = fileHandle ?? throw new ArgumentNullException(nameof(fileHandle)); - } - - internal IJSInProcessObjectReference FileHandle => _fileHandle ?? throw new ObjectDisposedException(nameof(JSStorageItem)); - - public string Name => FileHandle.Invoke("getName"); - - public bool TryGetUri([NotNullWhen(true)] out Uri? uri) - { - uri = new Uri(Name, UriKind.Relative); - return false; - } - - public async Task GetBasicPropertiesAsync() - { - var properties = await FileHandle.InvokeAsync("getProperties"); - - return new StorageItemProperties( - properties?.Size, - dateCreated: null, - dateModified: properties?.LastModified > 0 ? DateTimeOffset.FromUnixTimeMilliseconds(properties.LastModified) : null); - } - - public bool CanBookmark => true; - - public Task SaveBookmarkAsync() - { - return FileHandle.InvokeAsync("saveBookmark").AsTask(); - } - - public Task GetParentAsync() - { - return Task.FromResult(null); - } - - public Task ReleaseBookmarkAsync() - { - return FileHandle.InvokeAsync("deleteBookmark").AsTask(); - } - - public void Dispose() - { - _fileHandle?.Dispose(); - _fileHandle = null; - } - } - - internal class JSStorageFile : JSStorageItem, IStorageBookmarkFile - { - public JSStorageFile(IJSInProcessObjectReference fileHandle) : base(fileHandle) - { - } - - public bool CanOpenRead => true; - public async Task OpenReadAsync() - { - var stream = await FileHandle.InvokeAsync("openRead"); - // Remove maxAllowedSize limit, as developer can decide if they read only small part or everything. - return await stream.OpenReadStreamAsync(long.MaxValue, CancellationToken.None); - } - - public bool CanOpenWrite => true; - public async Task OpenWriteAsync() - { - var properties = await FileHandle.InvokeAsync("getProperties"); - var streamWriter = await FileHandle.InvokeAsync("openWrite"); - - return new JSWriteableStream(streamWriter, (long)(properties?.Size ?? 0)); - } - } - - internal class JSStorageFolder : JSStorageItem, IStorageBookmarkFolder - { - public JSStorageFolder(IJSInProcessObjectReference fileHandle) : base(fileHandle) - { - } - - public async Task> GetItemsAsync() - { - var items = await FileHandle.InvokeAsync("getItems"); - if (items is null) - { - return Array.Empty(); - } - - var count = items.Invoke("count"); - - return Enumerable.Range(0, count) - .Select(index => - { - var reference = items.Invoke("at", index); - return reference.Invoke("getKind") switch - { - "directory" => (IStorageItem)new JSStorageFolder(reference), - "file" => new JSStorageFile(reference), - _ => null - }; - }) - .Where(i => i is not null) - .ToArray()!; - } - } -} diff --git a/src/Web/Avalonia.Web.Blazor/Interop/Storage/WriteableStream.cs b/src/Web/Avalonia.Web.Blazor/Interop/Storage/WriteableStream.cs deleted file mode 100644 index 55a7831b1a..0000000000 --- a/src/Web/Avalonia.Web.Blazor/Interop/Storage/WriteableStream.cs +++ /dev/null @@ -1,124 +0,0 @@ -using System.Buffers; -using System.Text.Json.Serialization; - -using Microsoft.JSInterop; - -namespace Avalonia.Web.Blazor.Interop.Storage -{ - // Loose wrapper implementaion of a stream on top of FileAPI FileSystemWritableFileStream - internal sealed class JSWriteableStream : Stream - { - private IJSInProcessObjectReference? _jSReference; - - // Unfortunatelly we can't read current length/position, so we need to keep it C#-side only. - private long _length, _position; - - internal JSWriteableStream(IJSInProcessObjectReference jSReference, long initialLength) - { - _jSReference = jSReference; - _length = initialLength; - } - - private IJSInProcessObjectReference JSReference => _jSReference ?? throw new ObjectDisposedException(nameof(JSWriteableStream)); - - public override bool CanRead => false; - - public override bool CanSeek => true; - - public override bool CanWrite => true; - - public override long Length => _length; - - public override long Position - { - get => _position; - set => Seek(_position, SeekOrigin.Begin); - } - - public override void Flush() - { - // no-op - } - - public override int Read(byte[] buffer, int offset, int count) - { - throw new NotSupportedException(); - } - - public override long Seek(long offset, SeekOrigin origin) - { - var position = origin switch - { - SeekOrigin.Current => _position + offset, - SeekOrigin.End => _length + offset, - _ => offset - }; - JSReference.InvokeVoid("seek", position); - return position; - } - - public override void SetLength(long value) - { - _length = value; - - // See https://docs.w3cub.com/dom/filesystemwritablefilestream/truncate - // If the offset is smaller than the size, it remains unchanged. If the offset is larger than size, the offset is set to that size - if (_position > _length) - { - _position = _length; - } - - JSReference.InvokeVoid("truncate", value); - } - - public override void Write(byte[] buffer, int offset, int count) - { - throw new NotSupportedException("Synchronous writes are not supported."); - } - - public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - if (offset != 0 || count != buffer.Length) - { - // TODO, we need to pass prepared buffer to the JS - // Can't use ArrayPool as it can return bigger array than requested - // Can't use Span/Memory, as it's not supported by JS interop yet. - // Alternatively we can pass original buffer and offset+count, so it can be trimmed on the JS side (but is it more efficient tho?) - buffer = buffer.AsMemory(offset, count).ToArray(); - } - return WriteAsyncInternal(buffer, cancellationToken).AsTask(); - } - - public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) - { - return WriteAsyncInternal(buffer.ToArray(), cancellationToken); - } - - private ValueTask WriteAsyncInternal(byte[] buffer, CancellationToken _) - { - _position += buffer.Length; - - return JSReference.InvokeVoidAsync("write", buffer); - } - - protected override void Dispose(bool disposing) - { - if (_jSReference is { } jsReference) - { - _jSReference = null; - jsReference.InvokeVoid("close"); - jsReference.Dispose(); - } - } - - public override async ValueTask DisposeAsync() - { - if (_jSReference is { } jsReference) - { - _jSReference = null; - await jsReference.InvokeVoidAsync("close"); - await jsReference.DisposeAsync(); - } - } - } -} diff --git a/src/Web/Avalonia.Web.Blazor/JSObjectControlHandle.cs b/src/Web/Avalonia.Web.Blazor/JSObjectControlHandle.cs deleted file mode 100644 index 4426c3fbd7..0000000000 --- a/src/Web/Avalonia.Web.Blazor/JSObjectControlHandle.cs +++ /dev/null @@ -1,35 +0,0 @@ -#nullable enable -using Avalonia.Controls.Platform; - -using Microsoft.JSInterop; - -namespace Avalonia.Web.Blazor -{ - public class JSObjectControlHandle : INativeControlHostDestroyableControlHandle - { - internal const string ElementReferenceDescriptor = "JSObjectReference"; - - public JSObjectControlHandle(IJSObjectReference reference) - { - Object = reference; - } - - public IJSObjectReference Object { get; } - - public IntPtr Handle => throw new NotSupportedException(); - - public string? HandleDescriptor => ElementReferenceDescriptor; - - public void Destroy() - { - if (Object is IJSInProcessObjectReference inProcess) - { - inProcess.Dispose(); - } - else - { - _ = Object.DisposeAsync(); - } - } - } -} diff --git a/src/Web/Avalonia.Web.Blazor/Keycodes.cs b/src/Web/Avalonia.Web.Blazor/Keycodes.cs deleted file mode 100644 index ea30f0a9f0..0000000000 --- a/src/Web/Avalonia.Web.Blazor/Keycodes.cs +++ /dev/null @@ -1,127 +0,0 @@ -using Avalonia.Input; - -namespace Avalonia.Web.Blazor -{ - internal static class Keycodes - { - public static Dictionary KeyCodes = new() - { - { "Escape", Key.Escape }, - { "Digit1", Key.D1 }, - { "Digit2", Key.D2 }, - { "Digit3", Key.D3 }, - { "Digit4", Key.D4 }, - { "Digit5", Key.D5 }, - { "Digit6", Key.D6 }, - { "Digit7", Key.D7 }, - { "Digit8", Key.D8 }, - { "Digit9", Key.D9 }, - { "Digit0", Key.D0 }, - { "Minus", Key.OemMinus }, - //{ "Equal" , Key. }, - { "Backspace", Key.Back }, - { "Tab", Key.Tab }, - { "KeyQ", Key.Q }, - { "KeyW", Key.W }, - { "KeyE", Key.E }, - { "KeyR", Key.R }, - { "KeyT", Key.T }, - { "KeyY", Key.Y }, - { "KeyU", Key.U }, - { "KeyI", Key.I }, - { "KeyO", Key.O }, - { "KeyP", Key.P }, - { "BracketLeft", Key.OemOpenBrackets }, - { "BracketRight", Key.OemCloseBrackets }, - { "Enter", Key.Enter }, - { "ControlLeft", Key.LeftCtrl }, - { "KeyA", Key.A }, - { "KeyS", Key.S }, - { "KeyD", Key.D }, - { "KeyF", Key.F }, - { "KeyG", Key.G }, - { "KeyH", Key.H }, - { "KeyJ", Key.J }, - { "KeyK", Key.K }, - { "KeyL", Key.L }, - { "Semicolon", Key.OemSemicolon }, - { "Quote", Key.OemQuotes }, - //{ "Backquote" , Key. }, - { "ShiftLeft", Key.LeftShift }, - { "Backslash", Key.OemBackslash }, - { "KeyZ", Key.Z }, - { "KeyX", Key.X }, - { "KeyC", Key.C }, - { "KeyV", Key.V }, - { "KeyB", Key.B }, - { "KeyN", Key.N }, - { "KeyM", Key.M }, - { "Comma", Key.OemComma }, - { "Period", Key.OemPeriod }, - //{ "Slash" , Key. }, - { "ShiftRight", Key.RightShift }, - { "NumpadMultiply", Key.Multiply }, - { "AltLeft", Key.LeftAlt }, - { "Space", Key.Space }, - { "CapsLock", Key.CapsLock }, - { "F1", Key.F1 }, - { "F2", Key.F2 }, - { "F3", Key.F3 }, - { "F4", Key.F4 }, - { "F5", Key.F5 }, - { "F6", Key.F6 }, - { "F7", Key.F7 }, - { "F8", Key.F8 }, - { "F9", Key.F9 }, - { "F10", Key.F10 }, - { "NumLock", Key.NumLock }, - { "ScrollLock", Key.Scroll }, - { "Numpad7", Key.NumPad7 }, - { "Numpad8", Key.NumPad8 }, - { "Numpad9", Key.NumPad9 }, - { "NumpadSubtract", Key.Subtract }, - { "Numpad4", Key.NumPad4 }, - { "Numpad5", Key.NumPad5 }, - { "Numpad6", Key.NumPad6 }, - { "NumpadAdd", Key.Add }, - { "Numpad1", Key.NumPad1 }, - { "Numpad2", Key.NumPad2 }, - { "Numpad3", Key.NumPad3 }, - { "Numpad0", Key.NumPad0 }, - { "NumpadDecimal", Key.Decimal }, - { "Unidentified", Key.NoName }, - //{ "IntlBackslash" , Key.bac }, - { "F11", Key.F11 }, - { "F12", Key.F12 }, - //{ "IntlRo" , Key.Ro }, - //{ "Unidentified" , Key. }, - { "Convert", Key.ImeConvert }, - { "KanaMode", Key.KanaMode }, - { "NonConvert", Key.ImeNonConvert }, - //{ "Unidentified" , Key. }, - { "NumpadEnter", Key.Enter }, - { "ControlRight", Key.RightCtrl }, - { "NumpadDivide", Key.Divide }, - { "PrintScreen", Key.PrintScreen }, - { "AltRight", Key.RightAlt }, - //{ "Unidentified" , Key. }, - { "Home", Key.Home }, - { "ArrowUp", Key.Up }, - { "PageUp", Key.PageUp }, - { "ArrowLeft", Key.Left }, - { "ArrowRight", Key.Right }, - { "End", Key.End }, - { "ArrowDown", Key.Down }, - { "PageDown", Key.PageDown }, - { "Insert", Key.Insert }, - { "Delete", Key.Delete }, - //{ "Unidentified" , Key. }, - { "AudioVolumeMute", Key.VolumeMute }, - { "AudioVolumeDown", Key.VolumeDown }, - { "AudioVolumeUp", Key.VolumeUp }, - //{ "NumpadEqual" , Key. }, - { "Pause", Key.Pause }, - { "NumpadComma", Key.OemComma } - }; - } -} diff --git a/src/Web/Avalonia.Web.Blazor/ManualTriggerRenderTimer.cs b/src/Web/Avalonia.Web.Blazor/ManualTriggerRenderTimer.cs deleted file mode 100644 index 7b9feab2e3..0000000000 --- a/src/Web/Avalonia.Web.Blazor/ManualTriggerRenderTimer.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Diagnostics; -using Avalonia.Rendering; - -namespace Avalonia.Web.Blazor -{ - public class ManualTriggerRenderTimer : IRenderTimer - { - private static readonly Stopwatch s_sw = Stopwatch.StartNew(); - - public static ManualTriggerRenderTimer Instance { get; } = new(); - - public void RaiseTick() => Tick?.Invoke(s_sw.Elapsed); - - public event Action? Tick; - public bool RunsInBackground => false; - } -} diff --git a/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs b/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs deleted file mode 100644 index 3a09c16932..0000000000 --- a/src/Web/Avalonia.Web.Blazor/RazorViewTopLevelImpl.cs +++ /dev/null @@ -1,222 +0,0 @@ -using System.Diagnostics; -using Avalonia.Controls; -using Avalonia.Controls.Platform; -using Avalonia.Input; -using Avalonia.Input.Raw; -using Avalonia.Input.TextInput; -using Avalonia.Platform; -using Avalonia.Platform.Storage; -using Avalonia.Rendering; -using Avalonia.Rendering.Composition; -using Avalonia.Web.Blazor.Interop; -using SkiaSharp; - -#nullable enable - -namespace Avalonia.Web.Blazor -{ - internal class RazorViewTopLevelImpl : ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost, ITopLevelImplWithStorageProvider - { - private Size _clientSize; - private IBlazorSkiaSurface? _currentSurface; - private IInputRoot? _inputRoot; - private readonly Stopwatch _sw = Stopwatch.StartNew(); - private readonly AvaloniaView _avaloniaView; - private readonly TouchDevice _touchDevice; - private readonly PenDevice _penDevice; - private string _currentCursor = CssCursor.Default; - - public RazorViewTopLevelImpl(AvaloniaView avaloniaView) - { - _avaloniaView = avaloniaView; - TransparencyLevel = WindowTransparencyLevel.None; - AcrylicCompensationLevels = new AcrylicPlatformCompensationLevels(1, 1, 1); - _touchDevice = new TouchDevice(); - _penDevice = new PenDevice(); - } - - public ulong Timestamp => (ulong)_sw.ElapsedMilliseconds; - - - internal void SetSurface(GRContext context, SKHtmlCanvasInterop.GLInfo glInfo, SKColorType colorType, PixelSize size, double scaling) - { - _currentSurface = - new BlazorSkiaSurface(context, glInfo, colorType, size, scaling, GRSurfaceOrigin.BottomLeft); - } - - internal void SetSurface(SKColorType colorType, PixelSize size, double scaling, Action blitCallback) - { - _currentSurface = new BlazorSkiaRasterSurface(colorType, size, scaling, blitCallback); - } - - public void SetClientSize(SKSize size, double dpi) - { - var newSize = new Size(size.Width, size.Height); - - if (Math.Abs(RenderScaling - dpi) > 0.0001) - { - if (_currentSurface is { }) - { - _currentSurface.Scaling = dpi; - } - - ScalingChanged?.Invoke(dpi); - } - - if (newSize != _clientSize) - { - _clientSize = newSize; - - if (_currentSurface is { }) - { - _currentSurface.Size = new PixelSize((int)size.Width, (int)size.Height); - } - - Resized?.Invoke(newSize, PlatformResizeReason.User); - } - } - - public void RawPointerEvent( - RawPointerEventType eventType, string pointerType, - RawPointerPoint p, RawInputModifiers modifiers, long touchPointId) - { - if (_inputRoot is { } - && Input is { } input) - { - var device = GetPointerDevice(pointerType); - var args = device is TouchDevice ? - new RawTouchEventArgs(device, Timestamp, _inputRoot, eventType, p, modifiers, touchPointId) : - new RawPointerEventArgs(device, Timestamp, _inputRoot, eventType, p, modifiers) - { - RawPointerId = touchPointId - }; - - input.Invoke(args); - } - } - - private IPointerDevice GetPointerDevice(string pointerType) - { - return pointerType switch - { - "touch" => _touchDevice, - "pen" => _penDevice, - _ => MouseDevice - }; - } - - public void RawMouseWheelEvent(Point p, Vector v, RawInputModifiers modifiers) - { - if (_inputRoot is { }) - { - Input?.Invoke(new RawMouseWheelEventArgs(MouseDevice, Timestamp, _inputRoot, p, v, modifiers)); - } - } - - public bool RawKeyboardEvent(RawKeyEventType type, string code, string key, RawInputModifiers modifiers) - { - if (Keycodes.KeyCodes.TryGetValue(code, out var avkey)) - { - if (_inputRoot is { }) - { - var args = new RawKeyEventArgs(KeyboardDevice, Timestamp, _inputRoot, type, avkey, modifiers); - - Input?.Invoke(args); - - return args.Handled; - } - } - else if (Keycodes.KeyCodes.TryGetValue(key, out avkey)) - { - if (_inputRoot is { }) - { - var args = new RawKeyEventArgs(KeyboardDevice, Timestamp, _inputRoot, type, avkey, modifiers); - - Input?.Invoke(args); - - return args.Handled; - } - } - - return false; - } - - public void RawTextEvent(string text) - { - if (_inputRoot is { }) - { - Input?.Invoke(new RawTextInputEventArgs(KeyboardDevice, Timestamp, _inputRoot, text)); - } - } - - public void Dispose() - { - - } - - public IRenderer CreateRenderer(IRenderRoot root) - { - var loop = AvaloniaLocator.Current.GetRequiredService(); - return new CompositingRenderer(root, new Compositor(loop, null)); - } - - public void Invalidate(Rect rect) - { - //Console.WriteLine("invalidate rect called"); - } - - public void SetInputRoot(IInputRoot inputRoot) - { - _inputRoot = inputRoot; - } - - public Point PointToClient(PixelPoint point) => new Point(point.X, point.Y); - - public PixelPoint PointToScreen(Point point) => new PixelPoint((int)point.X, (int)point.Y); - - public void SetCursor(ICursorImpl? cursor) - { - var val = (cursor as CssCursor)?.Value ?? CssCursor.Default; - if (_currentCursor != val) - { - SetCssCursor?.Invoke(val); - _currentCursor = val; - } - } - - public IPopupImpl? CreatePopup() - { - return null; - } - - public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) - { - - } - - public Size ClientSize => _clientSize; - public Size? FrameSize => null; - public double RenderScaling => _currentSurface?.Scaling ?? 1; - - public IEnumerable Surfaces => new object[] { _currentSurface! }; - - public Action? SetCssCursor { get; set; } - public Action? Input { get; set; } - public Action? Paint { get; set; } - public Action? Resized { get; set; } - public Action? ScalingChanged { get; set; } - public Action? TransparencyLevelChanged { get; set; } - public Action? Closed { get; set; } - public Action? LostFocus { get; set; } - public IMouseDevice MouseDevice { get; } = new MouseDevice(); - - public IKeyboardDevice KeyboardDevice { get; } = BlazorWindowingPlatform.Keyboard; - public WindowTransparencyLevel TransparencyLevel { get; } - public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } - - public ITextInputMethodImpl TextInputMethod => _avaloniaView; - - public INativeControlHostImpl? NativeControlHost => _avaloniaView.GetNativeControlHostImpl(); - public IStorageProvider StorageProvider => _avaloniaView.GetStorageProvider(); - } -} diff --git a/src/Web/Avalonia.Web.Blazor/WinStubs.cs b/src/Web/Avalonia.Web.Blazor/WinStubs.cs deleted file mode 100644 index 808d1526d3..0000000000 --- a/src/Web/Avalonia.Web.Blazor/WinStubs.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Avalonia.Controls; -using Avalonia.Controls.Platform; -using Avalonia.Input; -using Avalonia.Input.Platform; -using Avalonia.Platform; - -#nullable enable - -namespace Avalonia.Web.Blazor -{ - internal class IconLoaderStub : IPlatformIconLoader - { - private class IconStub : IWindowIconImpl - { - public void Save(Stream outputStream) - { - - } - } - - public IWindowIconImpl LoadIcon(string fileName) => new IconStub(); - - public IWindowIconImpl LoadIcon(Stream stream) => new IconStub(); - - public IWindowIconImpl LoadIcon(IBitmapImpl bitmap) => new IconStub(); - } - - internal class ScreenStub : IScreenImpl - { - public int ScreenCount => 1; - - public IReadOnlyList AllScreens { get; } = - new[] { new Screen(96, new PixelRect(0, 0, 4000, 4000), new PixelRect(0, 0, 4000, 4000), true) }; - - public Screen? ScreenFromPoint(PixelPoint point) - { - return ScreenHelper.ScreenFromPoint(point, AllScreens); - } - - public Screen? ScreenFromRect(PixelRect rect) - { - return ScreenHelper.ScreenFromRect(rect, AllScreens); - } - - public Screen? ScreenFromWindow(IWindowBaseImpl window) - { - return ScreenHelper.ScreenFromWindow(window, AllScreens); - } - } -} diff --git a/src/Web/Avalonia.Web.Blazor/WindowingPlatform.cs b/src/Web/Avalonia.Web.Blazor/WindowingPlatform.cs deleted file mode 100644 index 46c05d8e8c..0000000000 --- a/src/Web/Avalonia.Web.Blazor/WindowingPlatform.cs +++ /dev/null @@ -1,106 +0,0 @@ -using Avalonia.Controls.Platform; -using Avalonia.Input; -using Avalonia.Input.Platform; -using Avalonia.Platform; -using Avalonia.Rendering; -using Avalonia.Threading; - -#nullable enable - -namespace Avalonia.Web.Blazor -{ - public class BlazorWindowingPlatform : IWindowingPlatform, IPlatformSettings, IPlatformThreadingInterface - { - private bool _signaled; - private static KeyboardDevice? s_keyboard; - - public IWindowImpl CreateWindow() => throw new NotSupportedException(); - - IWindowImpl IWindowingPlatform.CreateEmbeddableWindow() - { - throw new NotImplementedException(); - } - - public ITrayIconImpl? CreateTrayIcon() - { - return null; - } - - public static KeyboardDevice Keyboard => s_keyboard ?? - throw new InvalidOperationException("BlazorWindowingPlatform not registered."); - - public static void Register() - { - var instance = new BlazorWindowingPlatform(); - s_keyboard = new KeyboardDevice(); - AvaloniaLocator.CurrentMutable - .Bind().ToSingleton() - .Bind().ToSingleton() - .Bind().ToConstant(s_keyboard) - .Bind().ToConstant(instance) - .Bind().ToConstant(instance) - .Bind().ToConstant(new RenderLoop()) - .Bind().ToConstant(ManualTriggerRenderTimer.Instance) - .Bind().ToConstant(instance) - .Bind().ToSingleton() - .Bind().ToSingleton(); - } - - public Size DoubleClickSize { get; } = new Size(2, 2); - - public TimeSpan DoubleClickTime { get; } = TimeSpan.FromMilliseconds(500); - - public Size TouchDoubleClickSize => new Size(16, 16); - - public TimeSpan TouchDoubleClickTime => DoubleClickTime; - public void RunLoop(CancellationToken cancellationToken) - { - throw new NotSupportedException(); - } - - public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick) - { - return GetRuntimePlatform() - .StartSystemTimer(interval, () => - { - Dispatcher.UIThread.RunJobs(priority); - tick(); - }); - } - - public void Signal(DispatcherPriority priority) - { - if (_signaled) - return; - - _signaled = true; - - IDisposable? disp = null; - - disp = GetRuntimePlatform() - .StartSystemTimer(TimeSpan.FromMilliseconds(1), - () => - { - _signaled = false; - disp?.Dispose(); - - Signaled?.Invoke(null); - }); - } - - public bool CurrentThreadIsLoopThread - { - get - { - return true; // Blazor is single threaded. - } - } - - public event Action? Signaled; - - private static IRuntimePlatform GetRuntimePlatform() - { - return AvaloniaLocator.Current.GetRequiredService(); - } - } -} diff --git a/src/Web/Avalonia.Web.Blazor/_Imports.razor b/src/Web/Avalonia.Web.Blazor/_Imports.razor deleted file mode 100644 index 77285129da..0000000000 --- a/src/Web/Avalonia.Web.Blazor/_Imports.razor +++ /dev/null @@ -1 +0,0 @@ -@using Microsoft.AspNetCore.Components.Web diff --git a/src/Web/Avalonia.Web.Blazor/webapp/build.js b/src/Web/Avalonia.Web.Blazor/webapp/build.js deleted file mode 100644 index 9d585dcade..0000000000 --- a/src/Web/Avalonia.Web.Blazor/webapp/build.js +++ /dev/null @@ -1,16 +0,0 @@ -require("esbuild").build({ - entryPoints: [ - "./modules/Avalonia.ts", - "./modules/Storage.ts" - ], - outdir: "../wwwroot", - bundle: true, - minify: true, - format: "esm", - target: "es2016", - platform: "browser", - sourcemap: "linked", - loader: {".ts": "ts"} - }) - .then(() => console.log("⚡ Done")) - .catch(() => process.exit(1)); \ No newline at end of file diff --git a/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia.ts b/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia.ts deleted file mode 100644 index 369f628a44..0000000000 --- a/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia.ts +++ /dev/null @@ -1,7 +0,0 @@ -export { DpiWatcher } from "./Avalonia/DpiWatcher" -export { InputHelper } from "./Avalonia/InputHelper" -export { FocusHelper } from "./Avalonia/FocusHelper" -export { NativeControlHost } from "./Avalonia/NativeControlHost" -export { SizeWatcher } from "./Avalonia/SizeWatcher" -export { SKHtmlCanvas } from "./Avalonia/SKHtmlCanvas" -export { CaretHelper } from "./Avalonia/CaretHelper" diff --git a/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/CaretHelper.ts b/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/CaretHelper.ts deleted file mode 100644 index 5709854087..0000000000 --- a/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/CaretHelper.ts +++ /dev/null @@ -1,149 +0,0 @@ -// Based on https://github.com/component/textarea-caret-position/blob/master/index.js -export class CaretHelper { - public static getCaretCoordinates( - element: HTMLInputElement | HTMLTextAreaElement, - position: number, - options?: { debug: boolean } - ) { - if (!isBrowser) { - throw new Error( - "textarea-caret-position#getCaretCoordinates should only be called in a browser" - ); - } - - const debug = (options && options.debug) || false; - if (debug) { - const el = document.querySelector( - "#input-textarea-caret-position-mirror-div" - ); - if (el) el.parentNode?.removeChild(el); - } - - // The mirror div will replicate the textarea's style - const div = document.createElement("div"); - div.id = "input-textarea-caret-position-mirror-div"; - document.body.appendChild(div); - - const style = div.style; - const computed = window.getComputedStyle - ? window.getComputedStyle(element) - : ((element as any)["currentStyle"] as CSSStyleDeclaration); // currentStyle for IE < 9 - const isInput = element.nodeName === "INPUT"; - - // Default textarea styles - style.whiteSpace = "pre-wrap"; - if (!isInput) style.wordWrap = "break-word"; // only for textarea-s - - // Position off-screen - style.position = "absolute"; // required to return coordinates properly - if (!debug) style.visibility = "hidden"; // not 'display: none' because we want rendering - - // Transfer the element's properties to the div - properties.forEach((prop: string) => { - if (isInput && prop === "lineHeight") { - // Special case for s because text is rendered centered and line height may be != height - if (computed.boxSizing === "border-box") { - const height = parseInt(computed.height); - const outerHeight = - parseInt(computed.paddingTop) + - parseInt(computed.paddingBottom) + - parseInt(computed.borderTopWidth) + - parseInt(computed.borderBottomWidth); - const targetHeight = outerHeight + parseInt(computed.lineHeight); - if (height > targetHeight) { - style.lineHeight = height - outerHeight + "px"; - } else if (height === targetHeight) { - style.lineHeight = computed.lineHeight; - } else { - style.lineHeight = "0"; - } - } else { - style.lineHeight = computed.height; - } - } else { - (style as any)[prop] = (computed as any)[prop]; - } - }); - - if (isFirefox) { - // Firefox lies about the overflow property for textareas: https://bugzilla.mozilla.org/show_bug.cgi?id=984275 - if (element.scrollHeight > parseInt(computed.height)) - style.overflowY = "scroll"; - } else { - style.overflow = "hidden"; // for Chrome to not render a scrollbar; IE keeps overflowY = 'scroll' - } - - div.textContent = element.value.substring(0, position); - // The second special handling for input type="text" vs textarea: - // spaces need to be replaced with non-breaking spaces - http://stackoverflow.com/a/13402035/1269037 - if (isInput) div.textContent = div.textContent.replace(/\s/g, "\u00a0"); - - const span = document.createElement("span"); - // Wrapping must be replicated *exactly*, including when a long word gets - // onto the next line, with whitespace at the end of the line before (#7). - // The *only* reliable way to do that is to copy the *entire* rest of the - // textarea's content into the created at the caret position. - // For inputs, just '.' would be enough, but no need to bother. - span.textContent = element.value.substring(position) || "."; // || because a completely empty faux span doesn't render at all - div.appendChild(span); - - const coordinates = { - top: span.offsetTop + parseInt(computed["borderTopWidth"]), - left: span.offsetLeft + parseInt(computed["borderLeftWidth"]), - height: parseInt(computed["lineHeight"]), - }; - - if (debug) { - span.style.backgroundColor = "#aaa"; - } else { - document.body.removeChild(div); - } - - return coordinates; - } -} - - -var properties = [ - "direction", // RTL support - "boxSizing", - "width", // on Chrome and IE, exclude the scrollbar, so the mirror div wraps exactly as the textarea does - "height", - "overflowX", - "overflowY", // copy the scrollbar for IE - - "borderTopWidth", - "borderRightWidth", - "borderBottomWidth", - "borderLeftWidth", - "borderStyle", - - "paddingTop", - "paddingRight", - "paddingBottom", - "paddingLeft", - - // https://developer.mozilla.org/en-US/docs/Web/CSS/font - "fontStyle", - "fontVariant", - "fontWeight", - "fontStretch", - "fontSize", - "fontSizeAdjust", - "lineHeight", - "fontFamily", - - "textAlign", - "textTransform", - "textIndent", - "textDecoration", // might not make a difference, but better be safe - - "letterSpacing", - "wordSpacing", - - "tabSize", - "MozTabSize", -]; - -const isBrowser = typeof window !== "undefined"; -const isFirefox = isBrowser && (window as any).mozInnerScreenX != null; diff --git a/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/DpiWatcher.ts b/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/DpiWatcher.ts deleted file mode 100644 index 06235782f8..0000000000 --- a/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/DpiWatcher.ts +++ /dev/null @@ -1,40 +0,0 @@ -export class DpiWatcher { - static lastDpi: number; - static timerId: number; - static callback?: DotNet.DotNetObject; - - public static getDpi() { - return window.devicePixelRatio; - } - - public static start(callback: DotNet.DotNetObject): number { - //console.info(`Starting DPI watcher with callback ${callback._id}...`); - - DpiWatcher.lastDpi = window.devicePixelRatio; - DpiWatcher.timerId = window.setInterval(DpiWatcher.update, 1000); - DpiWatcher.callback = callback; - - return DpiWatcher.lastDpi; - } - - public static stop() { - //console.info(`Stopping DPI watcher with callback ${DpiWatcher.callback._id}...`); - - window.clearInterval(DpiWatcher.timerId); - - DpiWatcher.callback = undefined; - } - - static update() { - if (!DpiWatcher.callback) - return; - - const currentDpi = window.devicePixelRatio; - const lastDpi = DpiWatcher.lastDpi; - DpiWatcher.lastDpi = currentDpi; - - if (Math.abs(lastDpi - currentDpi) > 0.001) { - DpiWatcher.callback.invokeMethod('Invoke', lastDpi, currentDpi); - } - } -} diff --git a/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/FocusHelper.ts b/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/FocusHelper.ts deleted file mode 100644 index 96ffee3d53..0000000000 --- a/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/FocusHelper.ts +++ /dev/null @@ -1,9 +0,0 @@ -export class FocusHelper { - public static focus(inputElement: HTMLElement) { - inputElement.focus(); - } - - public static setCursor(inputElement: HTMLInputElement, kind: string) { - inputElement.style.cursor = kind; - } -} diff --git a/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/InputHelper.ts b/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/InputHelper.ts deleted file mode 100644 index 1ea85d84e3..0000000000 --- a/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/InputHelper.ts +++ /dev/null @@ -1,86 +0,0 @@ -import {CaretHelper} from "./CaretHelper"; - -export class InputHelper { - static inputCallback?: DotNet.DotNetObject; - static compositionCallback?: DotNet.DotNetObject - - public static start(inputElement: HTMLInputElement, compositionCallback: DotNet.DotNetObject, inputCallback: DotNet.DotNetObject) - { - InputHelper.compositionCallback = compositionCallback; - - inputElement.addEventListener('compositionstart', InputHelper.onCompositionEvent); - inputElement.addEventListener('compositionupdate', InputHelper.onCompositionEvent); - inputElement.addEventListener('compositionend', InputHelper.onCompositionEvent); - - InputHelper.inputCallback = inputCallback; - - inputElement.addEventListener('input', InputHelper.onInputEvent); - } - - public static clear(inputElement: HTMLInputElement) { - inputElement.value = ""; - } - public static focus(inputElement: HTMLInputElement) { - inputElement.focus(); - } - - public static setCursor(inputElement: HTMLInputElement, kind: string) { - inputElement.style.cursor = kind; - } - - public static setBounds(inputElement: HTMLInputElement, x: number, y: number, caretWidth: number, caretHeight: number, caret: number) { - inputElement.style.left = (x).toFixed(0) + "px"; - inputElement.style.top = (y).toFixed(0) + "px"; - - let {height, left, top} = CaretHelper.getCaretCoordinates(inputElement, caret); - - inputElement.style.left = (x - left).toFixed(0) + "px"; - inputElement.style.top = (y - top).toFixed(0) + "px"; - } - - public static hide(inputElement: HTMLInputElement) { - inputElement.style.display = 'none'; - } - - public static show(inputElement: HTMLInputElement) { - inputElement.style.display = 'block'; - } - - public static setSurroundingText(inputElement: HTMLInputElement, text: string, start: number, end: number) { - if (!inputElement) { - return; - } - - inputElement.value = text; - inputElement.setSelectionRange(start, end); - inputElement.style.width = "20px"; - inputElement.style.width = inputElement.scrollWidth + "px"; - } - - private static onCompositionEvent(ev: CompositionEvent) - { - if(!InputHelper.compositionCallback) - return; - - switch (ev.type) - { - case "compositionstart": - case "compositionupdate": - case "compositionend": - InputHelper.compositionCallback.invokeMethod('Invoke', ev.type, ev.data); - break; - } - } - - private static onInputEvent(ev: Event) { - if (!InputHelper.inputCallback) - return; - - var inputEvent = ev as InputEvent; - - InputHelper.inputCallback.invokeMethod('Invoke', ev.type, inputEvent.data); - } -} - - - diff --git a/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/NativeControlHost.ts b/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/NativeControlHost.ts deleted file mode 100644 index 9e5c3843c8..0000000000 --- a/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/NativeControlHost.ts +++ /dev/null @@ -1,61 +0,0 @@ -export class NativeControlHost { - public static CreateDefaultChild(parent: HTMLElement): HTMLElement { - return document.createElement("div"); - } - - // Used to convert ElementReference to JSObjectReference. - // Is there a better way? - public static GetReference(element: Element): Element { - return element; - } - - public static CreateAttachment(): NativeControlHostTopLevelAttachment { - return new NativeControlHostTopLevelAttachment(); - } -} - -class NativeControlHostTopLevelAttachment { - _child?: HTMLElement; - _host?: HTMLElement; - - InitializeWithChildHandle(child: HTMLElement) { - this._child = child; - this._child.style.position = "absolute"; - } - - AttachTo(host: HTMLElement): void { - if (this._host && this._child) { - this._host.removeChild(this._child); - } - - this._host = host; - - if (this._host && this._child) { - this._host.appendChild(this._child); - } - } - - ShowInBounds(x: number, y: number, width: number, height: number): void { - if (this._child) { - this._child.style.top = y + "px"; - this._child.style.left = x + "px"; - this._child.style.width = width + "px"; - this._child.style.height = height + "px"; - this._child.style.display = "block"; - } - } - - HideWithSize(width: number, height: number): void { - if (this._child) { - this._child.style.width = width + "px"; - this._child.style.height = height + "px"; - this._child.style.display = "none"; - } - } - - ReleaseChild(): void { - if (this._child) { - this._child = undefined; - } - } -} diff --git a/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/SKHtmlCanvas.ts b/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/SKHtmlCanvas.ts deleted file mode 100644 index e934f74807..0000000000 --- a/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/SKHtmlCanvas.ts +++ /dev/null @@ -1,255 +0,0 @@ -// aliases for emscripten -declare let GL: any; -declare let GLctx: WebGLRenderingContext; -declare let Module: EmscriptenModule; - -// container for gl info -type SKGLViewInfo = { - context: WebGLRenderingContext | WebGL2RenderingContext | undefined; - fboId: number; - stencil: number; - sample: number; - depth: number; -} - -// alias for a potential skia html canvas -type SKHtmlCanvasElement = { - SKHtmlCanvas: SKHtmlCanvas | undefined -} & HTMLCanvasElement - -export class SKHtmlCanvas { - static elements: Map; - - htmlCanvas: HTMLCanvasElement; - glInfo?: SKGLViewInfo; - renderFrameCallback: DotNet.DotNetObject; - renderLoopEnabled: boolean = false; - renderLoopRequest: number = 0; - newWidth?: number; - newHeight?: number; - - public static initGL(element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObject): SKGLViewInfo | null { - var view = SKHtmlCanvas.init(true, element, elementId, callback); - if (!view || !view.glInfo) - return null; - - return view.glInfo; - } - - public static initRaster(element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObject): boolean { - var view = SKHtmlCanvas.init(false, element, elementId, callback); - if (!view) - return false; - - return true; - } - - static init(useGL: boolean, element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObject): SKHtmlCanvas | null { - var htmlCanvas = element as SKHtmlCanvasElement; - if (!htmlCanvas) { - console.error(`No canvas element was provided.`); - return null; - } - - if (!SKHtmlCanvas.elements) - SKHtmlCanvas.elements = new Map(); - SKHtmlCanvas.elements.set(elementId, element); - - const view = new SKHtmlCanvas(useGL, element, callback); - - htmlCanvas.SKHtmlCanvas = view; - - return view; - } - - public static deinit(elementId: string) { - if (!elementId) - return; - - const element = SKHtmlCanvas.elements.get(elementId); - SKHtmlCanvas.elements.delete(elementId); - - const htmlCanvas = element as SKHtmlCanvasElement; - if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas) - return; - - htmlCanvas.SKHtmlCanvas.deinit(); - htmlCanvas.SKHtmlCanvas = undefined; - } - - public static requestAnimationFrame(element: HTMLCanvasElement, renderLoop?: boolean) { - const htmlCanvas = element as SKHtmlCanvasElement; - if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas) - return; - - htmlCanvas.SKHtmlCanvas.requestAnimationFrame(renderLoop); - } - - public static setCanvasSize(element: HTMLCanvasElement, width: number, height: number) { - const htmlCanvas = element as SKHtmlCanvasElement; - if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas) - return; - - htmlCanvas.SKHtmlCanvas.setCanvasSize(width, height); - } - - public static setEnableRenderLoop(element: HTMLCanvasElement, enable: boolean) { - const htmlCanvas = element as SKHtmlCanvasElement; - if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas) - return; - - htmlCanvas.SKHtmlCanvas.setEnableRenderLoop(enable); - } - - public static putImageData(element: HTMLCanvasElement, pData: number, width: number, height: number) { - const htmlCanvas = element as SKHtmlCanvasElement; - if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas) - return; - - htmlCanvas.SKHtmlCanvas.putImageData(pData, width, height); - } - - public constructor(useGL: boolean, element: HTMLCanvasElement, callback: DotNet.DotNetObject) { - this.htmlCanvas = element; - this.renderFrameCallback = callback; - - if (useGL) { - const ctx = SKHtmlCanvas.createWebGLContext(this.htmlCanvas); - if (!ctx) { - console.error(`Failed to create WebGL context: err ${ctx}`); - return; - } - - // make current - GL.makeContextCurrent(ctx); - - // read values - const fbo = GLctx.getParameter(GLctx.FRAMEBUFFER_BINDING); - this.glInfo = { - context: ctx, - fboId: fbo ? fbo.id : 0, - stencil: GLctx.getParameter(GLctx.STENCIL_BITS), - sample: 0, // TODO: GLctx.getParameter(GLctx.SAMPLES) - depth: GLctx.getParameter(GLctx.DEPTH_BITS), - }; - } - } - - public deinit() { - this.setEnableRenderLoop(false); - } - - public setCanvasSize(width: number, height: number) { - this.newWidth = width; - this.newHeight = height; - - if (this.htmlCanvas.width != this.newWidth) { - this.htmlCanvas.width = this.newWidth; - } - - if (this.htmlCanvas.height != this.newHeight) { - this.htmlCanvas.height = this.newHeight; - } - - if (this.glInfo) { - // make current - GL.makeContextCurrent(this.glInfo.context); - } - } - - public requestAnimationFrame(renderLoop?: boolean) { - // optionally update the render loop - if (renderLoop !== undefined && this.renderLoopEnabled !== renderLoop) - this.setEnableRenderLoop(renderLoop); - - // skip because we have a render loop - if (this.renderLoopRequest !== 0) - return; - - // add the draw to the next frame - this.renderLoopRequest = window.requestAnimationFrame(() => { - if (this.glInfo) { - // make current - GL.makeContextCurrent(this.glInfo.context); - } - - if (this.htmlCanvas.width != this.newWidth) { - this.htmlCanvas.width = this.newWidth || 0; - } - - if (this.htmlCanvas.height != this.newHeight) { - this.htmlCanvas.height = this.newHeight || 0; - } - - this.renderFrameCallback.invokeMethod('Invoke'); - this.renderLoopRequest = 0; - - // we may want to draw the next frame - if (this.renderLoopEnabled) - this.requestAnimationFrame(); - }); - } - - public setEnableRenderLoop(enable: boolean) { - this.renderLoopEnabled = enable; - - // either start the new frame or cancel the existing one - if (enable) { - //console.info(`Enabling render loop with callback ${this.renderFrameCallback._id}...`); - this.requestAnimationFrame(); - } else if (this.renderLoopRequest !== 0) { - window.cancelAnimationFrame(this.renderLoopRequest); - this.renderLoopRequest = 0; - } - } - - public putImageData(pData: number, width: number, height: number): boolean { - if (this.glInfo || !pData || width <= 0 || width <= 0) - return false; - - var ctx = this.htmlCanvas.getContext('2d'); - if (!ctx) { - console.error(`Failed to obtain 2D canvas context.`); - return false; - } - - // make sure the canvas is scaled correctly for the drawing - this.htmlCanvas.width = width; - this.htmlCanvas.height = height; - - // set the canvas to be the bytes - var buffer = new Uint8ClampedArray(Module.HEAPU8.buffer, pData, width * height * 4); - var imageData = new ImageData(buffer, width, height); - ctx.putImageData(imageData, 0, 0); - - return true; - } - - static createWebGLContext(htmlCanvas: HTMLCanvasElement): WebGLRenderingContext | WebGL2RenderingContext { - const contextAttributes = { - alpha: 1, - depth: 1, - stencil: 8, - antialias: 0, - premultipliedAlpha: 1, - preserveDrawingBuffer: 0, - preferLowPowerToHighPerformance: 0, - failIfMajorPerformanceCaveat: 0, - majorVersion: 2, - minorVersion: 0, - enableExtensionsByDefault: 1, - explicitSwapControl: 0, - renderViaOffscreenBackBuffer: 1, - }; - - let ctx: WebGLRenderingContext = GL.createContext(htmlCanvas, contextAttributes); - if (!ctx && contextAttributes.majorVersion > 1) { - console.warn('Falling back to WebGL 1.0'); - contextAttributes.majorVersion = 1; - contextAttributes.minorVersion = 0; - ctx = GL.createContext(htmlCanvas, contextAttributes); - } - - return ctx; - } -} diff --git a/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/SizeWatcher.ts b/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/SizeWatcher.ts deleted file mode 100644 index 715b252988..0000000000 --- a/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/SizeWatcher.ts +++ /dev/null @@ -1,67 +0,0 @@ -type SizeWatcherElement = { - SizeWatcher: SizeWatcherInstance; -} & HTMLElement - -type SizeWatcherInstance = { - callback: DotNet.DotNetObject; -} - -export class SizeWatcher { - static observer: ResizeObserver; - static elements: Map; - - public static observe(element: HTMLElement, elementId: string, callback: DotNet.DotNetObject) { - if (!element || !callback) - return; - - //console.info(`Adding size watcher observation with callback ${callback._id}...`); - - SizeWatcher.init(); - - const watcherElement = element as SizeWatcherElement; - watcherElement.SizeWatcher = { - callback: callback - }; - - SizeWatcher.elements.set(elementId, element); - SizeWatcher.observer.observe(element); - - SizeWatcher.invoke(element); - } - - public static unobserve(elementId: string) { - if (!elementId || !SizeWatcher.observer) - return; - - //console.info('Removing size watcher observation...'); - - const element = SizeWatcher.elements.get(elementId)!; - - SizeWatcher.elements.delete(elementId); - SizeWatcher.observer.unobserve(element); - } - - static init() { - if (SizeWatcher.observer) - return; - - //console.info('Starting size watcher...'); - - SizeWatcher.elements = new Map(); - SizeWatcher.observer = new ResizeObserver((entries) => { - for (let entry of entries) { - SizeWatcher.invoke(entry.target); - } - }); - } - - static invoke(element: Element) { - const watcherElement = element as SizeWatcherElement; - const instance = watcherElement.SizeWatcher; - - if (!instance || !instance.callback) - return; - - return instance.callback.invokeMethod('Invoke', element.clientWidth, element.clientHeight); - } -} diff --git a/src/Web/Avalonia.Web.Blazor/webapp/modules/Storage.ts b/src/Web/Avalonia.Web.Blazor/webapp/modules/Storage.ts deleted file mode 100644 index 2c4654e81b..0000000000 --- a/src/Web/Avalonia.Web.Blazor/webapp/modules/Storage.ts +++ /dev/null @@ -1 +0,0 @@ -export { StorageProvider } from "./Storage/StorageProvider" diff --git a/src/Web/Avalonia.Web.Blazor/webapp/modules/Storage/IndexedDbWrapper.ts b/src/Web/Avalonia.Web.Blazor/webapp/modules/Storage/IndexedDbWrapper.ts deleted file mode 100644 index 2eaa8de2fe..0000000000 --- a/src/Web/Avalonia.Web.Blazor/webapp/modules/Storage/IndexedDbWrapper.ts +++ /dev/null @@ -1,79 +0,0 @@ -class InnerDbConnection { - constructor(private database: IDBDatabase) { } - - private openStore(store: string, mode: IDBTransactionMode): IDBObjectStore { - const tx = this.database.transaction(store, mode); - return tx.objectStore(store); - } - - public put(store: string, obj: any, key?: IDBValidKey): Promise { - const os = this.openStore(store, "readwrite"); - - return new Promise((resolve, reject) => { - const response = os.put(obj, key); - response.onsuccess = () => { - resolve(response.result); - }; - response.onerror = () => { - reject(response.error); - }; - }); - } - - public get(store: string, key: IDBValidKey): any { - const os = this.openStore(store, "readonly"); - - return new Promise((resolve, reject) => { - const response = os.get(key); - response.onsuccess = () => { - resolve(response.result); - }; - response.onerror = () => { - reject(response.error); - }; - }); - } - - public delete(store: string, key: IDBValidKey): Promise { - const os = this.openStore(store, "readwrite"); - - return new Promise((resolve, reject) => { - const response = os.delete(key); - response.onsuccess = () => { - resolve(); - }; - response.onerror = () => { - reject(response.error); - }; - }); - } - - public close() { - this.database.close(); - } -} - -export class IndexedDbWrapper { - constructor(private databaseName: string, private objectStores: [string]) { - } - - public connect(): Promise { - const conn = window.indexedDB.open(this.databaseName, 1); - - conn.onupgradeneeded = event => { - const db = (>event.target).result; - this.objectStores.forEach(store => { - db.createObjectStore(store); - }); - }; - - return new Promise((resolve, reject) => { - conn.onsuccess = event => { - resolve(new InnerDbConnection((>event.target).result)); - }; - conn.onerror = event => { - reject((>event.target).error); - }; - }); - } -} diff --git a/src/Web/Avalonia.Web.Blazor/webapp/modules/Storage/StorageProvider.ts b/src/Web/Avalonia.Web.Blazor/webapp/modules/Storage/StorageProvider.ts deleted file mode 100644 index 896e174e43..0000000000 --- a/src/Web/Avalonia.Web.Blazor/webapp/modules/Storage/StorageProvider.ts +++ /dev/null @@ -1,204 +0,0 @@ -import { IndexedDbWrapper } from "./IndexedDbWrapper"; - -declare global { - type WellKnownDirectory = "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos"; - type StartInDirectory = WellKnownDirectory | FileSystemHandle; - interface OpenFilePickerOptions { - startIn?: StartInDirectory - } - interface SaveFilePickerOptions { - startIn?: StartInDirectory - } -} - -const fileBookmarksStore: string = "fileBookmarks"; -const avaloniaDb = new IndexedDbWrapper("AvaloniaDb", [ - fileBookmarksStore -]); - -class StorageItem { - constructor(public handle: FileSystemHandle, private bookmarkId?: string) { } - - public getName(): string { - return this.handle.name - } - - public getKind(): string { - return this.handle.kind; - } - - public async openRead(): Promise { - if (!(this.handle instanceof FileSystemFileHandle)) { - throw new Error("StorageItem is not a file"); - } - - await this.verityPermissions('read'); - - const file = await this.handle.getFile(); - return file; - } - - public async openWrite(): Promise { - if (!(this.handle instanceof FileSystemFileHandle)) { - throw new Error("StorageItem is not a file"); - } - - await this.verityPermissions('readwrite'); - - return await this.handle.createWritable({ keepExistingData: true }); - } - - public async getProperties(): Promise<{ Size: number, LastModified: number, Type: string } | null> { - const file = this.handle instanceof FileSystemFileHandle - && await this.handle.getFile(); - - if (!file) { - return null; - } - - return { - Size: file.size, - LastModified: file.lastModified, - Type: file.type - } - } - - public async getItems(): Promise { - if (this.handle.kind !== "directory"){ - return new StorageItems([]); - } - - const items: StorageItem[] = []; - for await (const [key, value] of (this.handle as any).entries()) { - items.push(new StorageItem(value)); - } - return new StorageItems(items); - } - - private async verityPermissions(mode: FileSystemPermissionMode): Promise { - if (await this.handle.queryPermission({ mode }) === 'granted') { - return; - } - - if (await this.handle.requestPermission({ mode }) === "denied") { - throw new Error("Read permissions denied"); - } - } - - public async saveBookmark(): Promise { - // If file was previously bookmarked, just return old one. - if (this.bookmarkId) { - return this.bookmarkId; - } - - const connection = await avaloniaDb.connect(); - try { - const key = await connection.put(fileBookmarksStore, this.handle, this.generateBookmarkId()); - return key; - } - finally { - connection.close(); - } - } - - public async deleteBookmark(): Promise { - if (!this.bookmarkId) { - return; - } - - const connection = await avaloniaDb.connect(); - try { - const key = await connection.delete(fileBookmarksStore, this.bookmarkId); - } - finally { - connection.close(); - } - } - - private generateBookmarkId(): string { - return Date.now().toString(36) + Math.random().toString(36).substring(2); - } -} - -class StorageItems { - constructor(private items: StorageItem[]) { } - - public count(): number { - return this.items.length; - } - - public at(index: number): StorageItem { - return this.items[index]; - } -} - -export class StorageProvider { - - public static canOpen(): boolean { - return typeof window.showOpenFilePicker !== 'undefined'; - } - - public static canSave(): boolean { - return typeof window.showSaveFilePicker !== 'undefined'; - } - - public static canPickFolder(): boolean { - return typeof window.showDirectoryPicker !== 'undefined'; - } - - public static async selectFolderDialog( - startIn: StorageItem | null) - : Promise { - - // 'Picker' API doesn't accept "null" as a parameter, so it should be set to undefined. - const options: DirectoryPickerOptions = { - startIn: (startIn?.handle || undefined) - }; - - const handle = await window.showDirectoryPicker(options); - return new StorageItem(handle); - } - - public static async openFileDialog( - startIn: StorageItem | null, multiple: boolean, - types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean) - : Promise { - - const options: OpenFilePickerOptions = { - startIn: (startIn?.handle || undefined), - multiple, - excludeAcceptAllOption, - types: (types || undefined) - }; - - const handles = await window.showOpenFilePicker(options); - return new StorageItems(handles.map((handle: FileSystemHandle) => new StorageItem(handle))); - } - - public static async saveFileDialog( - startIn: StorageItem | null, suggestedName: string | null, - types: FilePickerAcceptType[] | null, excludeAcceptAllOption: boolean) - : Promise { - - const options: SaveFilePickerOptions = { - startIn: (startIn?.handle || undefined), - suggestedName: (suggestedName || undefined), - excludeAcceptAllOption, - types: (types || undefined) - }; - - const handle = await window.showSaveFilePicker(options); - return new StorageItem(handle); - } - - public static async openBookmark(key: string): Promise { - const connection = await avaloniaDb.connect(); - try { - const handle = await connection.get(fileBookmarksStore, key); - return handle && new StorageItem(handle, key); - } - finally { - connection.close(); - } - } -} diff --git a/src/Web/Avalonia.Web.Blazor/webapp/package.json b/src/Web/Avalonia.Web.Blazor/webapp/package.json deleted file mode 100644 index 27e15b0abd..0000000000 --- a/src/Web/Avalonia.Web.Blazor/webapp/package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "avalonia.web", - "scripts": { - "prebuild": "tsc -noEmit", - "build": "node build.js" - }, - "devDependencies": { - "@types/emscripten": "^1.39.6", - "@types/wicg-file-system-access": "^2020.9.5", - "typescript": "^4.7.4", - "esbuild": "^0.15.7" - } -} diff --git a/src/Web/Avalonia.Web.Blazor/webapp/tsconfig.json b/src/Web/Avalonia.Web.Blazor/webapp/tsconfig.json deleted file mode 100644 index 4e90bc230d..0000000000 --- a/src/Web/Avalonia.Web.Blazor/webapp/tsconfig.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "compilerOptions": { - "target": "es2016", - "strict": true, - "sourceMap": true, - "outDir": "../wwwroot", - "noEmitOnError": true, - "isolatedModules": true, // we need it for esbuild - "lib": [ - "dom", - "es2016", - "esnext.asynciterable" - ] - }, - "exclude": [ - "node_modules" - ] -} diff --git a/src/Web/Avalonia.Web.Blazor/webapp/types/dotnet/index.d.ts b/src/Web/Avalonia.Web.Blazor/webapp/types/dotnet/index.d.ts deleted file mode 100644 index 932dfa1e1f..0000000000 --- a/src/Web/Avalonia.Web.Blazor/webapp/types/dotnet/index.d.ts +++ /dev/null @@ -1,56 +0,0 @@ -// Type definitions for non-npm package @blazor/javascript-interop 3.1 -// Project: https://docs.microsoft.com/en-us/aspnet/core/blazor/javascript-interop?view=aspnetcore-3.1 -// Definitions by: Piotr Błażejewicz (Peter Blazejewicz) -// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped -// Minimum TypeScript Version: 3.0 - -// Here be dragons! -// This is community-maintained definition file intended to ease the process of developing -// high quality JavaScript interop code to be used in Blazor application from your C# .NET code. -// Could be removed without a notice in case official definition types ships with Blazor itself. - -// tslint:disable:no-unnecessary-generics - -declare namespace DotNet { - /** - * Invokes the specified .NET public method synchronously. Not all hosting scenarios support - * synchronous invocation, so if possible use invokeMethodAsync instead. - * - * @param assemblyName The short name (without key/version or .dll extension) of the .NET assembly containing the method. - * @param methodIdentifier The identifier of the method to invoke. The method must have a [JSInvokable] attribute specifying this identifier. - * @param args Arguments to pass to the method, each of which must be JSON-serializable. - * @returns The result of the operation. - */ - function invokeMethod(assemblyName: string, methodIdentifier: string, ...args: any[]): T; - /** - * Invokes the specified .NET public method asynchronously. - * - * @param assemblyName The short name (without key/version or .dll extension) of the .NET assembly containing the method. - * @param methodIdentifier The identifier of the method to invoke. The method must have a [JSInvokable] attribute specifying this identifier. - * @param args Arguments to pass to the method, each of which must be JSON-serializable. - * @returns A promise representing the result of the operation. - */ - function invokeMethodAsync(assemblyName: string, methodIdentifier: string, ...args: any[]): Promise; - /** - * Represents the .NET instance passed by reference to JavaScript. - */ - interface DotNetObject { - /** - * Invokes the specified .NET instance public method synchronously. Not all hosting scenarios support - * synchronous invocation, so if possible use invokeMethodAsync instead. - * - * @param methodIdentifier The identifier of the method to invoke. The method must have a [JSInvokable] attribute specifying this identifier. - * @param args Arguments to pass to the method, each of which must be JSON-serializable. - * @returns The result of the operation. - */ - invokeMethod(methodIdentifier: string, ...args: any[]): T; - /** - * Invokes the specified .NET instance public method asynchronously. - * - * @param methodIdentifier The identifier of the method to invoke. The method must have a [JSInvokable] attribute specifying this identifier. - * @param args Arguments to pass to the method, each of which must be JSON-serializable. - * @returns A promise representing the result of the operation. - */ - invokeMethodAsync(methodIdentifier: string, ...args: any[]): Promise; - } -} diff --git a/src/Web/Avalonia.Web.Sample/main.js b/src/Web/Avalonia.Web.Sample/main.js index 3683aea181..87f8a4f943 100644 --- a/src/Web/Avalonia.Web.Sample/main.js +++ b/src/Web/Avalonia.Web.Sample/main.js @@ -2,7 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. import { dotnet } from './dotnet.js' -import { createAvaloniaRuntime } from './avalonia.js'; +import { registerAvaloniaModule } from './avalonia.js'; const is_browser = typeof window != "undefined"; if (!is_browser) throw new Error(`Expected to be running in a browser`); @@ -12,7 +12,7 @@ const dotnetRuntime = await dotnet .withApplicationArgumentsFromQuery() .create(); -await createAvaloniaRuntime(dotnetRuntime); +await registerAvaloniaModule(dotnetRuntime); const config = dotnetRuntime.getConfig(); diff --git a/src/Web/Avalonia.Web/AvaloniaView.cs b/src/Web/Avalonia.Web/AvaloniaView.cs index e81620ffde..a5da719912 100644 --- a/src/Web/Avalonia.Web/AvaloniaView.cs +++ b/src/Web/Avalonia.Web/AvaloniaView.cs @@ -40,13 +40,12 @@ namespace Avalonia.Web private static int _canvasCount; public AvaloniaView(string divId) + : this(DomHelper.GetElementById(divId) ?? throw new Exception($"Element with id {divId} was not found in the html document.")) { - var host = DomHelper.GetElementById(divId); - if (host == null) - { - throw new Exception($"Element with id {divId} was not found in the html document."); - } + } + public AvaloniaView(JSObject host) + { var hostContent = DomHelper.CreateAvaloniaHost(host); if (hostContent == null) { @@ -137,7 +136,7 @@ namespace Avalonia.Web _topLevelImpl.SetClientSize(_canvasSize, _dpi); - DomHelper.ObserveSize(host, divId, OnSizeChanged); + DomHelper.ObserveSize(host, null, OnSizeChanged); CanvasHelper.RequestAnimationFrame(_canvas, true); } @@ -387,7 +386,7 @@ namespace Avalonia.Web InputHelper.FocusElement(_containerElement); } - public void SetClient(ITextInputMethodClient? client) + void ITextInputMethodImpl.SetClient(ITextInputMethodClient? client) { Console.WriteLine("Set Client"); if (_client != null) @@ -431,18 +430,18 @@ namespace Avalonia.Web } } - public void SetCursorRect(Rect rect) + void ITextInputMethodImpl.SetCursorRect(Rect rect) { InputHelper.FocusElement(_inputElement); InputHelper.SetBounds(_inputElement, (int)rect.X, (int)rect.Y, (int)rect.Width, (int)rect.Height, _client?.SurroundingText.CursorOffset ?? 0); InputHelper.FocusElement(_inputElement); } - public void SetOptions(TextInputOptions options) + void ITextInputMethodImpl.SetOptions(TextInputOptions options) { } - public void Reset() + void ITextInputMethodImpl.Reset() { InputHelper.ClearInputElement(_inputElement); InputHelper.SetSurroundingText(_inputElement, "", 0, 0); diff --git a/src/Web/Avalonia.Web/BrowserSingleViewLifetime.cs b/src/Web/Avalonia.Web/BrowserSingleViewLifetime.cs index 091ab3f68c..d962956567 100644 --- a/src/Web/Avalonia.Web/BrowserSingleViewLifetime.cs +++ b/src/Web/Avalonia.Web/BrowserSingleViewLifetime.cs @@ -4,10 +4,11 @@ using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Media; using Avalonia.Web.Skia; +using System.Runtime.Versioning; namespace Avalonia.Web { - [System.Runtime.Versioning.SupportedOSPlatform("browser")] // gets rid of callsite warnings + [SupportedOSPlatform("browser")] public class BrowserSingleViewLifetime : ISingleViewApplicationLifetime { public AvaloniaView? View; @@ -19,23 +20,32 @@ namespace Avalonia.Web } } + [SupportedOSPlatform("browser")] public static partial class WebAppBuilder { public static T SetupBrowserApp( - this T builder, string mainDivId) - where T : AppBuilderBase, new() + this T builder, string mainDivId) + where T : AppBuilderBase, new() { var lifetime = new BrowserSingleViewLifetime(); return builder - .UseWindowingSubsystem(BrowserWindowingPlatform.Register) - .UseSkia() - .With(new SkiaOptions { CustomGpuFactory = () => new BrowserSkiaGpu() }) + .UseBrowser() .AfterSetup(b => { lifetime.View = new AvaloniaView(mainDivId); }) .SetupWithLifetime(lifetime); } + + public static T UseBrowser( + this T builder) + where T : AppBuilderBase, new() + { + return builder + .UseWindowingSubsystem(BrowserWindowingPlatform.Register) + .UseSkia() + .With(new SkiaOptions { CustomGpuFactory = () => new BrowserSkiaGpu() }); + } } } diff --git a/src/Web/Avalonia.Web/Cursor.cs b/src/Web/Avalonia.Web/Cursor.cs index 5db0bdeda2..af7098f800 100644 --- a/src/Web/Avalonia.Web/Cursor.cs +++ b/src/Web/Avalonia.Web/Cursor.cs @@ -5,7 +5,7 @@ using Avalonia.Platform; namespace Avalonia.Web { - public class CssCursor : ICursorImpl + internal class CssCursor : ICursorImpl { public const string Default = "default"; public string? Value { get; set; } diff --git a/src/Web/Avalonia.Web/Interop/CanvasHelper.cs b/src/Web/Avalonia.Web/Interop/CanvasHelper.cs index 66a1d421b7..ff07d1757b 100644 --- a/src/Web/Avalonia.Web/Interop/CanvasHelper.cs +++ b/src/Web/Avalonia.Web/Interop/CanvasHelper.cs @@ -29,13 +29,13 @@ internal static partial class CanvasHelper return glInfo; } - [JSImport("Canvas.requestAnimationFrame", "avalonia.ts")] + [JSImport("Canvas.requestAnimationFrame", "avalonia")] public static partial void RequestAnimationFrame(JSObject canvas, bool renderLoop); - [JSImport("Canvas.setCanvasSize", "avalonia.ts")] + [JSImport("Canvas.setCanvasSize", "avalonia")] public static partial void SetCanvasSize(JSObject canvas, int height, int width); - [JSImport("Canvas.initGL", "avalonia.ts")] + [JSImport("Canvas.initGL", "avalonia")] private static partial JSObject InitGL( JSObject canvas, string canvasId, diff --git a/src/Web/Avalonia.Web/Interop/DomHelper.cs b/src/Web/Avalonia.Web/Interop/DomHelper.cs index c02bc4dae3..c2cf0a0c44 100644 --- a/src/Web/Avalonia.Web/Interop/DomHelper.cs +++ b/src/Web/Avalonia.Web/Interop/DomHelper.cs @@ -8,20 +8,20 @@ internal static partial class DomHelper [JSImport("globalThis.document.getElementById")] internal static partial JSObject? GetElementById(string id); - [JSImport("AvaloniaDOM.createAvaloniaHost", "avalonia.ts")] + [JSImport("AvaloniaDOM.createAvaloniaHost", "avalonia")] public static partial JSObject CreateAvaloniaHost(JSObject element); - [JSImport("AvaloniaDOM.addClass", "avalonia.ts")] + [JSImport("AvaloniaDOM.addClass", "avalonia")] public static partial void AddCssClass(JSObject element, string className); - [JSImport("SizeWatcher.observe", "avalonia.ts")] + [JSImport("SizeWatcher.observe", "avalonia")] public static partial JSObject ObserveSize( JSObject canvas, - string canvasId, + string? canvasId, [JSMarshalAs>] Action onSizeChanged); - [JSImport("DpiWatcher.start", "avalonia.ts")] + [JSImport("DpiWatcher.start", "avalonia")] public static partial double ObserveDpi( [JSMarshalAs>] Action onDpiChanged); diff --git a/src/Web/Avalonia.Web/Interop/InputHelper.cs b/src/Web/Avalonia.Web/Interop/InputHelper.cs index b32b6566f2..bdd1957e03 100644 --- a/src/Web/Avalonia.Web/Interop/InputHelper.cs +++ b/src/Web/Avalonia.Web/Interop/InputHelper.cs @@ -6,7 +6,7 @@ namespace Avalonia.Web.Interop; internal static partial class InputHelper { - [JSImport("InputHelper.subscribeKeyEvents", "avalonia.ts")] + [JSImport("InputHelper.subscribeKeyEvents", "avalonia")] public static partial void SubscribeKeyEvents( JSObject htmlElement, [JSMarshalAs>] @@ -14,7 +14,7 @@ internal static partial class InputHelper [JSMarshalAs>] Func keyUp); - [JSImport("InputHelper.subscribeTextEvents", "avalonia.ts")] + [JSImport("InputHelper.subscribeTextEvents", "avalonia")] public static partial void SubscribeTextEvents( JSObject htmlElement, [JSMarshalAs>] @@ -26,7 +26,7 @@ internal static partial class InputHelper [JSMarshalAs>] Func onCompositionEnd); - [JSImport("InputHelper.subscribePointerEvents", "avalonia.ts")] + [JSImport("InputHelper.subscribePointerEvents", "avalonia")] public static partial void SubscribePointerEvents( JSObject htmlElement, [JSMarshalAs>] @@ -39,35 +39,35 @@ internal static partial class InputHelper Func wheel); - [JSImport("InputHelper.subscribeInputEvents", "avalonia.ts")] + [JSImport("InputHelper.subscribeInputEvents", "avalonia")] public static partial void SubscribeInputEvents( JSObject htmlElement, [JSMarshalAs>] Func input); - [JSImport("InputHelper.clearInput", "avalonia.ts")] + [JSImport("InputHelper.clearInput", "avalonia")] public static partial void ClearInputElement(JSObject htmlElement); - [JSImport("InputHelper.isInputElement", "avalonia.ts")] + [JSImport("InputHelper.isInputElement", "avalonia")] public static partial void IsInputElement(JSObject htmlElement); - [JSImport("InputHelper.focusElement", "avalonia.ts")] + [JSImport("InputHelper.focusElement", "avalonia")] public static partial void FocusElement(JSObject htmlElement); - [JSImport("InputHelper.setCursor", "avalonia.ts")] + [JSImport("InputHelper.setCursor", "avalonia")] public static partial void SetCursor(JSObject htmlElement, string kind); - [JSImport("InputHelper.hide", "avalonia.ts")] + [JSImport("InputHelper.hide", "avalonia")] public static partial void HideElement(JSObject htmlElement); - [JSImport("InputHelper.show", "avalonia.ts")] + [JSImport("InputHelper.show", "avalonia")] public static partial void ShowElement(JSObject htmlElement); - [JSImport("InputHelper.setSurroundingText", "avalonia.ts")] + [JSImport("InputHelper.setSurroundingText", "avalonia")] public static partial void SetSurroundingText(JSObject htmlElement, string text, int start, int end); - [JSImport("InputHelper.setBounds", "avalonia.ts")] + [JSImport("InputHelper.setBounds", "avalonia")] public static partial void SetBounds(JSObject htmlElement, int x, int y, int width, int height, int caret); [JSImport("globalThis.navigator.clipboard.readText")] diff --git a/src/Web/Avalonia.Web/Interop/NativeControlHostHelper.cs b/src/Web/Avalonia.Web/Interop/NativeControlHostHelper.cs index 5cc86bf622..8144f64fc7 100644 --- a/src/Web/Avalonia.Web/Interop/NativeControlHostHelper.cs +++ b/src/Web/Avalonia.Web/Interop/NativeControlHostHelper.cs @@ -5,24 +5,24 @@ namespace Avalonia.Web.Interop; internal static partial class NativeControlHostHelper { - [JSImport("NativeControlHost.createDefaultChild", "avalonia.ts")] + [JSImport("NativeControlHost.createDefaultChild", "avalonia")] internal static partial JSObject CreateDefaultChild(JSObject? parent); - [JSImport("NativeControlHost.createAttachment", "avalonia.ts")] + [JSImport("NativeControlHost.createAttachment", "avalonia")] internal static partial JSObject CreateAttachment(); - [JSImport("NativeControlHost.initializeWithChildHandle", "avalonia.ts")] + [JSImport("NativeControlHost.initializeWithChildHandle", "avalonia")] internal static partial void InitializeWithChildHandle(JSObject element, JSObject child); - [JSImport("NativeControlHost.attachTo", "avalonia.ts")] + [JSImport("NativeControlHost.attachTo", "avalonia")] internal static partial void AttachTo(JSObject element, JSObject? host); - [JSImport("NativeControlHost.showInBounds", "avalonia.ts")] + [JSImport("NativeControlHost.showInBounds", "avalonia")] internal static partial void ShowInBounds(JSObject element, double x, double y, double width, double height); - [JSImport("NativeControlHost.hideWithSize", "avalonia.ts")] + [JSImport("NativeControlHost.hideWithSize", "avalonia")] internal static partial void HideWithSize(JSObject element, double width, double height); - [JSImport("NativeControlHost.releaseChild", "avalonia.ts")] + [JSImport("NativeControlHost.releaseChild", "avalonia")] internal static partial void ReleaseChild(JSObject element); } diff --git a/src/Web/Avalonia.Web/Interop/StorageHelper.cs b/src/Web/Avalonia.Web/Interop/StorageHelper.cs index 4dc55fd959..d770e852d9 100644 --- a/src/Web/Avalonia.Web/Interop/StorageHelper.cs +++ b/src/Web/Avalonia.Web/Interop/StorageHelper.cs @@ -5,51 +5,51 @@ namespace Avalonia.Web.Interop; internal static partial class StorageHelper { - [JSImport("Caniuse.canShowOpenFilePicker", "avalonia.ts")] + [JSImport("Caniuse.canShowOpenFilePicker", "avalonia")] public static partial bool CanShowOpenFilePicker(); - [JSImport("Caniuse.canShowSaveFilePicker", "avalonia.ts")] + [JSImport("Caniuse.canShowSaveFilePicker", "avalonia")] public static partial bool CanShowSaveFilePicker(); - [JSImport("Caniuse.canShowDirectoryPicker", "avalonia.ts")] + [JSImport("Caniuse.canShowDirectoryPicker", "avalonia")] public static partial bool CanShowDirectoryPicker(); - [JSImport("StorageProvider.selectFolderDialog", "storage.ts")] + [JSImport("StorageProvider.selectFolderDialog", "storage")] public static partial Task SelectFolderDialog(JSObject? startIn); - [JSImport("StorageProvider.openFileDialog", "storage.ts")] + [JSImport("StorageProvider.openFileDialog", "storage")] public static partial Task OpenFileDialog(JSObject? startIn, bool multiple, [JSMarshalAs>] object[]? types, bool excludeAcceptAllOption); - [JSImport("StorageProvider.saveFileDialog", "storage.ts")] + [JSImport("StorageProvider.saveFileDialog", "storage")] public static partial Task SaveFileDialog(JSObject? startIn, string? suggestedName, [JSMarshalAs>] object[]? types, bool excludeAcceptAllOption); - [JSImport("StorageProvider.openBookmark", "storage.ts")] + [JSImport("StorageProvider.openBookmark", "storage")] public static partial Task OpenBookmark(string key); - [JSImport("StorageItem.saveBookmark", "storage.ts")] + [JSImport("StorageItem.saveBookmark", "storage")] public static partial Task SaveBookmark(JSObject item); - [JSImport("StorageItem.deleteBookmark", "storage.ts")] + [JSImport("StorageItem.deleteBookmark", "storage")] public static partial Task DeleteBookmark(JSObject item); - [JSImport("StorageItem.getProperties", "storage.ts")] + [JSImport("StorageItem.getProperties", "storage")] public static partial Task GetProperties(JSObject item); - [JSImport("StorageItem.openWrite", "storage.ts")] + [JSImport("StorageItem.openWrite", "storage")] public static partial Task OpenWrite(JSObject item); - [JSImport("StorageItem.openRead", "storage.ts")] + [JSImport("StorageItem.openRead", "storage")] public static partial Task OpenRead(JSObject item); - [JSImport("StorageItem.getItems", "storage.ts")] + [JSImport("StorageItem.getItems", "storage")] [return: JSMarshalAs>] public static partial Task GetItems(JSObject item); - [JSImport("StorageItems.itemsArray", "storage.ts")] + [JSImport("StorageItems.itemsArray", "storage")] public static partial JSObject[] ItemsArray(JSObject item); - [JSImport("StorageProvider.createAcceptType", "storage.ts")] + [JSImport("StorageProvider.createAcceptType", "storage")] public static partial JSObject CreateAcceptType(string description, string[] mimeTypes); } diff --git a/src/Web/Avalonia.Web/Interop/StreamHelper.cs b/src/Web/Avalonia.Web/Interop/StreamHelper.cs index 0a83bbb871..9cd5ca2591 100644 --- a/src/Web/Avalonia.Web/Interop/StreamHelper.cs +++ b/src/Web/Avalonia.Web/Interop/StreamHelper.cs @@ -9,26 +9,26 @@ namespace Avalonia.Web.Storage; /// internal static partial class StreamHelper { - [JSImport("StreamHelper.seek", "avalonia.ts")] + [JSImport("StreamHelper.seek", "avalonia")] public static partial void Seek(JSObject stream, [JSMarshalAs] long position); - [JSImport("StreamHelper.truncate", "avalonia.ts")] + [JSImport("StreamHelper.truncate", "avalonia")] public static partial void Truncate(JSObject stream, [JSMarshalAs] long size); - [JSImport("StreamHelper.write", "avalonia.ts")] + [JSImport("StreamHelper.write", "avalonia")] public static partial Task WriteAsync(JSObject stream, [JSMarshalAs] ArraySegment data); - [JSImport("StreamHelper.close", "avalonia.ts")] + [JSImport("StreamHelper.close", "avalonia")] public static partial Task CloseAsync(JSObject stream); - [JSImport("StreamHelper.byteLength", "avalonia.ts")] + [JSImport("StreamHelper.byteLength", "avalonia")] [return: JSMarshalAs] public static partial long ByteLength(JSObject stream); - [JSImport("StreamHelper.sliceArrayBuffer", "avalonia.ts")] + [JSImport("StreamHelper.sliceArrayBuffer", "avalonia")] private static partial Task SliceToArrayBuffer(JSObject stream, [JSMarshalAs] long offset, int count); - [JSImport("StreamHelper.toMemoryView", "avalonia.ts")] + [JSImport("StreamHelper.toMemoryView", "avalonia")] [return: JSMarshalAs>] private static partial byte[] ArrayBufferToMemoryView(JSObject stream); diff --git a/src/Web/Avalonia.Web/ManualTriggerRenderTimer.cs b/src/Web/Avalonia.Web/ManualTriggerRenderTimer.cs index 774848e19f..3309a6dd9f 100644 --- a/src/Web/Avalonia.Web/ManualTriggerRenderTimer.cs +++ b/src/Web/Avalonia.Web/ManualTriggerRenderTimer.cs @@ -4,7 +4,7 @@ using Avalonia.Rendering; namespace Avalonia.Web { - public class ManualTriggerRenderTimer : IRenderTimer + internal class ManualTriggerRenderTimer : IRenderTimer { private static readonly Stopwatch s_sw = Stopwatch.StartNew(); diff --git a/src/Web/Avalonia.Web/Storage/BrowserStorageProvider.cs b/src/Web/Avalonia.Web/Storage/BrowserStorageProvider.cs index 55ae2bdef0..81a621747e 100644 --- a/src/Web/Avalonia.Web/Storage/BrowserStorageProvider.cs +++ b/src/Web/Avalonia.Web/Storage/BrowserStorageProvider.cs @@ -20,7 +20,7 @@ internal class BrowserStorageProvider : IStorageProvider internal const string PickerCancelMessage = "The user aborted a request"; internal const string NoPermissionsMessage = "Permissions denied"; - private readonly Lazy> _lazyModule = new(() => JSHost.ImportAsync("storage.ts", "./storage.js")); + private readonly Lazy> _lazyModule = new(() => JSHost.ImportAsync("storage", "./storage.js")); public bool CanOpen => StorageHelper.CanShowOpenFilePicker(); public bool CanSave => StorageHelper.CanShowSaveFilePicker(); diff --git a/src/Web/Avalonia.Web/WindowingPlatform.cs b/src/Web/Avalonia.Web/WindowingPlatform.cs index 3f14680241..7c2a84516b 100644 --- a/src/Web/Avalonia.Web/WindowingPlatform.cs +++ b/src/Web/Avalonia.Web/WindowingPlatform.cs @@ -8,7 +8,7 @@ using Avalonia.Threading; namespace Avalonia.Web { - public class BrowserWindowingPlatform : IWindowingPlatform, IPlatformSettings, IPlatformThreadingInterface + internal class BrowserWindowingPlatform : IWindowingPlatform, IPlatformSettings, IPlatformThreadingInterface { private bool _signaled; private static KeyboardDevice? s_keyboard; diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia.ts index 98c912f940..a78fd7ca87 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia.ts @@ -6,15 +6,19 @@ import { Caniuse } from "./avalonia/caniuse"; import { StreamHelper } from "./avalonia/stream"; import { NativeControlHost } from "./avalonia/nativeControlHost"; -export async function createAvaloniaRuntime(api: RuntimeAPI): Promise { - api.setModuleImports("avalonia.ts", { - Caniuse, - Canvas, - InputHelper, - SizeWatcher, - DpiWatcher, - AvaloniaDOM, - StreamHelper, - NativeControlHost - }); +async function registerAvaloniaModule(api: RuntimeAPI): Promise { + api.setModuleImports("avalonia", avaloniaModule); } + +export const avaloniaModule = { + Caniuse, + Canvas, + InputHelper, + SizeWatcher, + DpiWatcher, + AvaloniaDOM, + StreamHelper, + NativeControlHost, + + registerAvaloniaModule +}; diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts index 017ca58b8f..9ae9b3d2a8 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts @@ -211,7 +211,7 @@ export class SizeWatcher { static observer: ResizeObserver; static elements: Map; - public static observe(element: HTMLElement, elementId: string, callback: (width: number, height: number) => void): void { + public static observe(element: HTMLElement, elementId: string | undefined, callback: (width: number, height: number) => void): void { if (!element || !callback) { return; } @@ -223,7 +223,7 @@ export class SizeWatcher { callback }; - SizeWatcher.elements.set(elementId, element); + SizeWatcher.elements.set(elementId ?? element.id, element); SizeWatcher.observer.observe(element); SizeWatcher.invoke(element); From 897f7c97064d9a4ca7894df35ee245bf77bd0bb3 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 10 Oct 2022 05:10:49 -0400 Subject: [PATCH 099/169] Reuse module initialization and make it configurable + some fixes --- samples/ControlCatalog.Web/App.razor.cs | 5 +- src/Web/Avalonia.Web.Blazor/AvaloniaView.cs | 3 +- .../BlazorSingleViewLifetime.cs | 21 +++-- src/Web/Avalonia.Web/Avalonia.Web.csproj | 8 +- src/Web/Avalonia.Web/AvaloniaView.cs | 3 - .../Avalonia.Web/BrowserSingleViewLifetime.cs | 79 ++++++++++--------- .../Avalonia.Web/Interop/AvaloniaModule.cs | 22 ++++++ src/Web/Avalonia.Web/Interop/CanvasHelper.cs | 6 +- src/Web/Avalonia.Web/Interop/DomHelper.cs | 8 +- src/Web/Avalonia.Web/Interop/InputHelper.cs | 24 +++--- .../Interop/NativeControlHostHelper.cs | 14 ++-- src/Web/Avalonia.Web/Interop/StorageHelper.cs | 30 +++---- src/Web/Avalonia.Web/Interop/StreamHelper.cs | 16 ++-- .../Storage/BlobReadableStream.cs | 2 + .../Storage/BrowserStorageProvider.cs | 12 +-- .../Avalonia.Web/Storage/WriteableStream.cs | 2 + .../Avalonia.Web/webapp/modules/avalonia.ts | 14 +++- .../webapp/modules/avalonia/dom.ts | 5 ++ 18 files changed, 163 insertions(+), 111 deletions(-) create mode 100644 src/Web/Avalonia.Web/Interop/AvaloniaModule.cs diff --git a/samples/ControlCatalog.Web/App.razor.cs b/samples/ControlCatalog.Web/App.razor.cs index ca3d3604b1..09ab4c3b68 100644 --- a/samples/ControlCatalog.Web/App.razor.cs +++ b/samples/ControlCatalog.Web/App.razor.cs @@ -7,8 +7,9 @@ public partial class App { protected override void OnParametersSet() { - WebAppBuilder.Configure() - .With(new SkiaOptions { CustomGpuFactory = null }) // uncomment to disable GPU/GL rendering + AppBuilder.Configure() + .UseBlazor() + // .With(new SkiaOptions { CustomGpuFactory = null }) // uncomment to disable GPU/GL rendering .SetupWithSingleViewLifetime(); base.OnParametersSet(); diff --git a/src/Web/Avalonia.Web.Blazor/AvaloniaView.cs b/src/Web/Avalonia.Web.Blazor/AvaloniaView.cs index ae294151a6..909e2dd441 100644 --- a/src/Web/Avalonia.Web.Blazor/AvaloniaView.cs +++ b/src/Web/Avalonia.Web.Blazor/AvaloniaView.cs @@ -24,6 +24,7 @@ public class AvaloniaView : ComponentBase { builder.OpenElement(0, "div"); builder.AddAttribute(1, "id", _containerId); + builder.AddAttribute(2, "style", "width:100vw;height:100vh"); builder.CloseElement(); } @@ -31,7 +32,7 @@ public class AvaloniaView : ComponentBase { if (OperatingSystem.IsBrowser()) { - _ = await JSHost.ImportAsync("avalonia", "/_content/Avalonia.Web.Blazor/avalonia.js"); + await Avalonia.Web.Interop.AvaloniaModule.ImportMain(); _browserView = new BrowserView(_containerId); if (Application.Current?.ApplicationLifetime is ISingleViewApplicationLifetime lifetime) diff --git a/src/Web/Avalonia.Web.Blazor/BlazorSingleViewLifetime.cs b/src/Web/Avalonia.Web.Blazor/BlazorSingleViewLifetime.cs index 26b4b15863..f38779f834 100644 --- a/src/Web/Avalonia.Web.Blazor/BlazorSingleViewLifetime.cs +++ b/src/Web/Avalonia.Web.Blazor/BlazorSingleViewLifetime.cs @@ -1,8 +1,11 @@ -using Avalonia.Controls; +using System.Runtime.Versioning; + +using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; namespace Avalonia.Web.Blazor; +[SupportedOSPlatform("browser")] public static class WebAppBuilder { public static T SetupWithSingleViewLifetime( @@ -12,13 +15,21 @@ public static class WebAppBuilder return builder.SetupWithLifetime(new BlazorSingleViewLifetime()); } + public static T UseBlazor(this T builder) where T : AppBuilderBase, new() + { + return builder + .UseBrowser() + .With(new BrowserPlatformOptions + { + FrameworkAssetPathResolver = new(filePath => $"/_content/Avalonia.Web.Blazor/{filePath}") + }); + } + public static AppBuilder Configure() where TApp : Application, new() { - var builder = AppBuilder.Configure() - .UseBrowser(); - - return builder; + return AppBuilder.Configure() + .UseBlazor(); } internal class BlazorSingleViewLifetime : ISingleViewApplicationLifetime diff --git a/src/Web/Avalonia.Web/Avalonia.Web.csproj b/src/Web/Avalonia.Web/Avalonia.Web.csproj index a4756a5e2b..cdfa095865 100644 --- a/src/Web/Avalonia.Web/Avalonia.Web.csproj +++ b/src/Web/Avalonia.Web/Avalonia.Web.csproj @@ -39,10 +39,6 @@ - - - - @@ -52,4 +48,8 @@ + + + + diff --git a/src/Web/Avalonia.Web/AvaloniaView.cs b/src/Web/Avalonia.Web/AvaloniaView.cs index a5da719912..3a31679424 100644 --- a/src/Web/Avalonia.Web/AvaloniaView.cs +++ b/src/Web/Avalonia.Web/AvaloniaView.cs @@ -37,7 +37,6 @@ namespace Avalonia.Web private bool _useGL; private ITextInputMethodClient? _client; - private static int _canvasCount; public AvaloniaView(string divId) : this(DomHelper.GetElementById(divId) ?? throw new Exception($"Element with id {divId} was not found in the html document.")) @@ -63,8 +62,6 @@ namespace Avalonia.Web _splash = DomHelper.GetElementById("avalonia-splash"); - _canvas.SetProperty("id", $"avaloniaCanvas{_canvasCount++}"); - _topLevelImpl = new BrowserTopLevelImpl(this); _topLevel = new WebEmbeddableControlRoot(_topLevelImpl, () => diff --git a/src/Web/Avalonia.Web/BrowserSingleViewLifetime.cs b/src/Web/Avalonia.Web/BrowserSingleViewLifetime.cs index d962956567..00ed961fbe 100644 --- a/src/Web/Avalonia.Web/BrowserSingleViewLifetime.cs +++ b/src/Web/Avalonia.Web/BrowserSingleViewLifetime.cs @@ -1,51 +1,54 @@ -using System.Runtime.InteropServices.JavaScript; -using System; +using System; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; -using Avalonia.Media; using Avalonia.Web.Skia; using System.Runtime.Versioning; -namespace Avalonia.Web +namespace Avalonia.Web; + +[SupportedOSPlatform("browser")] +public class BrowserSingleViewLifetime : ISingleViewApplicationLifetime +{ + public AvaloniaView? View; + + public Control? MainView + { + get => View!.Content; + set => View!.Content = value; + } +} + +public class BrowserPlatformOptions +{ + public Func FrameworkAssetPathResolver { get; set; } = new(fileName => $"./{fileName}"); +} + + +[SupportedOSPlatform("browser")] +public static class WebAppBuilder { - [SupportedOSPlatform("browser")] - public class BrowserSingleViewLifetime : ISingleViewApplicationLifetime + public static T SetupBrowserApp( + this T builder, string mainDivId) + where T : AppBuilderBase, new() { - public AvaloniaView? View; + var lifetime = new BrowserSingleViewLifetime(); - public Control? MainView - { - get => View!.Content; - set => View!.Content = value; - } + return builder + .UseBrowser() + .AfterSetup(b => + { + lifetime.View = new AvaloniaView(mainDivId); + }) + .SetupWithLifetime(lifetime); } - [SupportedOSPlatform("browser")] - public static partial class WebAppBuilder + public static T UseBrowser( + this T builder) + where T : AppBuilderBase, new() { - public static T SetupBrowserApp( - this T builder, string mainDivId) - where T : AppBuilderBase, new() - { - var lifetime = new BrowserSingleViewLifetime(); - - return builder - .UseBrowser() - .AfterSetup(b => - { - lifetime.View = new AvaloniaView(mainDivId); - }) - .SetupWithLifetime(lifetime); - } - - public static T UseBrowser( - this T builder) - where T : AppBuilderBase, new() - { - return builder - .UseWindowingSubsystem(BrowserWindowingPlatform.Register) - .UseSkia() - .With(new SkiaOptions { CustomGpuFactory = () => new BrowserSkiaGpu() }); - } + return builder + .UseWindowingSubsystem(BrowserWindowingPlatform.Register) + .UseSkia() + .With(new SkiaOptions { CustomGpuFactory = () => new BrowserSkiaGpu() }); } } diff --git a/src/Web/Avalonia.Web/Interop/AvaloniaModule.cs b/src/Web/Avalonia.Web/Interop/AvaloniaModule.cs new file mode 100644 index 0000000000..176b8d60fc --- /dev/null +++ b/src/Web/Avalonia.Web/Interop/AvaloniaModule.cs @@ -0,0 +1,22 @@ +using System.Runtime.InteropServices.JavaScript; +using System.Threading.Tasks; + +namespace Avalonia.Web.Interop; + +internal static class AvaloniaModule +{ + public const string MainModuleName = "avalonia"; + public const string StorageModuleName = "storage"; + + public static Task ImportMain() + { + var options = AvaloniaLocator.Current.GetService() ?? new BrowserPlatformOptions(); + return JSHost.ImportAsync(MainModuleName, options.FrameworkAssetPathResolver("avalonia.js")); + } + + public static Task ImportStorage() + { + var options = AvaloniaLocator.Current.GetService() ?? new BrowserPlatformOptions(); + return JSHost.ImportAsync(StorageModuleName, options.FrameworkAssetPathResolver("storage.js")); + } +} diff --git a/src/Web/Avalonia.Web/Interop/CanvasHelper.cs b/src/Web/Avalonia.Web/Interop/CanvasHelper.cs index ff07d1757b..efa94916fa 100644 --- a/src/Web/Avalonia.Web/Interop/CanvasHelper.cs +++ b/src/Web/Avalonia.Web/Interop/CanvasHelper.cs @@ -29,13 +29,13 @@ internal static partial class CanvasHelper return glInfo; } - [JSImport("Canvas.requestAnimationFrame", "avalonia")] + [JSImport("Canvas.requestAnimationFrame", AvaloniaModule.MainModuleName)] public static partial void RequestAnimationFrame(JSObject canvas, bool renderLoop); - [JSImport("Canvas.setCanvasSize", "avalonia")] + [JSImport("Canvas.setCanvasSize", AvaloniaModule.MainModuleName)] public static partial void SetCanvasSize(JSObject canvas, int height, int width); - [JSImport("Canvas.initGL", "avalonia")] + [JSImport("Canvas.initGL", AvaloniaModule.MainModuleName)] private static partial JSObject InitGL( JSObject canvas, string canvasId, diff --git a/src/Web/Avalonia.Web/Interop/DomHelper.cs b/src/Web/Avalonia.Web/Interop/DomHelper.cs index c2cf0a0c44..80f146a57a 100644 --- a/src/Web/Avalonia.Web/Interop/DomHelper.cs +++ b/src/Web/Avalonia.Web/Interop/DomHelper.cs @@ -8,20 +8,20 @@ internal static partial class DomHelper [JSImport("globalThis.document.getElementById")] internal static partial JSObject? GetElementById(string id); - [JSImport("AvaloniaDOM.createAvaloniaHost", "avalonia")] + [JSImport("AvaloniaDOM.createAvaloniaHost", AvaloniaModule.MainModuleName)] public static partial JSObject CreateAvaloniaHost(JSObject element); - [JSImport("AvaloniaDOM.addClass", "avalonia")] + [JSImport("AvaloniaDOM.addClass", AvaloniaModule.MainModuleName)] public static partial void AddCssClass(JSObject element, string className); - [JSImport("SizeWatcher.observe", "avalonia")] + [JSImport("SizeWatcher.observe", AvaloniaModule.MainModuleName)] public static partial JSObject ObserveSize( JSObject canvas, string? canvasId, [JSMarshalAs>] Action onSizeChanged); - [JSImport("DpiWatcher.start", "avalonia")] + [JSImport("DpiWatcher.start", AvaloniaModule.MainModuleName)] public static partial double ObserveDpi( [JSMarshalAs>] Action onDpiChanged); diff --git a/src/Web/Avalonia.Web/Interop/InputHelper.cs b/src/Web/Avalonia.Web/Interop/InputHelper.cs index bdd1957e03..cfec9f30dc 100644 --- a/src/Web/Avalonia.Web/Interop/InputHelper.cs +++ b/src/Web/Avalonia.Web/Interop/InputHelper.cs @@ -6,7 +6,7 @@ namespace Avalonia.Web.Interop; internal static partial class InputHelper { - [JSImport("InputHelper.subscribeKeyEvents", "avalonia")] + [JSImport("InputHelper.subscribeKeyEvents", AvaloniaModule.MainModuleName)] public static partial void SubscribeKeyEvents( JSObject htmlElement, [JSMarshalAs>] @@ -14,7 +14,7 @@ internal static partial class InputHelper [JSMarshalAs>] Func keyUp); - [JSImport("InputHelper.subscribeTextEvents", "avalonia")] + [JSImport("InputHelper.subscribeTextEvents", AvaloniaModule.MainModuleName)] public static partial void SubscribeTextEvents( JSObject htmlElement, [JSMarshalAs>] @@ -26,7 +26,7 @@ internal static partial class InputHelper [JSMarshalAs>] Func onCompositionEnd); - [JSImport("InputHelper.subscribePointerEvents", "avalonia")] + [JSImport("InputHelper.subscribePointerEvents", AvaloniaModule.MainModuleName)] public static partial void SubscribePointerEvents( JSObject htmlElement, [JSMarshalAs>] @@ -39,35 +39,35 @@ internal static partial class InputHelper Func wheel); - [JSImport("InputHelper.subscribeInputEvents", "avalonia")] + [JSImport("InputHelper.subscribeInputEvents", AvaloniaModule.MainModuleName)] public static partial void SubscribeInputEvents( JSObject htmlElement, [JSMarshalAs>] Func input); - [JSImport("InputHelper.clearInput", "avalonia")] + [JSImport("InputHelper.clearInput", AvaloniaModule.MainModuleName)] public static partial void ClearInputElement(JSObject htmlElement); - [JSImport("InputHelper.isInputElement", "avalonia")] + [JSImport("InputHelper.isInputElement", AvaloniaModule.MainModuleName)] public static partial void IsInputElement(JSObject htmlElement); - [JSImport("InputHelper.focusElement", "avalonia")] + [JSImport("InputHelper.focusElement", AvaloniaModule.MainModuleName)] public static partial void FocusElement(JSObject htmlElement); - [JSImport("InputHelper.setCursor", "avalonia")] + [JSImport("InputHelper.setCursor", AvaloniaModule.MainModuleName)] public static partial void SetCursor(JSObject htmlElement, string kind); - [JSImport("InputHelper.hide", "avalonia")] + [JSImport("InputHelper.hide", AvaloniaModule.MainModuleName)] public static partial void HideElement(JSObject htmlElement); - [JSImport("InputHelper.show", "avalonia")] + [JSImport("InputHelper.show", AvaloniaModule.MainModuleName)] public static partial void ShowElement(JSObject htmlElement); - [JSImport("InputHelper.setSurroundingText", "avalonia")] + [JSImport("InputHelper.setSurroundingText", AvaloniaModule.MainModuleName)] public static partial void SetSurroundingText(JSObject htmlElement, string text, int start, int end); - [JSImport("InputHelper.setBounds", "avalonia")] + [JSImport("InputHelper.setBounds", AvaloniaModule.MainModuleName)] public static partial void SetBounds(JSObject htmlElement, int x, int y, int width, int height, int caret); [JSImport("globalThis.navigator.clipboard.readText")] diff --git a/src/Web/Avalonia.Web/Interop/NativeControlHostHelper.cs b/src/Web/Avalonia.Web/Interop/NativeControlHostHelper.cs index 8144f64fc7..d3baaa2533 100644 --- a/src/Web/Avalonia.Web/Interop/NativeControlHostHelper.cs +++ b/src/Web/Avalonia.Web/Interop/NativeControlHostHelper.cs @@ -5,24 +5,24 @@ namespace Avalonia.Web.Interop; internal static partial class NativeControlHostHelper { - [JSImport("NativeControlHost.createDefaultChild", "avalonia")] + [JSImport("NativeControlHost.createDefaultChild", AvaloniaModule.MainModuleName)] internal static partial JSObject CreateDefaultChild(JSObject? parent); - [JSImport("NativeControlHost.createAttachment", "avalonia")] + [JSImport("NativeControlHost.createAttachment", AvaloniaModule.MainModuleName)] internal static partial JSObject CreateAttachment(); - [JSImport("NativeControlHost.initializeWithChildHandle", "avalonia")] + [JSImport("NativeControlHost.initializeWithChildHandle", AvaloniaModule.MainModuleName)] internal static partial void InitializeWithChildHandle(JSObject element, JSObject child); - [JSImport("NativeControlHost.attachTo", "avalonia")] + [JSImport("NativeControlHost.attachTo", AvaloniaModule.MainModuleName)] internal static partial void AttachTo(JSObject element, JSObject? host); - [JSImport("NativeControlHost.showInBounds", "avalonia")] + [JSImport("NativeControlHost.showInBounds", AvaloniaModule.MainModuleName)] internal static partial void ShowInBounds(JSObject element, double x, double y, double width, double height); - [JSImport("NativeControlHost.hideWithSize", "avalonia")] + [JSImport("NativeControlHost.hideWithSize", AvaloniaModule.MainModuleName)] internal static partial void HideWithSize(JSObject element, double width, double height); - [JSImport("NativeControlHost.releaseChild", "avalonia")] + [JSImport("NativeControlHost.releaseChild", AvaloniaModule.MainModuleName)] internal static partial void ReleaseChild(JSObject element); } diff --git a/src/Web/Avalonia.Web/Interop/StorageHelper.cs b/src/Web/Avalonia.Web/Interop/StorageHelper.cs index d770e852d9..9a6cfb9fc2 100644 --- a/src/Web/Avalonia.Web/Interop/StorageHelper.cs +++ b/src/Web/Avalonia.Web/Interop/StorageHelper.cs @@ -5,51 +5,51 @@ namespace Avalonia.Web.Interop; internal static partial class StorageHelper { - [JSImport("Caniuse.canShowOpenFilePicker", "avalonia")] + [JSImport("Caniuse.canShowOpenFilePicker", AvaloniaModule.MainModuleName)] public static partial bool CanShowOpenFilePicker(); - [JSImport("Caniuse.canShowSaveFilePicker", "avalonia")] + [JSImport("Caniuse.canShowSaveFilePicker", AvaloniaModule.MainModuleName)] public static partial bool CanShowSaveFilePicker(); - [JSImport("Caniuse.canShowDirectoryPicker", "avalonia")] + [JSImport("Caniuse.canShowDirectoryPicker", AvaloniaModule.MainModuleName)] public static partial bool CanShowDirectoryPicker(); - [JSImport("StorageProvider.selectFolderDialog", "storage")] + [JSImport("StorageProvider.selectFolderDialog", AvaloniaModule.StorageModuleName)] public static partial Task SelectFolderDialog(JSObject? startIn); - [JSImport("StorageProvider.openFileDialog", "storage")] + [JSImport("StorageProvider.openFileDialog", AvaloniaModule.StorageModuleName)] public static partial Task OpenFileDialog(JSObject? startIn, bool multiple, [JSMarshalAs>] object[]? types, bool excludeAcceptAllOption); - [JSImport("StorageProvider.saveFileDialog", "storage")] + [JSImport("StorageProvider.saveFileDialog", AvaloniaModule.StorageModuleName)] public static partial Task SaveFileDialog(JSObject? startIn, string? suggestedName, [JSMarshalAs>] object[]? types, bool excludeAcceptAllOption); - [JSImport("StorageProvider.openBookmark", "storage")] + [JSImport("StorageProvider.openBookmark", AvaloniaModule.StorageModuleName)] public static partial Task OpenBookmark(string key); - [JSImport("StorageItem.saveBookmark", "storage")] + [JSImport("StorageItem.saveBookmark", AvaloniaModule.StorageModuleName)] public static partial Task SaveBookmark(JSObject item); - [JSImport("StorageItem.deleteBookmark", "storage")] + [JSImport("StorageItem.deleteBookmark", AvaloniaModule.StorageModuleName)] public static partial Task DeleteBookmark(JSObject item); - [JSImport("StorageItem.getProperties", "storage")] + [JSImport("StorageItem.getProperties", AvaloniaModule.StorageModuleName)] public static partial Task GetProperties(JSObject item); - [JSImport("StorageItem.openWrite", "storage")] + [JSImport("StorageItem.openWrite", AvaloniaModule.StorageModuleName)] public static partial Task OpenWrite(JSObject item); - [JSImport("StorageItem.openRead", "storage")] + [JSImport("StorageItem.openRead", AvaloniaModule.StorageModuleName)] public static partial Task OpenRead(JSObject item); - [JSImport("StorageItem.getItems", "storage")] + [JSImport("StorageItem.getItems", AvaloniaModule.StorageModuleName)] [return: JSMarshalAs>] public static partial Task GetItems(JSObject item); - [JSImport("StorageItems.itemsArray", "storage")] + [JSImport("StorageItems.itemsArray", AvaloniaModule.StorageModuleName)] public static partial JSObject[] ItemsArray(JSObject item); - [JSImport("StorageProvider.createAcceptType", "storage")] + [JSImport("StorageProvider.createAcceptType", AvaloniaModule.StorageModuleName)] public static partial JSObject CreateAcceptType(string description, string[] mimeTypes); } diff --git a/src/Web/Avalonia.Web/Interop/StreamHelper.cs b/src/Web/Avalonia.Web/Interop/StreamHelper.cs index 9cd5ca2591..d9de7bcbd8 100644 --- a/src/Web/Avalonia.Web/Interop/StreamHelper.cs +++ b/src/Web/Avalonia.Web/Interop/StreamHelper.cs @@ -2,33 +2,33 @@ using System.Runtime.InteropServices.JavaScript; using System.Threading.Tasks; -namespace Avalonia.Web.Storage; +namespace Avalonia.Web.Interop; /// /// Set of FileSystemWritableFileStream and Blob methods. /// internal static partial class StreamHelper { - [JSImport("StreamHelper.seek", "avalonia")] + [JSImport("StreamHelper.seek", AvaloniaModule.MainModuleName)] public static partial void Seek(JSObject stream, [JSMarshalAs] long position); - [JSImport("StreamHelper.truncate", "avalonia")] + [JSImport("StreamHelper.truncate", AvaloniaModule.MainModuleName)] public static partial void Truncate(JSObject stream, [JSMarshalAs] long size); - [JSImport("StreamHelper.write", "avalonia")] + [JSImport("StreamHelper.write", AvaloniaModule.MainModuleName)] public static partial Task WriteAsync(JSObject stream, [JSMarshalAs] ArraySegment data); - [JSImport("StreamHelper.close", "avalonia")] + [JSImport("StreamHelper.close", AvaloniaModule.MainModuleName)] public static partial Task CloseAsync(JSObject stream); - [JSImport("StreamHelper.byteLength", "avalonia")] + [JSImport("StreamHelper.byteLength", AvaloniaModule.MainModuleName)] [return: JSMarshalAs] public static partial long ByteLength(JSObject stream); - [JSImport("StreamHelper.sliceArrayBuffer", "avalonia")] + [JSImport("StreamHelper.sliceArrayBuffer", AvaloniaModule.MainModuleName)] private static partial Task SliceToArrayBuffer(JSObject stream, [JSMarshalAs] long offset, int count); - [JSImport("StreamHelper.toMemoryView", "avalonia")] + [JSImport("StreamHelper.toMemoryView", AvaloniaModule.MainModuleName)] [return: JSMarshalAs>] private static partial byte[] ArrayBufferToMemoryView(JSObject stream); diff --git a/src/Web/Avalonia.Web/Storage/BlobReadableStream.cs b/src/Web/Avalonia.Web/Storage/BlobReadableStream.cs index 640c2fd745..77734ea62f 100644 --- a/src/Web/Avalonia.Web/Storage/BlobReadableStream.cs +++ b/src/Web/Avalonia.Web/Storage/BlobReadableStream.cs @@ -4,6 +4,8 @@ using System.Runtime.InteropServices.JavaScript; using System.Threading; using System.Threading.Tasks; +using Avalonia.Web.Interop; + namespace Avalonia.Web.Storage; [System.Runtime.Versioning.SupportedOSPlatform("browser")] diff --git a/src/Web/Avalonia.Web/Storage/BrowserStorageProvider.cs b/src/Web/Avalonia.Web/Storage/BrowserStorageProvider.cs index 81a621747e..3932b79ad0 100644 --- a/src/Web/Avalonia.Web/Storage/BrowserStorageProvider.cs +++ b/src/Web/Avalonia.Web/Storage/BrowserStorageProvider.cs @@ -20,7 +20,7 @@ internal class BrowserStorageProvider : IStorageProvider internal const string PickerCancelMessage = "The user aborted a request"; internal const string NoPermissionsMessage = "Permissions denied"; - private readonly Lazy> _lazyModule = new(() => JSHost.ImportAsync("storage", "./storage.js")); + private readonly Lazy _lazyModule = new(() => AvaloniaModule.ImportStorage()); public bool CanOpen => StorageHelper.CanShowOpenFilePicker(); public bool CanSave => StorageHelper.CanShowSaveFilePicker(); @@ -28,7 +28,7 @@ internal class BrowserStorageProvider : IStorageProvider public async Task> OpenFilePickerAsync(FilePickerOpenOptions options) { - _ = await _lazyModule.Value; + await _lazyModule.Value; var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle; var (types, exludeAll) = ConvertFileTypes(options.FileTypeFilter); @@ -62,7 +62,7 @@ internal class BrowserStorageProvider : IStorageProvider public async Task SaveFilePickerAsync(FilePickerSaveOptions options) { - _ = await _lazyModule.Value; + await _lazyModule.Value; var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle; var (types, exludeAll) = ConvertFileTypes(options.FileTypeChoices); @@ -90,7 +90,7 @@ internal class BrowserStorageProvider : IStorageProvider public async Task> OpenFolderPickerAsync(FolderPickerOpenOptions options) { - _ = await _lazyModule.Value; + await _lazyModule.Value; var startIn = (options.SuggestedStartLocation as JSStorageItem)?.FileHandle; try @@ -106,14 +106,14 @@ internal class BrowserStorageProvider : IStorageProvider public async Task OpenFileBookmarkAsync(string bookmark) { - _ = await _lazyModule.Value; + await _lazyModule.Value; var item = await StorageHelper.OpenBookmark(bookmark); return item is not null ? new JSStorageFile(item) : null; } public async Task OpenFolderBookmarkAsync(string bookmark) { - _ = await _lazyModule.Value; + await _lazyModule.Value; var item = await StorageHelper.OpenBookmark(bookmark); return item is not null ? new JSStorageFolder(item) : null; } diff --git a/src/Web/Avalonia.Web/Storage/WriteableStream.cs b/src/Web/Avalonia.Web/Storage/WriteableStream.cs index f9699fef7a..09e438c34e 100644 --- a/src/Web/Avalonia.Web/Storage/WriteableStream.cs +++ b/src/Web/Avalonia.Web/Storage/WriteableStream.cs @@ -4,6 +4,8 @@ using System.Runtime.InteropServices.JavaScript; using System.Threading; using System.Threading.Tasks; +using Avalonia.Web.Interop; + namespace Avalonia.Web.Storage; [System.Runtime.Versioning.SupportedOSPlatform("browser")] diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia.ts index a78fd7ca87..0642bd475d 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia.ts @@ -7,10 +7,18 @@ import { StreamHelper } from "./avalonia/stream"; import { NativeControlHost } from "./avalonia/nativeControlHost"; async function registerAvaloniaModule(api: RuntimeAPI): Promise { - api.setModuleImports("avalonia", avaloniaModule); + api.setModuleImports("avalonia", { + Caniuse, + Canvas, + InputHelper, + SizeWatcher, + DpiWatcher, + AvaloniaDOM, + StreamHelper, + NativeControlHost + }); } - -export const avaloniaModule = { +export { Caniuse, Canvas, InputHelper, diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/dom.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/dom.ts index 943b8330d5..494fde23e2 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia/dom.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/dom.ts @@ -4,6 +4,8 @@ export class AvaloniaDOM { } static createAvaloniaHost(host: HTMLElement) { + const randomIdPart = Math.random().toString(36).replace(/[^a-z]+/g, "").substr(2, 10); + // Root element host.classList.add("avalonia-container"); host.tabIndex = 0; @@ -11,6 +13,7 @@ export class AvaloniaDOM { // Rendering target canvas const canvas = document.createElement("canvas"); + canvas.id = `canvas${randomIdPart}`; canvas.classList.add("avalonia-canvas"); canvas.style.backgroundColor = "#ccc"; canvas.style.width = "100%"; @@ -19,6 +22,7 @@ export class AvaloniaDOM { // Native controls host const nativeHost = document.createElement("div"); + canvas.id = `nativeHost${randomIdPart}`; nativeHost.classList.add("avalonia-native-host"); nativeHost.style.left = "0px"; nativeHost.style.top = "0px"; @@ -28,6 +32,7 @@ export class AvaloniaDOM { // IME const inputElement = document.createElement("input"); + canvas.id = `input${randomIdPart}`; inputElement.classList.add("avalonia-input-element"); inputElement.autocapitalize = "none"; inputElement.type = "text"; From 75b3abbdd86720e2e688687283b49ef95e2ca63b Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Mon, 10 Oct 2022 18:25:44 +0200 Subject: [PATCH 100/169] Properly arrange Embedded controls during ArrangeOverride --- .../Documents/IInlineHost.cs | 2 - src/Avalonia.Controls/Documents/InlineRun.cs | 42 +++++++++ .../Documents/InlineUIContainer.cs | 48 +--------- src/Avalonia.Controls/RichTextBlock.cs | 87 ++++++++++++------- .../RichTextBlockTests.cs | 36 ++++++++ 5 files changed, 137 insertions(+), 78 deletions(-) create mode 100644 src/Avalonia.Controls/Documents/InlineRun.cs diff --git a/src/Avalonia.Controls/Documents/IInlineHost.cs b/src/Avalonia.Controls/Documents/IInlineHost.cs index da72c207be..5d142952ab 100644 --- a/src/Avalonia.Controls/Documents/IInlineHost.cs +++ b/src/Avalonia.Controls/Documents/IInlineHost.cs @@ -4,8 +4,6 @@ namespace Avalonia.Controls.Documents { internal interface IInlineHost : ILogical { - void AddVisualChild(IControl child); - void Invalidate(); } } diff --git a/src/Avalonia.Controls/Documents/InlineRun.cs b/src/Avalonia.Controls/Documents/InlineRun.cs new file mode 100644 index 0000000000..68c61ca3fa --- /dev/null +++ b/src/Avalonia.Controls/Documents/InlineRun.cs @@ -0,0 +1,42 @@ +using Avalonia.Media; +using Avalonia.Media.TextFormatting; +using Avalonia.Utilities; + +namespace Avalonia.Controls.Documents +{ + internal class EmbeddedControlRun : DrawableTextRun + { + public EmbeddedControlRun(IControl control, TextRunProperties properties) + { + Control = control; + Properties = properties; + } + + public IControl Control { get; } + + public override TextRunProperties? Properties { get; } + + public override Size Size => Control.DesiredSize; + + public override double Baseline + { + get + { + double baseline = Size.Height; + double baselineOffsetValue = Control.GetValue(TextBlock.BaselineOffsetProperty); + + if (!MathUtilities.IsZero(baselineOffsetValue)) + { + baseline = baselineOffsetValue; + } + + return -baseline; + } + } + + public override void Draw(DrawingContext drawingContext, Point origin) + { + //noop + } + } +} diff --git a/src/Avalonia.Controls/Documents/InlineUIContainer.cs b/src/Avalonia.Controls/Documents/InlineUIContainer.cs index d632e5fea7..7107fb7fed 100644 --- a/src/Avalonia.Controls/Documents/InlineUIContainer.cs +++ b/src/Avalonia.Controls/Documents/InlineUIContainer.cs @@ -3,7 +3,6 @@ using System.Text; using Avalonia.Media; using Avalonia.Media.TextFormatting; using Avalonia.Metadata; -using Avalonia.Utilities; namespace Avalonia.Controls.Documents { @@ -59,56 +58,11 @@ namespace Avalonia.Controls.Documents internal override void BuildTextRun(IList textRuns) { - if(InlineHost == null) - { - return; - } - - ((ISetLogicalParent)Child).SetParent(InlineHost); - - InlineHost.AddVisualChild(Child); - - textRuns.Add(new InlineRun(Child, CreateTextRunProperties())); + textRuns.Add(new EmbeddedControlRun(Child, CreateTextRunProperties())); } internal override void AppendText(StringBuilder stringBuilder) { } - - private class InlineRun : DrawableTextRun - { - public InlineRun(IControl control, TextRunProperties properties) - { - Control = control; - Properties = properties; - } - - public IControl Control { get; } - - public override TextRunProperties? Properties { get; } - - public override Size Size => Control.DesiredSize; - - public override double Baseline - { - get - { - double baseline = Size.Height; - double baselineOffsetValue = Control.GetValue(TextBlock.BaselineOffsetProperty); - - if (!MathUtilities.IsZero(baselineOffsetValue)) - { - baseline = baselineOffsetValue; - } - - return -baseline; - } - } - - public override void Draw(DrawingContext drawingContext, Point origin) - { - //noop - } - } } } diff --git a/src/Avalonia.Controls/RichTextBlock.cs b/src/Avalonia.Controls/RichTextBlock.cs index 906a038ec3..a7eee504ec 100644 --- a/src/Avalonia.Controls/RichTextBlock.cs +++ b/src/Avalonia.Controls/RichTextBlock.cs @@ -61,6 +61,7 @@ namespace Avalonia.Controls private int _selectionStart; private int _selectionEnd; private int _wordSelectionStart = -1; + private IReadOnlyList? _textRuns; static RichTextBlock() { @@ -277,8 +278,8 @@ namespace Avalonia.Controls protected override void SetText(string? text) { var oldValue = GetText(); - - AddText(text); + + AddText(text); RaisePropertyChanged(TextProperty, oldValue, text); } @@ -301,18 +302,9 @@ namespace Avalonia.Controls ITextSource textSource; - if (HasComplexContent) + if (_textRuns != null) { - var inlines = Inlines!; - - var textRuns = new List(); - - foreach (var inline in inlines) - { - inline.BuildTextRun(textRuns); - } - - textSource = new InlinesTextSource(textRuns); + textSource = new InlinesTextSource(_textRuns); } else { @@ -546,27 +538,72 @@ namespace Avalonia.Controls protected override Size MeasureOverride(Size availableSize) { - foreach (var child in VisualChildren) + LogicalChildren.Clear(); + + VisualChildren.Clear(); + + if (Inlines != null && Inlines.Count > 0) { - if (child is Control control) + var inlines = Inlines; + + var textRuns = new List(); + + foreach (var inline in inlines) { - control.Measure(Size.Infinity); + inline.BuildTextRun(textRuns); + } + + foreach (var textRun in textRuns) + { + if (textRun is EmbeddedControlRun controlRun && + controlRun.Control is Control control) + { + ((ISetLogicalParent)control).SetParent(this); + + VisualChildren.Add(control); + + control.Measure(Size.Infinity); + } } + + _textRuns = textRuns; + } + else + { + _textRuns = null; } - + return base.MeasureOverride(availableSize); } protected override Size ArrangeOverride(Size finalSize) { - foreach (var child in VisualChildren) + if (HasComplexContent) { - if (child is Control control) + var currentY = 0.0; + + foreach (var textLine in TextLayout.TextLines) { - control.Arrange(new Rect(control.DesiredSize)); + var currentX = textLine.Start; + + foreach (var run in textLine.TextRuns) + { + if (run is DrawableTextRun drawable) + { + if (drawable is EmbeddedControlRun controlRun + && controlRun.Control is Control control) + { + control.Arrange(new Rect(new Point(currentX, currentY), control.DesiredSize)); + } + + currentX += drawable.Size.Width; + } + } + + currentY += textLine.Height; } } - + return base.ArrangeOverride(finalSize); } @@ -618,14 +655,6 @@ namespace Avalonia.Controls } } - void IInlineHost.AddVisualChild(IControl child) - { - if (child.VisualParent == null) - { - VisualChildren.Add(child); - } - } - void IInlineHost.Invalidate() { InvalidateTextLayout(); diff --git a/tests/Avalonia.Controls.UnitTests/RichTextBlockTests.cs b/tests/Avalonia.Controls.UnitTests/RichTextBlockTests.cs index c74f13b808..05007e4f2e 100644 --- a/tests/Avalonia.Controls.UnitTests/RichTextBlockTests.cs +++ b/tests/Avalonia.Controls.UnitTests/RichTextBlockTests.cs @@ -1,4 +1,6 @@ using Avalonia.Controls.Documents; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Templates; using Avalonia.Media; using Avalonia.UnitTests; using Xunit; @@ -92,5 +94,39 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(target, run.Parent); } } + + [Fact] + public void InlineUIContainer_Child_Schould_Be_Arranged() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var target = new RichTextBlock(); + + var button = new Button { Content = "12345678" }; + + button.Template = new FuncControlTemplate