From 6b70e56baa763bc4c41e15f4f990fdb663d8f231 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 15 Jul 2022 23:10:10 -0400 Subject: [PATCH 1/5] Use npm and typescript without msbuild tasks --- .gitignore | 2 + .../Avalonia.Web.Blazor.csproj | 33 +- .../Interop/Typescript/DpiWatcher.ts | 41 --- .../Interop/Typescript/InputHelper.ts | 23 -- .../Interop/Typescript/SKHtmlCanvas.ts | 261 -------------- .../Interop/Typescript/SizeWatcher.ts | 68 ---- .../Typescript/types/dotnet/extras.d.ts | 7 - .../Typescript/types/emscripten/index.d.ts | 326 ------------------ src/Web/Avalonia.Web.Blazor/tsconfig.json | 14 - .../webapp/modules/Common/Common.ts | 1 + .../webapp/modules/Common/DpiWatcher.ts | 40 +++ .../webapp/modules/Common/IndexedDbWrapper.ts | 79 +++++ .../webapp/modules/Common/InputHelper.ts | 22 ++ .../modules/Common}/NativeControlHost.ts | 35 +- .../webapp/modules/Common/SKHtmlCanvas.ts | 255 ++++++++++++++ .../webapp/modules/Common/SizeWatcher.ts | 67 ++++ .../modules/Storage}/StorageProvider.ts | 164 ++------- .../Avalonia.Web.Blazor/webapp/package.json | 14 + .../Avalonia.Web.Blazor/webapp/tsconfig.json | 18 + .../types/dotnet/index.d.ts | 0 .../webapp/webpack.config.js | 0 21 files changed, 566 insertions(+), 904 deletions(-) delete mode 100644 src/Web/Avalonia.Web.Blazor/Interop/Typescript/DpiWatcher.ts delete mode 100644 src/Web/Avalonia.Web.Blazor/Interop/Typescript/InputHelper.ts delete mode 100644 src/Web/Avalonia.Web.Blazor/Interop/Typescript/SKHtmlCanvas.ts delete mode 100644 src/Web/Avalonia.Web.Blazor/Interop/Typescript/SizeWatcher.ts delete mode 100644 src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/dotnet/extras.d.ts delete mode 100644 src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/emscripten/index.d.ts delete mode 100644 src/Web/Avalonia.Web.Blazor/tsconfig.json create mode 100644 src/Web/Avalonia.Web.Blazor/webapp/modules/Common/Common.ts create mode 100644 src/Web/Avalonia.Web.Blazor/webapp/modules/Common/DpiWatcher.ts create mode 100644 src/Web/Avalonia.Web.Blazor/webapp/modules/Common/IndexedDbWrapper.ts create mode 100644 src/Web/Avalonia.Web.Blazor/webapp/modules/Common/InputHelper.ts rename src/Web/Avalonia.Web.Blazor/{Interop/Typescript => webapp/modules/Common}/NativeControlHost.ts (57%) create mode 100644 src/Web/Avalonia.Web.Blazor/webapp/modules/Common/SKHtmlCanvas.ts create mode 100644 src/Web/Avalonia.Web.Blazor/webapp/modules/Common/SizeWatcher.ts rename src/Web/Avalonia.Web.Blazor/{Interop/Typescript => webapp/modules/Storage}/StorageProvider.ts (52%) create mode 100644 src/Web/Avalonia.Web.Blazor/webapp/package.json create mode 100644 src/Web/Avalonia.Web.Blazor/webapp/tsconfig.json rename src/Web/Avalonia.Web.Blazor/{Interop/Typescript => webapp}/types/dotnet/index.d.ts (100%) create mode 100644 src/Web/Avalonia.Web.Blazor/webapp/webpack.config.js diff --git a/.gitignore b/.gitignore index 44fe5e4ba4..0f0e153e9c 100644 --- a/.gitignore +++ b/.gitignore @@ -212,3 +212,5 @@ coc-settings.json *.map src/Web/Avalonia.Web.Blazor/wwwroot/*.js src/Web/Avalonia.Web.Blazor/Interop/Typescript/*.js +node_modules +src/Web/Avalonia.Web.Blazor/webapp/package-lock.json diff --git a/src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.csproj b/src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.csproj index 2d598687a9..3fcfaa1c9c 100644 --- a/src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.csproj +++ b/src/Web/Avalonia.Web.Blazor/Avalonia.Web.Blazor.csproj @@ -8,31 +8,16 @@ false true - + - - wwwroot - true - true - - - - false - true - - - true - false - - - + true @@ -47,10 +32,24 @@ + + + + + + + + + + + + + + diff --git a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/DpiWatcher.ts b/src/Web/Avalonia.Web.Blazor/Interop/Typescript/DpiWatcher.ts deleted file mode 100644 index 72baf14d8f..0000000000 --- a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/DpiWatcher.ts +++ /dev/null @@ -1,41 +0,0 @@ - -export class DpiWatcher { - static lastDpi: number; - static timerId: number; - static callback: DotNet.DotNetObjectReference; - - public static getDpi() { - return window.devicePixelRatio; - } - - public static start(callback: DotNet.DotNetObjectReference): 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/Interop/Typescript/InputHelper.ts b/src/Web/Avalonia.Web.Blazor/Interop/Typescript/InputHelper.ts deleted file mode 100644 index a3bf9de31d..0000000000 --- a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/InputHelper.ts +++ /dev/null @@ -1,23 +0,0 @@ - -export class InputHelper { - public static clear (inputElement: HTMLInputElement){ - inputElement.value = ""; - } - - public static focus (inputElement: HTMLInputElement){ - inputElement.focus(); - inputElement.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'; - } -} diff --git a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/SKHtmlCanvas.ts b/src/Web/Avalonia.Web.Blazor/Interop/Typescript/SKHtmlCanvas.ts deleted file mode 100644 index 04d57a7756..0000000000 --- a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/SKHtmlCanvas.ts +++ /dev/null @@ -1,261 +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 -} & HTMLCanvasElement - -export class SKHtmlCanvas { - static elements: Map; - - htmlCanvas: HTMLCanvasElement; - glInfo: SKGLViewInfo; - renderFrameCallback: DotNet.DotNetObjectReference; - renderLoopEnabled: boolean = false; - renderLoopRequest: number = 0; - newWidth: number; - newHeight: number; - - public static initGL(element: HTMLCanvasElement, elementId: string, callback: DotNet.DotNetObjectReference): SKGLViewInfo { - 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.DotNetObjectReference): 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.DotNetObjectReference): SKHtmlCanvas { - 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[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[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.DotNetObjectReference) { - 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 null; - } - - // 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; - } - - if(this.htmlCanvas.height != this.newHeight) - { - this.htmlCanvas.height = this.newHeight; - } - - 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/Interop/Typescript/SizeWatcher.ts b/src/Web/Avalonia.Web.Blazor/Interop/Typescript/SizeWatcher.ts deleted file mode 100644 index 88b94f3a80..0000000000 --- a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/SizeWatcher.ts +++ /dev/null @@ -1,68 +0,0 @@ - -type SizeWatcherElement = { - SizeWatcher: SizeWatcherInstance; -} & HTMLElement - -type SizeWatcherInstance = { - callback: DotNet.DotNetObjectReference; -} - -export class SizeWatcher { - static observer: ResizeObserver; - static elements: Map; - - public static observe(element: HTMLElement, elementId: string, callback: DotNet.DotNetObjectReference) { - 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[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[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/Interop/Typescript/types/dotnet/extras.d.ts b/src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/dotnet/extras.d.ts deleted file mode 100644 index 4a3d71e034..0000000000 --- a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/dotnet/extras.d.ts +++ /dev/null @@ -1,7 +0,0 @@ - -declare namespace DotNet { - interface DotNetObjectReference extends DotNet.DotNetObject { - _id: number; - dispose(); - } -} diff --git a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/emscripten/index.d.ts b/src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/emscripten/index.d.ts deleted file mode 100644 index e3829d4db1..0000000000 --- a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/emscripten/index.d.ts +++ /dev/null @@ -1,326 +0,0 @@ -// Type definitions for Emscripten 1.39.16 -// Project: https://emscripten.org -// Definitions by: Kensuke Matsuzaki -// Periklis Tsirakidis -// Bumsik Kim -// Louis DeScioli -// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped -// TypeScript Version: 2.2 - -/** Other WebAssembly declarations, for compatibility with older versions of Typescript */ -declare namespace WebAssembly { - interface Module {} -} - -declare namespace Emscripten { - interface FileSystemType {} - type EnvironmentType = 'WEB' | 'NODE' | 'SHELL' | 'WORKER'; - - type JSType = 'number' | 'string' | 'array' | 'boolean'; - type TypeCompatibleWithC = number | string | any[] | boolean; - - type CIntType = 'i8' | 'i16' | 'i32' | 'i64'; - type CFloatType = 'float' | 'double'; - type CPointerType = 'i8*' | 'i16*' | 'i32*' | 'i64*' | 'float*' | 'double*' | '*'; - type CType = CIntType | CFloatType | CPointerType; - - type WebAssemblyImports = Array<{ - name: string; - kind: string; - }>; - - type WebAssemblyExports = Array<{ - module: string; - name: string; - kind: string; - }>; - - interface CCallOpts { - async?: boolean | undefined; - } -} - -interface EmscriptenModule { - print(str: string): void; - printErr(str: string): void; - arguments: string[]; - environment: Emscripten.EnvironmentType; - preInit: Array<{ (): void }>; - preRun: Array<{ (): void }>; - postRun: Array<{ (): void }>; - onAbort: { (what: any): void }; - onRuntimeInitialized: { (): void }; - preinitializedWebGLContext: WebGLRenderingContext; - noInitialRun: boolean; - noExitRuntime: boolean; - logReadFiles: boolean; - filePackagePrefixURL: string; - wasmBinary: ArrayBuffer; - - destroy(object: object): void; - getPreloadedPackage(remotePackageName: string, remotePackageSize: number): ArrayBuffer; - instantiateWasm( - imports: Emscripten.WebAssemblyImports, - successCallback: (module: WebAssembly.Module) => void, - ): Emscripten.WebAssemblyExports; - locateFile(url: string, scriptDirectory: string): string; - onCustomMessage(event: MessageEvent): void; - - // USE_TYPED_ARRAYS == 1 - HEAP: Int32Array; - IHEAP: Int32Array; - FHEAP: Float64Array; - - // USE_TYPED_ARRAYS == 2 - HEAP8: Int8Array; - HEAP16: Int16Array; - HEAP32: Int32Array; - HEAPU8: Uint8Array; - HEAPU16: Uint16Array; - HEAPU32: Uint32Array; - HEAPF32: Float32Array; - HEAPF64: Float64Array; - - TOTAL_STACK: number; - TOTAL_MEMORY: number; - FAST_MEMORY: number; - - addOnPreRun(cb: () => any): void; - addOnInit(cb: () => any): void; - addOnPreMain(cb: () => any): void; - addOnExit(cb: () => any): void; - addOnPostRun(cb: () => any): void; - - preloadedImages: any; - preloadedAudios: any; - - _malloc(size: number): number; - _free(ptr: number): void; -} - -/** - * A factory function is generated when setting the `MODULARIZE` build option - * to `1` in your Emscripten build. It return a Promise that resolves to an - * initialized, ready-to-call `EmscriptenModule` instance. - * - * By default, the factory function will be named `Module`. It's recommended to - * use the `EXPORT_ES6` option, in which the factory function will be the - * default export. If used without `EXPORT_ES6`, the factory function will be a - * global variable. You can rename the variable using the `EXPORT_NAME` build - * option. It's left to you to declare any global variables as needed in your - * application's types. - * @param moduleOverrides Default properties for the initialized module. - */ -type EmscriptenModuleFactory = ( - moduleOverrides?: Partial, -) => Promise; - -declare namespace FS { - interface Lookup { - path: string; - node: FSNode; - } - - interface FSStream {} - interface FSNode {} - interface ErrnoError {} - - let ignorePermissions: boolean; - let trackingDelegate: any; - let tracking: any; - let genericErrors: any; - - // - // paths - // - function lookupPath(path: string, opts: any): Lookup; - function getPath(node: FSNode): string; - - // - // nodes - // - function isFile(mode: number): boolean; - function isDir(mode: number): boolean; - function isLink(mode: number): boolean; - function isChrdev(mode: number): boolean; - function isBlkdev(mode: number): boolean; - function isFIFO(mode: number): boolean; - function isSocket(mode: number): boolean; - - // - // devices - // - function major(dev: number): number; - function minor(dev: number): number; - function makedev(ma: number, mi: number): number; - function registerDevice(dev: number, ops: any): void; - - // - // core - // - function syncfs(populate: boolean, callback: (e: any) => any): void; - function syncfs(callback: (e: any) => any, populate?: boolean): void; - function mount(type: Emscripten.FileSystemType, opts: any, mountpoint: string): any; - function unmount(mountpoint: string): void; - - function mkdir(path: string, mode?: number): any; - function mkdev(path: string, mode?: number, dev?: number): any; - function symlink(oldpath: string, newpath: string): any; - function rename(old_path: string, new_path: string): void; - function rmdir(path: string): void; - function readdir(path: string): any; - function unlink(path: string): void; - function readlink(path: string): string; - function stat(path: string, dontFollow?: boolean): any; - function lstat(path: string): any; - function chmod(path: string, mode: number, dontFollow?: boolean): void; - function lchmod(path: string, mode: number): void; - function fchmod(fd: number, mode: number): void; - function chown(path: string, uid: number, gid: number, dontFollow?: boolean): void; - function lchown(path: string, uid: number, gid: number): void; - function fchown(fd: number, uid: number, gid: number): void; - function truncate(path: string, len: number): void; - function ftruncate(fd: number, len: number): void; - function utime(path: string, atime: number, mtime: number): void; - function open(path: string, flags: string, mode?: number, fd_start?: number, fd_end?: number): FSStream; - function close(stream: FSStream): void; - function llseek(stream: FSStream, offset: number, whence: number): any; - function read(stream: FSStream, buffer: ArrayBufferView, offset: number, length: number, position?: number): number; - function write( - stream: FSStream, - buffer: ArrayBufferView, - offset: number, - length: number, - position?: number, - canOwn?: boolean, - ): number; - function allocate(stream: FSStream, offset: number, length: number): void; - function mmap( - stream: FSStream, - buffer: ArrayBufferView, - offset: number, - length: number, - position: number, - prot: number, - flags: number, - ): any; - function ioctl(stream: FSStream, cmd: any, arg: any): any; - function readFile(path: string, opts: { encoding: 'binary'; flags?: string | undefined }): Uint8Array; - function readFile(path: string, opts: { encoding: 'utf8'; flags?: string | undefined }): string; - function readFile(path: string, opts?: { flags?: string | undefined }): Uint8Array; - function writeFile(path: string, data: string | ArrayBufferView, opts?: { flags?: string | undefined }): void; - - // - // module-level FS code - // - function cwd(): string; - function chdir(path: string): void; - function init( - input: null | (() => number | null), - output: null | ((c: number) => any), - error: null | ((c: number) => any), - ): void; - - function createLazyFile( - parent: string | FSNode, - name: string, - url: string, - canRead: boolean, - canWrite: boolean, - ): FSNode; - function createPreloadedFile( - parent: string | FSNode, - name: string, - url: string, - canRead: boolean, - canWrite: boolean, - onload?: () => void, - onerror?: () => void, - dontCreateFile?: boolean, - canOwn?: boolean, - ): void; - function createDataFile( - parent: string | FSNode, - name: string, - data: ArrayBufferView, - canRead: boolean, - canWrite: boolean, - canOwn: boolean, - ): FSNode; -} - -declare var MEMFS: Emscripten.FileSystemType; -declare var NODEFS: Emscripten.FileSystemType; -declare var IDBFS: Emscripten.FileSystemType; - -// Below runtime function/variable declarations are exportable by -// -s EXTRA_EXPORTED_RUNTIME_METHODS. You can extend or merge -// EmscriptenModule interface to add runtime functions. -// -// For example, by using -s "EXTRA_EXPORTED_RUNTIME_METHODS=['ccall']" -// You can access ccall() via Module["ccall"]. In this case, you should -// extend EmscriptenModule to pass the compiler check like the following: -// -// interface YourOwnEmscriptenModule extends EmscriptenModule { -// ccall: typeof ccall; -// } -// -// See: https://emscripten.org/docs/getting_started/FAQ.html#why-do-i-get-typeerror-module-something-is-not-a-function - -declare function ccall( - ident: string, - returnType: Emscripten.JSType | null, - argTypes: Emscripten.JSType[], - args: Emscripten.TypeCompatibleWithC[], - opts?: Emscripten.CCallOpts, -): any; -declare function cwrap( - ident: string, - returnType: Emscripten.JSType | null, - argTypes: Emscripten.JSType[], - opts?: Emscripten.CCallOpts, -): (...args: any[]) => any; - -declare function setValue(ptr: number, value: any, type: Emscripten.CType, noSafe?: boolean): void; -declare function getValue(ptr: number, type: Emscripten.CType, noSafe?: boolean): number; - -declare function allocate( - slab: number[] | ArrayBufferView | number, - types: Emscripten.CType | Emscripten.CType[], - allocator: number, - ptr?: number, -): number; - -declare function stackAlloc(size: number): number; -declare function stackSave(): number; -declare function stackRestore(ptr: number): void; - -declare function UTF8ToString(ptr: number, maxBytesToRead?: number): string; -declare function stringToUTF8(str: string, outPtr: number, maxBytesToRead?: number): void; -declare function lengthBytesUTF8(str: string): number; -declare function allocateUTF8(str: string): number; -declare function allocateUTF8OnStack(str: string): number; -declare function UTF16ToString(ptr: number): string; -declare function stringToUTF16(str: string, outPtr: number, maxBytesToRead?: number): void; -declare function lengthBytesUTF16(str: string): number; -declare function UTF32ToString(ptr: number): string; -declare function stringToUTF32(str: string, outPtr: number, maxBytesToRead?: number): void; -declare function lengthBytesUTF32(str: string): number; - -declare function intArrayFromString(stringy: string, dontAddNull?: boolean, length?: number): number[]; -declare function intArrayToString(array: number[]): string; -declare function writeStringToMemory(str: string, buffer: number, dontAddNull: boolean): void; -declare function writeArrayToMemory(array: number[], buffer: number): void; -declare function writeAsciiToMemory(str: string, buffer: number, dontAddNull: boolean): void; - -declare function addRunDependency(id: any): void; -declare function removeRunDependency(id: any): void; - -declare function addFunction(func: (...args: any[]) => any, signature?: string): number; -declare function removeFunction(funcPtr: number): void; - -declare var ALLOC_NORMAL: number; -declare var ALLOC_STACK: number; -declare var ALLOC_STATIC: number; -declare var ALLOC_DYNAMIC: number; -declare var ALLOC_NONE: number; diff --git a/src/Web/Avalonia.Web.Blazor/tsconfig.json b/src/Web/Avalonia.Web.Blazor/tsconfig.json deleted file mode 100644 index 71c462cd60..0000000000 --- a/src/Web/Avalonia.Web.Blazor/tsconfig.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "compilerOptions": { - "noImplicitAny": false, - "noEmitOnError": true, - "removeComments": false, - "sourceMap": true, - "target": "ES2020", - "module": "ES2020", - "outDir": "wwwroot" - }, - "exclude": [ - "node_modules" - ] -} diff --git a/src/Web/Avalonia.Web.Blazor/webapp/modules/Common/Common.ts b/src/Web/Avalonia.Web.Blazor/webapp/modules/Common/Common.ts new file mode 100644 index 0000000000..5f282702bb --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/webapp/modules/Common/Common.ts @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Web/Avalonia.Web.Blazor/webapp/modules/Common/DpiWatcher.ts b/src/Web/Avalonia.Web.Blazor/webapp/modules/Common/DpiWatcher.ts new file mode 100644 index 0000000000..06235782f8 --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/webapp/modules/Common/DpiWatcher.ts @@ -0,0 +1,40 @@ +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/Common/IndexedDbWrapper.ts b/src/Web/Avalonia.Web.Blazor/webapp/modules/Common/IndexedDbWrapper.ts new file mode 100644 index 0000000000..2eaa8de2fe --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/webapp/modules/Common/IndexedDbWrapper.ts @@ -0,0 +1,79 @@ +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/Common/InputHelper.ts b/src/Web/Avalonia.Web.Blazor/webapp/modules/Common/InputHelper.ts new file mode 100644 index 0000000000..2cce411376 --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/webapp/modules/Common/InputHelper.ts @@ -0,0 +1,22 @@ +export class InputHelper { + public static clear(inputElement: HTMLInputElement) { + inputElement.value = ""; + } + + public static focus(inputElement: HTMLInputElement) { + inputElement.focus(); + inputElement.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'; + } +} diff --git a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/NativeControlHost.ts b/src/Web/Avalonia.Web.Blazor/webapp/modules/Common/NativeControlHost.ts similarity index 57% rename from src/Web/Avalonia.Web.Blazor/Interop/Typescript/NativeControlHost.ts rename to src/Web/Avalonia.Web.Blazor/webapp/modules/Common/NativeControlHost.ts index baa9191845..9e5c3843c8 100644 --- a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/NativeControlHost.ts +++ b/src/Web/Avalonia.Web.Blazor/webapp/modules/Common/NativeControlHost.ts @@ -14,10 +14,9 @@ } } -class NativeControlHostTopLevelAttachment -{ - _child: HTMLElement; - _host: HTMLElement; +class NativeControlHostTopLevelAttachment { + _child?: HTMLElement; + _host?: HTMLElement; InitializeWithChildHandle(child: HTMLElement) { this._child = child; @@ -25,32 +24,38 @@ class NativeControlHostTopLevelAttachment } AttachTo(host: HTMLElement): void { - if (this._host) { + if (this._host && this._child) { this._host.removeChild(this._child); } this._host = host; - if (this._host) { + if (this._host && this._child) { this._host.appendChild(this._child); } } ShowInBounds(x: number, y: number, width: number, height: number): void { - 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"; + 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 { - this._child.style.width = width + "px"; - this._child.style.height = height + "px"; - this._child.style.display = "none"; + if (this._child) { + this._child.style.width = width + "px"; + this._child.style.height = height + "px"; + this._child.style.display = "none"; + } } ReleaseChild(): void { - this._child = null; + if (this._child) { + this._child = undefined; + } } } diff --git a/src/Web/Avalonia.Web.Blazor/webapp/modules/Common/SKHtmlCanvas.ts b/src/Web/Avalonia.Web.Blazor/webapp/modules/Common/SKHtmlCanvas.ts new file mode 100644 index 0000000000..e934f74807 --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/webapp/modules/Common/SKHtmlCanvas.ts @@ -0,0 +1,255 @@ +// 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/Common/SizeWatcher.ts b/src/Web/Avalonia.Web.Blazor/webapp/modules/Common/SizeWatcher.ts new file mode 100644 index 0000000000..715b252988 --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/webapp/modules/Common/SizeWatcher.ts @@ -0,0 +1,67 @@ +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/Interop/Typescript/StorageProvider.ts b/src/Web/Avalonia.Web.Blazor/webapp/modules/Storage/StorageProvider.ts similarity index 52% rename from src/Web/Avalonia.Web.Blazor/Interop/Typescript/StorageProvider.ts rename to src/Web/Avalonia.Web.Blazor/webapp/modules/Storage/StorageProvider.ts index 1ee055eff9..65b8fa908e 100644 --- a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/StorageProvider.ts +++ b/src/Web/Avalonia.Web.Blazor/webapp/modules/Storage/StorageProvider.ts @@ -1,132 +1,14 @@ -// As we don't have proper package managing for Avalonia.Web project, declare types manually -declare global { - interface FileSystemWritableFileStream { - write(position: number, data: BufferSource | Blob | string): Promise; - truncate(size: number): Promise; - close(): Promise; - } - type PermissionsMode = "read" | "readwrite"; - interface FileSystemFileHandle { - name: string, - getFile(): Promise; - createWritable(options?: { keepExistingData?: boolean }): Promise; - - queryPermission(options?: { mode: PermissionsMode }): Promise<"granted" | "denied" | "prompt">; - requestPermission(options?: { mode: PermissionsMode }): Promise<"granted" | "denied" | "prompt">; +import { IndexedDbWrapper } from "./IndexedDbWrapper"; - entries(): AsyncIterableIterator<[string, FileSystemFileHandle]>; - } - type WellKnownDirectory = "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos"; - type StartInDirectory = WellKnownDirectory | FileSystemFileHandle; - interface FilePickerAcceptType { - description: string, - // mime -> ext[] array - accept: { [mime: string]: string | string[] } - } - interface FilePickerOptions { - types?: FilePickerAcceptType[], - excludeAcceptAllOption: boolean, - id?: string, +declare global { + type WellKnownDirectory = "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos"; + type StartInDirectory = WellKnownDirectory | FileSystemFileHandle; + interface OpenFilePickerOptions { startIn?: StartInDirectory } - interface OpenFilePickerOptions extends FilePickerOptions { - multiple: boolean - } - interface SaveFilePickerOptions extends FilePickerOptions { - suggestedName?: string - } - interface DirectoryPickerOptions { - id?: string, + interface SaveFilePickerOptions { startIn?: StartInDirectory } - - interface Window { - showOpenFilePicker: (options: OpenFilePickerOptions) => Promise; - showSaveFilePicker: (options: SaveFilePickerOptions) => Promise; - showDirectoryPicker: (options: DirectoryPickerOptions) => Promise; - } -} - -// TODO move to another file and use import -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); - } - }); - } -} - -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(); - } } const fileBookmarksStore: string = "fileBookmarks"; @@ -135,7 +17,7 @@ const avaloniaDb = new IndexedDbWrapper("AvaloniaDb", [ ]) class StorageItem { - constructor(public handle: FileSystemFileHandle, private bookmarkId?: string) { } + constructor(private handle: FileSystemHandle, private bookmarkId?: string) { } public getName(): string { return this.handle.name @@ -146,21 +28,35 @@ class StorageItem { } public async openRead(): Promise { + if (!(this.handle instanceof FileSystemFileHandle)) { + throw new Error("StorageItem is not a file"); + } + await this.verityPermissions('read'); - return await this.handle.getFile(); + 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 }> { - const file = this.handle.getFile && await this.handle.getFile(); - - return file && { + 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 @@ -168,6 +64,10 @@ class StorageItem { } public async getItems(): Promise { + if (this.handle.kind !== "directory"){ + return new StorageItems([]); + } + const items: StorageItem[] = []; for await (const [key, value] of this.handle.entries()) { items.push(new StorageItem(value)); @@ -175,7 +75,7 @@ class StorageItem { return new StorageItems(items); } - private async verityPermissions(mode: PermissionsMode): Promise { + private async verityPermissions(mode: FileSystemPermissionMode): Promise { if (await this.handle.queryPermission({ mode }) === 'granted') { return; } @@ -200,7 +100,7 @@ class StorageItem { connection.close(); } } - + public async deleteBookmark(): Promise { if (!this.bookmarkId) { return; diff --git a/src/Web/Avalonia.Web.Blazor/webapp/package.json b/src/Web/Avalonia.Web.Blazor/webapp/package.json new file mode 100644 index 0000000000..130e3032b4 --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/webapp/package.json @@ -0,0 +1,14 @@ +{ + "name": "avalonia.web", + "scripts": { + "build": "tsc --build", + "clean": "tsc --build --clean" + }, + "devDependencies": { + "@types/emscripten": "^1.39.6", + "@types/wicg-file-system-access": "^2020.9.5", + "typescript": "^4.7.4", + "webpack": "^5.73.0", + "webpack-cli": "^4.10.0" + } +} diff --git a/src/Web/Avalonia.Web.Blazor/webapp/tsconfig.json b/src/Web/Avalonia.Web.Blazor/webapp/tsconfig.json new file mode 100644 index 0000000000..9a18b259c0 --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/webapp/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES6", + "strict": true, + "sourceMap": true, + "outDir": "../wwwroot", + "removeComments": true, + "noEmitOnError": true, + "lib": [ + "dom", + "ES6", + "esnext.asynciterable" + ] + }, + "exclude": [ + "node_modules" + ] +} diff --git a/src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/dotnet/index.d.ts b/src/Web/Avalonia.Web.Blazor/webapp/types/dotnet/index.d.ts similarity index 100% rename from src/Web/Avalonia.Web.Blazor/Interop/Typescript/types/dotnet/index.d.ts rename to src/Web/Avalonia.Web.Blazor/webapp/types/dotnet/index.d.ts diff --git a/src/Web/Avalonia.Web.Blazor/webapp/webpack.config.js b/src/Web/Avalonia.Web.Blazor/webapp/webpack.config.js new file mode 100644 index 0000000000..e69de29bb2 From c8a2e65120130669043d9251889bad2a7deb5b93 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 24 Aug 2022 14:55:01 +0200 Subject: [PATCH 2/5] Use webpack modules --- .../Avalonia.Web.Blazor/AvaloniaView.razor.cs | 15 +- .../Interop/AvaloniaModule.cs | 18 ++ .../Interop/DpiWatcherInterop.cs | 41 ++-- .../Interop/InputHelperInterop.cs | 31 +-- .../Interop/JSModuleInterop.cs | 12 +- .../Interop/NativeControlHostImpl.cs | 32 ++- .../Interop/SKHtmlCanvasInterop.cs | 45 ++-- .../Interop/SizeWatcherInterop.cs | 41 ++-- .../Interop/Storage/StorageProviderInterop.cs | 2 +- .../webapp/modules/Avalonia/Avalonia.ts | 5 + .../{Common => Avalonia}/DpiWatcher.ts | 0 .../{Common => Avalonia}/InputHelper.ts | 0 .../{Common => Avalonia}/NativeControlHost.ts | 0 .../{Common => Avalonia}/SKHtmlCanvas.ts | 0 .../{Common => Avalonia}/SizeWatcher.ts | 0 .../webapp/modules/Common/Common.ts | 1 - .../{Common => Storage}/IndexedDbWrapper.ts | 0 .../webapp/modules/Storage/StorageProvider.ts | 12 +- .../Avalonia.Web.Blazor/webapp/package.json | 6 +- .../webapp/webpack.config.js | 40 ++++ .../wwwroot/Avalonia/Avalonia.js | 6 + .../wwwroot/Avalonia/DpiWatcher.js | 26 +++ .../wwwroot/Avalonia/InputHelper.js | 19 ++ .../wwwroot/Avalonia/NativeControlHost.js | 48 +++++ .../wwwroot/Avalonia/SKHtmlCanvas.js | 172 +++++++++++++++ .../wwwroot/Avalonia/SizeWatcher.js | 39 ++++ .../wwwroot/Storage/IndexedDbWrapper.js | 72 +++++++ .../wwwroot/Storage/StorageProvider.js | 199 ++++++++++++++++++ 28 files changed, 739 insertions(+), 143 deletions(-) create mode 100644 src/Web/Avalonia.Web.Blazor/Interop/AvaloniaModule.cs create mode 100644 src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/Avalonia.ts rename src/Web/Avalonia.Web.Blazor/webapp/modules/{Common => Avalonia}/DpiWatcher.ts (100%) rename src/Web/Avalonia.Web.Blazor/webapp/modules/{Common => Avalonia}/InputHelper.ts (100%) rename src/Web/Avalonia.Web.Blazor/webapp/modules/{Common => Avalonia}/NativeControlHost.ts (100%) rename src/Web/Avalonia.Web.Blazor/webapp/modules/{Common => Avalonia}/SKHtmlCanvas.ts (100%) rename src/Web/Avalonia.Web.Blazor/webapp/modules/{Common => Avalonia}/SizeWatcher.ts (100%) delete mode 100644 src/Web/Avalonia.Web.Blazor/webapp/modules/Common/Common.ts rename src/Web/Avalonia.Web.Blazor/webapp/modules/{Common => Storage}/IndexedDbWrapper.ts (100%) create mode 100644 src/Web/Avalonia.Web.Blazor/wwwroot/Avalonia/Avalonia.js create mode 100644 src/Web/Avalonia.Web.Blazor/wwwroot/Avalonia/DpiWatcher.js create mode 100644 src/Web/Avalonia.Web.Blazor/wwwroot/Avalonia/InputHelper.js create mode 100644 src/Web/Avalonia.Web.Blazor/wwwroot/Avalonia/NativeControlHost.js create mode 100644 src/Web/Avalonia.Web.Blazor/wwwroot/Avalonia/SKHtmlCanvas.js create mode 100644 src/Web/Avalonia.Web.Blazor/wwwroot/Avalonia/SizeWatcher.js create mode 100644 src/Web/Avalonia.Web.Blazor/wwwroot/Storage/IndexedDbWrapper.js create mode 100644 src/Web/Avalonia.Web.Blazor/wwwroot/Storage/StorageProvider.js diff --git a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs index 07db6013c7..d4081ca043 100644 --- a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs +++ b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs @@ -28,6 +28,7 @@ namespace Avalonia.Web.Blazor private SizeWatcherInterop? _sizeWatcher = null; private DpiWatcherInterop? _dpiWatcher = null; private SKHtmlCanvasInterop.GLInfo? _jsGlInfo = null; + private AvaloniaModule? _avaloniaModule = null; private InputHelperInterop? _inputHelper = null; private InputHelperInterop? _canvasHelper = null; private NativeControlHostInterop? _nativeControlHost = null; @@ -241,8 +242,10 @@ namespace Avalonia.Web.Blazor { AvaloniaLocator.CurrentMutable.Bind().ToConstant((IJSInProcessRuntime)Js); - _inputHelper = await InputHelperInterop.ImportAsync(Js, _inputElement); - _canvasHelper = await InputHelperInterop.ImportAsync(Js, _htmlCanvas); + _avaloniaModule = await AvaloniaModule.ImportAsync(Js); + + _inputHelper = new InputHelperInterop(_avaloniaModule, _inputElement); + _canvasHelper = new InputHelperInterop(_avaloniaModule, _htmlCanvas); _inputHelper.Hide(); _canvasHelper.SetCursor("default"); @@ -252,11 +255,11 @@ namespace Avalonia.Web.Blazor _canvasHelper.SetCursor(x); //windows }; - _nativeControlHost = await NativeControlHostInterop.ImportAsync(Js, _nativeControlsContainer); + _nativeControlHost = new NativeControlHostInterop(_avaloniaModule, _nativeControlsContainer); _storageProvider = await StorageProviderInterop.ImportAsync(Js); Console.WriteLine("starting html canvas setup"); - _interop = await SKHtmlCanvasInterop.ImportAsync(Js, _htmlCanvas, OnRenderFrame); + _interop = new SKHtmlCanvasInterop(_avaloniaModule, _htmlCanvas, OnRenderFrame); Console.WriteLine("Interop created"); @@ -306,8 +309,8 @@ namespace Avalonia.Web.Blazor { _interop.RequestAnimationFrame(true); - _sizeWatcher = await SizeWatcherInterop.ImportAsync(Js, _htmlCanvas, OnSizeChanged); - _dpiWatcher = await DpiWatcherInterop.ImportAsync(Js, OnDpiChanged); + _sizeWatcher = new SizeWatcherInterop(_avaloniaModule, _htmlCanvas, OnSizeChanged); + _dpiWatcher = new DpiWatcherInterop(_avaloniaModule, OnDpiChanged); _topLevel.Prepare(); diff --git a/src/Web/Avalonia.Web.Blazor/Interop/AvaloniaModule.cs b/src/Web/Avalonia.Web.Blazor/Interop/AvaloniaModule.cs new file mode 100644 index 0000000000..7aac879fa6 --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/Interop/AvaloniaModule.cs @@ -0,0 +1,18 @@ +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 index 29a2686b3f..2c39cb8d00 100644 --- a/src/Web/Avalonia.Web.Blazor/Interop/DpiWatcherInterop.cs +++ b/src/Web/Avalonia.Web.Blazor/Interop/DpiWatcherInterop.cs @@ -1,43 +1,29 @@ -using System; -using System.Threading.Tasks; using Microsoft.JSInterop; namespace Avalonia.Web.Blazor.Interop { - internal class DpiWatcherInterop : JSModuleInterop + internal class DpiWatcherInterop : IDisposable { - private const string JsFilename = "./_content/Avalonia.Web.Blazor/DpiWatcher.js"; private const string StartSymbol = "DpiWatcher.start"; private const string StopSymbol = "DpiWatcher.stop"; private const string GetDpiSymbol = "DpiWatcher.getDpi"; - private static DpiWatcherInterop? instance; - private event Action? callbacksEvent; - private readonly FloatFloatActionHelper callbackHelper; + private readonly FloatFloatActionHelper _callbackHelper; + private readonly AvaloniaModule _module; private DotNetObjectReference? callbackReference; - public static async Task ImportAsync(IJSRuntime js, Action? callback = null) + public DpiWatcherInterop(AvaloniaModule module, Action? callback = null) { - var interop = Get(js); - await interop.ImportAsync(); - if (callback != null) - interop.Subscribe(callback); - return interop; - } + _module = module; + _callbackHelper = new FloatFloatActionHelper((o, n) => callbacksEvent?.Invoke(n)); - public static DpiWatcherInterop Get(IJSRuntime js) => - instance ??= new DpiWatcherInterop(js); - - private DpiWatcherInterop(IJSRuntime js) - : base(js, JsFilename) - { - callbackHelper = new FloatFloatActionHelper((o, n) => callbacksEvent?.Invoke(n)); + if (callback != null) + Subscribe(callback); } - protected override void OnDisposingModule() => - Stop(); + public void Dispose() => Stop(); public void Subscribe(Action callback) { @@ -65,9 +51,9 @@ namespace Avalonia.Web.Blazor.Interop if (callbackReference != null) return GetDpi(); - callbackReference = DotNetObjectReference.Create(callbackHelper); + callbackReference = DotNetObjectReference.Create(_callbackHelper); - return Invoke(StartSymbol, callbackReference); + return _module.Invoke(StartSymbol, callbackReference); } private void Stop() @@ -75,13 +61,12 @@ namespace Avalonia.Web.Blazor.Interop if (callbackReference == null) return; - Invoke(StopSymbol); + _module.Invoke(StopSymbol); callbackReference?.Dispose(); callbackReference = null; } - public double GetDpi() => - Invoke(GetDpiSymbol); + public double GetDpi() => _module.Invoke(GetDpiSymbol); } } diff --git a/src/Web/Avalonia.Web.Blazor/Interop/InputHelperInterop.cs b/src/Web/Avalonia.Web.Blazor/Interop/InputHelperInterop.cs index 6d71e0c48f..294e71eb1f 100644 --- a/src/Web/Avalonia.Web.Blazor/Interop/InputHelperInterop.cs +++ b/src/Web/Avalonia.Web.Blazor/Interop/InputHelperInterop.cs @@ -1,41 +1,32 @@ using Microsoft.AspNetCore.Components; -using Microsoft.JSInterop; -using SkiaSharp; namespace Avalonia.Web.Blazor.Interop { - internal class InputHelperInterop : JSModuleInterop + internal class InputHelperInterop { - private const string JsFilename = "./_content/Avalonia.Web.Blazor/InputHelper.js"; 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 readonly ElementReference inputElement; + private readonly AvaloniaModule _module; + private readonly ElementReference _inputElement; - public static async Task ImportAsync(IJSRuntime js, ElementReference element) + public InputHelperInterop(AvaloniaModule module, ElementReference element) { - var interop = new InputHelperInterop(js, element); - await interop.ImportAsync(); - return interop; + _module = module; + _inputElement = element; } - public InputHelperInterop(IJSRuntime js, ElementReference element) - : base(js, JsFilename) - { - inputElement = element; - } - - public void Clear() => Invoke(ClearSymbol, inputElement); + public void Clear() => _module.Invoke(ClearSymbol, _inputElement); - public void Focus() => Invoke(FocusSymbol, inputElement); + public void Focus() => _module.Invoke(FocusSymbol, _inputElement); - public void SetCursor(string kind) => Invoke(SetCursorSymbol, inputElement, kind); + public void SetCursor(string kind) => _module.Invoke(SetCursorSymbol, _inputElement, kind); - public void Hide() => Invoke(HideSymbol, inputElement); + public void Hide() => _module.Invoke(HideSymbol, _inputElement); - public void Show() => Invoke(ShowSymbol, inputElement); + public void Show() => _module.Invoke(ShowSymbol, _inputElement); } } diff --git a/src/Web/Avalonia.Web.Blazor/Interop/JSModuleInterop.cs b/src/Web/Avalonia.Web.Blazor/Interop/JSModuleInterop.cs index 589b6b56bb..dca1b53650 100644 --- a/src/Web/Avalonia.Web.Blazor/Interop/JSModuleInterop.cs +++ b/src/Web/Avalonia.Web.Blazor/Interop/JSModuleInterop.cs @@ -1,6 +1,4 @@ -using System; -using System.Threading.Tasks; -using Microsoft.JSInterop; +using Microsoft.JSInterop; namespace Avalonia.Web.Blazor.Interop { @@ -31,16 +29,16 @@ namespace Avalonia.Web.Blazor.Interop protected IJSUnmarshalledObjectReference Module => module ?? throw new InvalidOperationException("Make sure to run ImportAsync() first."); - protected void Invoke(string identifier, params object?[]? args) => + internal void Invoke(string identifier, params object?[]? args) => Module.InvokeVoid(identifier, args); - protected TValue Invoke(string identifier, params object?[]? args) => + internal TValue Invoke(string identifier, params object?[]? args) => Module.Invoke(identifier, args); - protected ValueTask InvokeAsync(string identifier, params object?[]? args) => + internal ValueTask InvokeAsync(string identifier, params object?[]? args) => Module.InvokeVoidAsync(identifier, args); - protected ValueTask InvokeAsync(string identifier, params object?[]? 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 index 48362b03c4..b824fcae46 100644 --- a/src/Web/Avalonia.Web.Blazor/Interop/NativeControlHostImpl.cs +++ b/src/Web/Avalonia.Web.Blazor/Interop/NativeControlHostImpl.cs @@ -1,5 +1,4 @@ -#nullable enable -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using Avalonia.Controls.Platform; using Avalonia.Platform; @@ -10,31 +9,24 @@ using Microsoft.JSInterop; namespace Avalonia.Web.Blazor.Interop { - internal class NativeControlHostInterop : JSModuleInterop, INativeControlHostImpl + internal class NativeControlHostInterop : INativeControlHostImpl { - private const string JsFilename = "./_content/Avalonia.Web.Blazor/NativeControlHost.js"; private const string CreateDefaultChildSymbol = "NativeControlHost.CreateDefaultChild"; private const string CreateAttachmentSymbol = "NativeControlHost.CreateAttachment"; private const string GetReferenceSymbol = "NativeControlHost.GetReference"; - private readonly ElementReference hostElement; + private readonly AvaloniaModule _module; + private readonly ElementReference _hostElement; - public static async Task ImportAsync(IJSRuntime js, ElementReference element) + public NativeControlHostInterop(AvaloniaModule module, ElementReference element) { - var interop = new NativeControlHostInterop(js, element); - await interop.ImportAsync(); - return interop; - } - - public NativeControlHostInterop(IJSRuntime js, ElementReference element) - : base(js, JsFilename) - { - hostElement = element; + _module = module; + _hostElement = element; } public INativeControlHostDestroyableControlHandle CreateDefaultChild(IPlatformHandle parent) { - var element = Invoke(CreateDefaultChildSymbol); + var element = _module.Invoke(CreateDefaultChildSymbol); return new JSObjectControlHandle(element); } @@ -43,9 +35,9 @@ namespace Avalonia.Web.Blazor.Interop Attachment? a = null; try { - using var hostElementJsReference = Invoke(GetReferenceSymbol, hostElement); + using var hostElementJsReference = _module.Invoke(GetReferenceSymbol, _hostElement); var child = create(new JSObjectControlHandle(hostElementJsReference)); - var attachmenetReference = Invoke(CreateAttachmentSymbol); + 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); @@ -62,7 +54,7 @@ namespace Avalonia.Web.Blazor.Interop public INativeControlHostControlTopLevelAttachment CreateNewAttachment(IPlatformHandle handle) { - var attachmenetReference = Invoke(CreateAttachmentSymbol); + var attachmenetReference = _module.Invoke(CreateAttachmentSymbol); var a = new Attachment(attachmenetReference, handle); a.AttachedTo = this; return a; @@ -111,7 +103,7 @@ namespace Avalonia.Web.Blazor.Interop } else { - _native.InvokeVoid(AttachToSymbol, host.hostElement); + _native.InvokeVoid(AttachToSymbol, host._hostElement); } _attachedTo = host; } diff --git a/src/Web/Avalonia.Web.Blazor/Interop/SKHtmlCanvasInterop.cs b/src/Web/Avalonia.Web.Blazor/Interop/SKHtmlCanvasInterop.cs index 9cbbf24086..cf9350fb62 100644 --- a/src/Web/Avalonia.Web.Blazor/Interop/SKHtmlCanvasInterop.cs +++ b/src/Web/Avalonia.Web.Blazor/Interop/SKHtmlCanvasInterop.cs @@ -4,7 +4,7 @@ using SkiaSharp; namespace Avalonia.Web.Blazor.Interop { - internal class SKHtmlCanvasInterop : JSModuleInterop + internal class SKHtmlCanvasInterop : IDisposable { private const string JsFilename = "./_content/Avalonia.Web.Blazor/SKHtmlCanvas.js"; private const string InitGLSymbol = "SKHtmlCanvas.initGL"; @@ -14,39 +14,32 @@ namespace Avalonia.Web.Blazor.Interop private const string SetCanvasSizeSymbol = "SKHtmlCanvas.setCanvasSize"; private const string PutImageDataSymbol = "SKHtmlCanvas.putImageData"; - private readonly ElementReference htmlCanvas; - private readonly string htmlElementId; - private readonly ActionHelper callbackHelper; + private readonly AvaloniaModule _module; + private readonly ElementReference _htmlCanvas; + private readonly string _htmlElementId; + private readonly ActionHelper _callbackHelper; private DotNetObjectReference? callbackReference; - public static async Task ImportAsync(IJSRuntime js, ElementReference element, Action callback) + public SKHtmlCanvasInterop(AvaloniaModule module, ElementReference element, Action renderFrameCallback) { - var interop = new SKHtmlCanvasInterop(js, element, callback); - await interop.ImportAsync(); - return interop; - } - - public SKHtmlCanvasInterop(IJSRuntime js, ElementReference element, Action renderFrameCallback) - : base(js, JsFilename) - { - htmlCanvas = element; - htmlElementId = element.Id; + _module = module; + _htmlCanvas = element; + _htmlElementId = element.Id; - callbackHelper = new ActionHelper(renderFrameCallback); + _callbackHelper = new ActionHelper(renderFrameCallback); } - protected override void OnDisposingModule() => - Deinit(); + 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); + callbackReference = DotNetObjectReference.Create(_callbackHelper); - return Invoke(InitGLSymbol, htmlCanvas, htmlElementId, callbackReference); + return _module.Invoke(InitGLSymbol, _htmlCanvas, _htmlElementId, callbackReference); } public bool InitRaster() @@ -54,9 +47,9 @@ namespace Avalonia.Web.Blazor.Interop if (callbackReference != null) throw new InvalidOperationException("Unable to initialize the same canvas more than once."); - callbackReference = DotNetObjectReference.Create(callbackHelper); + callbackReference = DotNetObjectReference.Create(_callbackHelper); - return Invoke(InitRasterSymbol, htmlCanvas, htmlElementId, callbackReference); + return _module.Invoke(InitRasterSymbol, _htmlCanvas, _htmlElementId, callbackReference); } public void Deinit() @@ -64,19 +57,19 @@ namespace Avalonia.Web.Blazor.Interop if (callbackReference == null) return; - Invoke(DeinitSymbol, htmlElementId); + _module.Invoke(DeinitSymbol, _htmlElementId); callbackReference?.Dispose(); } public void RequestAnimationFrame(bool enableRenderLoop) => - Invoke(RequestAnimationFrameSymbol, htmlCanvas, enableRenderLoop); + _module.Invoke(RequestAnimationFrameSymbol, _htmlCanvas, enableRenderLoop); public void SetCanvasSize(int rawWidth, int rawHeight) => - Invoke(SetCanvasSizeSymbol, htmlCanvas, rawWidth, rawHeight); + _module.Invoke(SetCanvasSizeSymbol, _htmlCanvas, rawWidth, rawHeight); public void PutImageData(IntPtr intPtr, SKSizeI rawSize) => - Invoke(PutImageDataSymbol, htmlCanvas, intPtr.ToInt64(), rawSize.Width, rawSize.Height); + _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 index 8904137b8b..10198751a9 100644 --- a/src/Web/Avalonia.Web.Blazor/Interop/SizeWatcherInterop.cs +++ b/src/Web/Avalonia.Web.Blazor/Interop/SizeWatcherInterop.cs @@ -1,50 +1,39 @@ -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components; using Microsoft.JSInterop; using SkiaSharp; namespace Avalonia.Web.Blazor.Interop { - internal class SizeWatcherInterop : JSModuleInterop + internal class SizeWatcherInterop : IDisposable { - private const string JsFilename = "./_content/Avalonia.Web.Blazor/SizeWatcher.js"; private const string ObserveSymbol = "SizeWatcher.observe"; private const string UnobserveSymbol = "SizeWatcher.unobserve"; - private readonly ElementReference htmlElement; - private readonly string htmlElementId; - private readonly FloatFloatActionHelper callbackHelper; + private readonly AvaloniaModule _module; + private readonly ElementReference _htmlElement; + private readonly string _htmlElementId; + private readonly FloatFloatActionHelper _callbackHelper; private DotNetObjectReference? callbackReference; - public static async Task ImportAsync(IJSRuntime js, ElementReference element, Action callback) + public SizeWatcherInterop(AvaloniaModule module, ElementReference element, Action callback) { - var interop = new SizeWatcherInterop(js, element, callback); - await interop.ImportAsync(); - interop.Start(); - return interop; + _module = module; + _htmlElement = element; + _htmlElementId = element.Id; + _callbackHelper = new FloatFloatActionHelper((x, y) => callback(new SKSize(x, y))); } - public SizeWatcherInterop(IJSRuntime js, ElementReference element, Action callback) - : base(js, JsFilename) - { - htmlElement = element; - htmlElementId = element.Id; - callbackHelper = new FloatFloatActionHelper((x, y) => callback(new SKSize(x, y))); - } - - protected override void OnDisposingModule() => - Stop(); + public void Dispose() => Stop(); public void Start() { if (callbackReference != null) return; - callbackReference = DotNetObjectReference.Create(callbackHelper); + callbackReference = DotNetObjectReference.Create(_callbackHelper); - Invoke(ObserveSymbol, htmlElement, htmlElementId, callbackReference); + _module.Invoke(ObserveSymbol, _htmlElement, _htmlElementId, callbackReference); } public void Stop() @@ -52,7 +41,7 @@ namespace Avalonia.Web.Blazor.Interop if (callbackReference == null) return; - Invoke(UnobserveSymbol, htmlElementId); + _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 index 2bc46e97b5..f6428c8ef1 100644 --- a/src/Web/Avalonia.Web.Blazor/Interop/Storage/StorageProviderInterop.cs +++ b/src/Web/Avalonia.Web.Blazor/Interop/Storage/StorageProviderInterop.cs @@ -12,7 +12,7 @@ namespace Avalonia.Web.Blazor.Interop.Storage internal class StorageProviderInterop : JSModuleInterop, IStorageProvider { - private const string JsFilename = "./_content/Avalonia.Web.Blazor/StorageProvider.js"; + private const string JsFilename = "./_content/Avalonia.Web.Blazor/avaloniaStorage.js"; private const string PickerCancelMessage = "The user aborted a request"; public static async Task ImportAsync(IJSRuntime js) diff --git a/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/Avalonia.ts b/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/Avalonia.ts new file mode 100644 index 0000000000..1ce2e850d9 --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/Avalonia.ts @@ -0,0 +1,5 @@ +export { DpiWatcher } from "./DpiWatcher" +export { InputHelper } from "./InputHelper" +export { NativeControlHost } from "./NativeControlHost" +export { SizeWatcher } from "./SizeWatcher" +export { SKHtmlCanvas } from "./SKHtmlCanvas" diff --git a/src/Web/Avalonia.Web.Blazor/webapp/modules/Common/DpiWatcher.ts b/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/DpiWatcher.ts similarity index 100% rename from src/Web/Avalonia.Web.Blazor/webapp/modules/Common/DpiWatcher.ts rename to src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/DpiWatcher.ts diff --git a/src/Web/Avalonia.Web.Blazor/webapp/modules/Common/InputHelper.ts b/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/InputHelper.ts similarity index 100% rename from src/Web/Avalonia.Web.Blazor/webapp/modules/Common/InputHelper.ts rename to src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/InputHelper.ts diff --git a/src/Web/Avalonia.Web.Blazor/webapp/modules/Common/NativeControlHost.ts b/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/NativeControlHost.ts similarity index 100% rename from src/Web/Avalonia.Web.Blazor/webapp/modules/Common/NativeControlHost.ts rename to src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/NativeControlHost.ts diff --git a/src/Web/Avalonia.Web.Blazor/webapp/modules/Common/SKHtmlCanvas.ts b/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/SKHtmlCanvas.ts similarity index 100% rename from src/Web/Avalonia.Web.Blazor/webapp/modules/Common/SKHtmlCanvas.ts rename to src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/SKHtmlCanvas.ts diff --git a/src/Web/Avalonia.Web.Blazor/webapp/modules/Common/SizeWatcher.ts b/src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/SizeWatcher.ts similarity index 100% rename from src/Web/Avalonia.Web.Blazor/webapp/modules/Common/SizeWatcher.ts rename to src/Web/Avalonia.Web.Blazor/webapp/modules/Avalonia/SizeWatcher.ts diff --git a/src/Web/Avalonia.Web.Blazor/webapp/modules/Common/Common.ts b/src/Web/Avalonia.Web.Blazor/webapp/modules/Common/Common.ts deleted file mode 100644 index 5f282702bb..0000000000 --- a/src/Web/Avalonia.Web.Blazor/webapp/modules/Common/Common.ts +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/Web/Avalonia.Web.Blazor/webapp/modules/Common/IndexedDbWrapper.ts b/src/Web/Avalonia.Web.Blazor/webapp/modules/Storage/IndexedDbWrapper.ts similarity index 100% rename from src/Web/Avalonia.Web.Blazor/webapp/modules/Common/IndexedDbWrapper.ts rename to src/Web/Avalonia.Web.Blazor/webapp/modules/Storage/IndexedDbWrapper.ts diff --git a/src/Web/Avalonia.Web.Blazor/webapp/modules/Storage/StorageProvider.ts b/src/Web/Avalonia.Web.Blazor/webapp/modules/Storage/StorageProvider.ts index 65b8fa908e..896e174e43 100644 --- a/src/Web/Avalonia.Web.Blazor/webapp/modules/Storage/StorageProvider.ts +++ b/src/Web/Avalonia.Web.Blazor/webapp/modules/Storage/StorageProvider.ts @@ -2,7 +2,7 @@ declare global { type WellKnownDirectory = "desktop" | "documents" | "downloads" | "music" | "pictures" | "videos"; - type StartInDirectory = WellKnownDirectory | FileSystemFileHandle; + type StartInDirectory = WellKnownDirectory | FileSystemHandle; interface OpenFilePickerOptions { startIn?: StartInDirectory } @@ -14,10 +14,10 @@ declare global { const fileBookmarksStore: string = "fileBookmarks"; const avaloniaDb = new IndexedDbWrapper("AvaloniaDb", [ fileBookmarksStore -]) +]); class StorageItem { - constructor(private handle: FileSystemHandle, private bookmarkId?: string) { } + constructor(public handle: FileSystemHandle, private bookmarkId?: string) { } public getName(): string { return this.handle.name @@ -69,7 +69,7 @@ class StorageItem { } const items: StorageItem[] = []; - for await (const [key, value] of this.handle.entries()) { + for await (const [key, value] of (this.handle as any).entries()) { items.push(new StorageItem(value)); } return new StorageItems(items); @@ -90,7 +90,7 @@ class StorageItem { if (this.bookmarkId) { return this.bookmarkId; } - + const connection = await avaloniaDb.connect(); try { const key = await connection.put(fileBookmarksStore, this.handle, this.generateBookmarkId()); @@ -172,7 +172,7 @@ export class StorageProvider { }; const handles = await window.showOpenFilePicker(options); - return new StorageItems(handles.map(handle => new StorageItem(handle))); + return new StorageItems(handles.map((handle: FileSystemHandle) => new StorageItem(handle))); } public static async saveFileDialog( diff --git a/src/Web/Avalonia.Web.Blazor/webapp/package.json b/src/Web/Avalonia.Web.Blazor/webapp/package.json index 130e3032b4..c312118a72 100644 --- a/src/Web/Avalonia.Web.Blazor/webapp/package.json +++ b/src/Web/Avalonia.Web.Blazor/webapp/package.json @@ -1,12 +1,14 @@ { "name": "avalonia.web", "scripts": { - "build": "tsc --build", - "clean": "tsc --build --clean" + "dist": "cross-env NODE_ENV=production webpack", + "build": "cross-env NODE_ENV=development webpack" }, "devDependencies": { "@types/emscripten": "^1.39.6", "@types/wicg-file-system-access": "^2020.9.5", + "cross-env": "^7.0.3", + "ts-loader": "^9.3.1", "typescript": "^4.7.4", "webpack": "^5.73.0", "webpack-cli": "^4.10.0" diff --git a/src/Web/Avalonia.Web.Blazor/webapp/webpack.config.js b/src/Web/Avalonia.Web.Blazor/webapp/webpack.config.js index e69de29bb2..52d507a2b2 100644 --- a/src/Web/Avalonia.Web.Blazor/webapp/webpack.config.js +++ b/src/Web/Avalonia.Web.Blazor/webapp/webpack.config.js @@ -0,0 +1,40 @@ +const path = require('path'); +const prod = process.env.NODE_ENV == 'production'; + +module.exports = { + mode: prod ? "production" : "development", + devtool: 'source-map', + target: ["web", "es2020"], + entry: { + avalonia: './modules/Avalonia/Avalonia.ts', + avaloniaStorage: { + import: './modules/Storage/StorageProvider.ts', + dependOn: 'avalonia', + } + }, + output: { + filename: '[name].js', + path: path.resolve(__dirname, '../wwwroot'), + library: { + type: 'module', + }, + }, + module: { + rules: [ + { + test: /\.tsx?$/, + use: 'ts-loader', + exclude: /node_modules/, + }, + ], + }, + resolve: { + extensions: ['.ts', '.js'], + }, + optimization: { + minimize: false + }, + experiments: { + outputModule: true, + } +}; diff --git a/src/Web/Avalonia.Web.Blazor/wwwroot/Avalonia/Avalonia.js b/src/Web/Avalonia.Web.Blazor/wwwroot/Avalonia/Avalonia.js new file mode 100644 index 0000000000..849ea089d5 --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/wwwroot/Avalonia/Avalonia.js @@ -0,0 +1,6 @@ +export { DpiWatcher } from "./DpiWatcher"; +export { InputHelper } from "./InputHelper"; +export { NativeControlHost } from "./NativeControlHost"; +export { SizeWatcher } from "./SizeWatcher"; +export { SKHtmlCanvas } from "./SKHtmlCanvas"; +//# sourceMappingURL=Avalonia.js.map \ No newline at end of file diff --git a/src/Web/Avalonia.Web.Blazor/wwwroot/Avalonia/DpiWatcher.js b/src/Web/Avalonia.Web.Blazor/wwwroot/Avalonia/DpiWatcher.js new file mode 100644 index 0000000000..17b6999108 --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/wwwroot/Avalonia/DpiWatcher.js @@ -0,0 +1,26 @@ +export class DpiWatcher { + static getDpi() { + return window.devicePixelRatio; + } + static start(callback) { + DpiWatcher.lastDpi = window.devicePixelRatio; + DpiWatcher.timerId = window.setInterval(DpiWatcher.update, 1000); + DpiWatcher.callback = callback; + return DpiWatcher.lastDpi; + } + static stop() { + 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); + } + } +} +//# sourceMappingURL=DpiWatcher.js.map \ No newline at end of file diff --git a/src/Web/Avalonia.Web.Blazor/wwwroot/Avalonia/InputHelper.js b/src/Web/Avalonia.Web.Blazor/wwwroot/Avalonia/InputHelper.js new file mode 100644 index 0000000000..97a4500a00 --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/wwwroot/Avalonia/InputHelper.js @@ -0,0 +1,19 @@ +export class InputHelper { + static clear(inputElement) { + inputElement.value = ""; + } + static focus(inputElement) { + inputElement.focus(); + inputElement.setSelectionRange(0, 0); + } + static setCursor(inputElement, kind) { + inputElement.style.cursor = kind; + } + static hide(inputElement) { + inputElement.style.display = 'none'; + } + static show(inputElement) { + inputElement.style.display = 'block'; + } +} +//# sourceMappingURL=InputHelper.js.map \ No newline at end of file diff --git a/src/Web/Avalonia.Web.Blazor/wwwroot/Avalonia/NativeControlHost.js b/src/Web/Avalonia.Web.Blazor/wwwroot/Avalonia/NativeControlHost.js new file mode 100644 index 0000000000..05f804ed8c --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/wwwroot/Avalonia/NativeControlHost.js @@ -0,0 +1,48 @@ +export class NativeControlHost { + static CreateDefaultChild(parent) { + return document.createElement("div"); + } + static GetReference(element) { + return element; + } + static CreateAttachment() { + return new NativeControlHostTopLevelAttachment(); + } +} +class NativeControlHostTopLevelAttachment { + InitializeWithChildHandle(child) { + this._child = child; + this._child.style.position = "absolute"; + } + AttachTo(host) { + 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, y, width, height) { + 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, height) { + if (this._child) { + this._child.style.width = width + "px"; + this._child.style.height = height + "px"; + this._child.style.display = "none"; + } + } + ReleaseChild() { + if (this._child) { + this._child = undefined; + } + } +} +//# sourceMappingURL=NativeControlHost.js.map \ No newline at end of file diff --git a/src/Web/Avalonia.Web.Blazor/wwwroot/Avalonia/SKHtmlCanvas.js b/src/Web/Avalonia.Web.Blazor/wwwroot/Avalonia/SKHtmlCanvas.js new file mode 100644 index 0000000000..5900026acd --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/wwwroot/Avalonia/SKHtmlCanvas.js @@ -0,0 +1,172 @@ +export class SKHtmlCanvas { + constructor(useGL, element, callback) { + this.renderLoopEnabled = false; + this.renderLoopRequest = 0; + 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; + } + 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, callback) { + var view = SKHtmlCanvas.init(true, element, elementId, callback); + if (!view || !view.glInfo) + return null; + return view.glInfo; + } + static initRaster(element, elementId, callback) { + var view = SKHtmlCanvas.init(false, element, elementId, callback); + if (!view) + return false; + return true; + } + static init(useGL, element, elementId, callback) { + var htmlCanvas = element; + 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; + } + static deinit(elementId) { + if (!elementId) + return; + const element = SKHtmlCanvas.elements.get(elementId); + SKHtmlCanvas.elements.delete(elementId); + const htmlCanvas = element; + if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas) + return; + htmlCanvas.SKHtmlCanvas.deinit(); + htmlCanvas.SKHtmlCanvas = undefined; + } + static requestAnimationFrame(element, renderLoop) { + const htmlCanvas = element; + if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas) + return; + htmlCanvas.SKHtmlCanvas.requestAnimationFrame(renderLoop); + } + static setCanvasSize(element, width, height) { + const htmlCanvas = element; + if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas) + return; + htmlCanvas.SKHtmlCanvas.setCanvasSize(width, height); + } + static setEnableRenderLoop(element, enable) { + const htmlCanvas = element; + if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas) + return; + htmlCanvas.SKHtmlCanvas.setEnableRenderLoop(enable); + } + static putImageData(element, pData, width, height) { + const htmlCanvas = element; + if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas) + return; + htmlCanvas.SKHtmlCanvas.putImageData(pData, width, height); + } + deinit() { + this.setEnableRenderLoop(false); + } + setCanvasSize(width, height) { + 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) { + GL.makeContextCurrent(this.glInfo.context); + } + } + requestAnimationFrame(renderLoop) { + if (renderLoop !== undefined && this.renderLoopEnabled !== renderLoop) + this.setEnableRenderLoop(renderLoop); + if (this.renderLoopRequest !== 0) + return; + this.renderLoopRequest = window.requestAnimationFrame(() => { + if (this.glInfo) { + 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; + if (this.renderLoopEnabled) + this.requestAnimationFrame(); + }); + } + setEnableRenderLoop(enable) { + this.renderLoopEnabled = enable; + if (enable) { + this.requestAnimationFrame(); + } + else if (this.renderLoopRequest !== 0) { + window.cancelAnimationFrame(this.renderLoopRequest); + this.renderLoopRequest = 0; + } + } + putImageData(pData, width, height) { + 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; + } + this.htmlCanvas.width = width; + this.htmlCanvas.height = height; + 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) { + 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 = 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; + } +} +//# sourceMappingURL=SKHtmlCanvas.js.map \ No newline at end of file diff --git a/src/Web/Avalonia.Web.Blazor/wwwroot/Avalonia/SizeWatcher.js b/src/Web/Avalonia.Web.Blazor/wwwroot/Avalonia/SizeWatcher.js new file mode 100644 index 0000000000..1c6ce26155 --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/wwwroot/Avalonia/SizeWatcher.js @@ -0,0 +1,39 @@ +export class SizeWatcher { + static observe(element, elementId, callback) { + if (!element || !callback) + return; + SizeWatcher.init(); + const watcherElement = element; + watcherElement.SizeWatcher = { + callback: callback + }; + SizeWatcher.elements.set(elementId, element); + SizeWatcher.observer.observe(element); + SizeWatcher.invoke(element); + } + static unobserve(elementId) { + if (!elementId || !SizeWatcher.observer) + return; + const element = SizeWatcher.elements.get(elementId); + SizeWatcher.elements.delete(elementId); + SizeWatcher.observer.unobserve(element); + } + static init() { + if (SizeWatcher.observer) + return; + SizeWatcher.elements = new Map(); + SizeWatcher.observer = new ResizeObserver((entries) => { + for (let entry of entries) { + SizeWatcher.invoke(entry.target); + } + }); + } + static invoke(element) { + const watcherElement = element; + const instance = watcherElement.SizeWatcher; + if (!instance || !instance.callback) + return; + return instance.callback.invokeMethod('Invoke', element.clientWidth, element.clientHeight); + } +} +//# sourceMappingURL=SizeWatcher.js.map \ No newline at end of file diff --git a/src/Web/Avalonia.Web.Blazor/wwwroot/Storage/IndexedDbWrapper.js b/src/Web/Avalonia.Web.Blazor/wwwroot/Storage/IndexedDbWrapper.js new file mode 100644 index 0000000000..bf9cab01bc --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/wwwroot/Storage/IndexedDbWrapper.js @@ -0,0 +1,72 @@ +class InnerDbConnection { + constructor(database) { + this.database = database; + } + openStore(store, mode) { + const tx = this.database.transaction(store, mode); + return tx.objectStore(store); + } + put(store, obj, key) { + 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); + }; + }); + } + get(store, key) { + 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); + }; + }); + } + delete(store, key) { + const os = this.openStore(store, "readwrite"); + return new Promise((resolve, reject) => { + const response = os.delete(key); + response.onsuccess = () => { + resolve(); + }; + response.onerror = () => { + reject(response.error); + }; + }); + } + close() { + this.database.close(); + } +} +export class IndexedDbWrapper { + constructor(databaseName, objectStores) { + this.databaseName = databaseName; + this.objectStores = objectStores; + } + connect() { + 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); + }; + }); + } +} +//# sourceMappingURL=IndexedDbWrapper.js.map \ No newline at end of file diff --git a/src/Web/Avalonia.Web.Blazor/wwwroot/Storage/StorageProvider.js b/src/Web/Avalonia.Web.Blazor/wwwroot/Storage/StorageProvider.js new file mode 100644 index 0000000000..042a83d177 --- /dev/null +++ b/src/Web/Avalonia.Web.Blazor/wwwroot/Storage/StorageProvider.js @@ -0,0 +1,199 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __asyncValues = (this && this.__asyncValues) || function (o) { + if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined."); + var m = o[Symbol.asyncIterator], i; + return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i); + function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; } + function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); } +}; +import { IndexedDbWrapper } from "./IndexedDbWrapper"; +const fileBookmarksStore = "fileBookmarks"; +const avaloniaDb = new IndexedDbWrapper("AvaloniaDb", [ + fileBookmarksStore +]); +class StorageItem { + constructor(handle, bookmarkId) { + this.handle = handle; + this.bookmarkId = bookmarkId; + } + getName() { + return this.handle.name; + } + getKind() { + return this.handle.kind; + } + openRead() { + return __awaiter(this, void 0, void 0, function* () { + if (!(this.handle instanceof FileSystemFileHandle)) { + throw new Error("StorageItem is not a file"); + } + yield this.verityPermissions('read'); + const file = yield this.handle.getFile(); + return file; + }); + } + openWrite() { + return __awaiter(this, void 0, void 0, function* () { + if (!(this.handle instanceof FileSystemFileHandle)) { + throw new Error("StorageItem is not a file"); + } + yield this.verityPermissions('readwrite'); + return yield this.handle.createWritable({ keepExistingData: true }); + }); + } + getProperties() { + return __awaiter(this, void 0, void 0, function* () { + const file = this.handle instanceof FileSystemFileHandle + && (yield this.handle.getFile()); + if (!file) { + return null; + } + return { + Size: file.size, + LastModified: file.lastModified, + Type: file.type + }; + }); + } + getItems() { + var e_1, _a; + return __awaiter(this, void 0, void 0, function* () { + if (this.handle.kind !== "directory") { + return new StorageItems([]); + } + const items = []; + try { + for (var _b = __asyncValues(this.handle.entries()), _c; _c = yield _b.next(), !_c.done;) { + const [key, value] = _c.value; + items.push(new StorageItem(value)); + } + } + catch (e_1_1) { e_1 = { error: e_1_1 }; } + finally { + try { + if (_c && !_c.done && (_a = _b.return)) yield _a.call(_b); + } + finally { if (e_1) throw e_1.error; } + } + return new StorageItems(items); + }); + } + verityPermissions(mode) { + return __awaiter(this, void 0, void 0, function* () { + if ((yield this.handle.queryPermission({ mode })) === 'granted') { + return; + } + if ((yield this.handle.requestPermission({ mode })) === "denied") { + throw new Error("Read permissions denied"); + } + }); + } + saveBookmark() { + return __awaiter(this, void 0, void 0, function* () { + if (this.bookmarkId) { + return this.bookmarkId; + } + const connection = yield avaloniaDb.connect(); + try { + const key = yield connection.put(fileBookmarksStore, this.handle, this.generateBookmarkId()); + return key; + } + finally { + connection.close(); + } + }); + } + deleteBookmark() { + return __awaiter(this, void 0, void 0, function* () { + if (!this.bookmarkId) { + return; + } + const connection = yield avaloniaDb.connect(); + try { + const key = yield connection.delete(fileBookmarksStore, this.bookmarkId); + } + finally { + connection.close(); + } + }); + } + generateBookmarkId() { + return Date.now().toString(36) + Math.random().toString(36).substring(2); + } +} +class StorageItems { + constructor(items) { + this.items = items; + } + count() { + return this.items.length; + } + at(index) { + return this.items[index]; + } +} +export class StorageProvider { + static canOpen() { + return typeof window.showOpenFilePicker !== 'undefined'; + } + static canSave() { + return typeof window.showSaveFilePicker !== 'undefined'; + } + static canPickFolder() { + return typeof window.showDirectoryPicker !== 'undefined'; + } + static selectFolderDialog(startIn) { + return __awaiter(this, void 0, void 0, function* () { + const options = { + startIn: ((startIn === null || startIn === void 0 ? void 0 : startIn.handle) || undefined) + }; + const handle = yield window.showDirectoryPicker(options); + return new StorageItem(handle); + }); + } + static openFileDialog(startIn, multiple, types, excludeAcceptAllOption) { + return __awaiter(this, void 0, void 0, function* () { + const options = { + startIn: ((startIn === null || startIn === void 0 ? void 0 : startIn.handle) || undefined), + multiple, + excludeAcceptAllOption, + types: (types || undefined) + }; + const handles = yield window.showOpenFilePicker(options); + return new StorageItems(handles.map((handle) => new StorageItem(handle))); + }); + } + static saveFileDialog(startIn, suggestedName, types, excludeAcceptAllOption) { + return __awaiter(this, void 0, void 0, function* () { + const options = { + startIn: ((startIn === null || startIn === void 0 ? void 0 : startIn.handle) || undefined), + suggestedName: (suggestedName || undefined), + excludeAcceptAllOption, + types: (types || undefined) + }; + const handle = yield window.showSaveFilePicker(options); + return new StorageItem(handle); + }); + } + static openBookmark(key) { + return __awaiter(this, void 0, void 0, function* () { + const connection = yield avaloniaDb.connect(); + try { + const handle = yield connection.get(fileBookmarksStore, key); + return handle && new StorageItem(handle, key); + } + finally { + connection.close(); + } + }); + } +} +//# sourceMappingURL=StorageProvider.js.map \ No newline at end of file From 53aa470898113e0a1372545eacd2f495d8dbd950 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 26 Aug 2022 14:16:59 +0100 Subject: [PATCH 3/5] start sizewatcher. --- src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs index d4081ca043..09128d714d 100644 --- a/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs +++ b/src/Web/Avalonia.Web.Blazor/AvaloniaView.razor.cs @@ -312,6 +312,7 @@ namespace Avalonia.Web.Blazor _sizeWatcher = new SizeWatcherInterop(_avaloniaModule, _htmlCanvas, OnSizeChanged); _dpiWatcher = new DpiWatcherInterop(_avaloniaModule, OnDpiChanged); + _sizeWatcher.Start(); _topLevel.Prepare(); _topLevel.Renderer.Start(); From 5501010f2dab979366a3a24a22cfd514d399d360 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 7 Sep 2022 15:17:04 +0100 Subject: [PATCH 4/5] remove wwwroot --- .../wwwroot/Avalonia/Avalonia.js | 6 - .../wwwroot/Avalonia/DpiWatcher.js | 26 --- .../wwwroot/Avalonia/InputHelper.js | 19 -- .../wwwroot/Avalonia/NativeControlHost.js | 48 ----- .../wwwroot/Avalonia/SKHtmlCanvas.js | 172 --------------- .../wwwroot/Avalonia/SizeWatcher.js | 39 ---- .../wwwroot/Storage/IndexedDbWrapper.js | 72 ------- .../wwwroot/Storage/StorageProvider.js | 199 ------------------ 8 files changed, 581 deletions(-) delete mode 100644 src/Web/Avalonia.Web.Blazor/wwwroot/Avalonia/Avalonia.js delete mode 100644 src/Web/Avalonia.Web.Blazor/wwwroot/Avalonia/DpiWatcher.js delete mode 100644 src/Web/Avalonia.Web.Blazor/wwwroot/Avalonia/InputHelper.js delete mode 100644 src/Web/Avalonia.Web.Blazor/wwwroot/Avalonia/NativeControlHost.js delete mode 100644 src/Web/Avalonia.Web.Blazor/wwwroot/Avalonia/SKHtmlCanvas.js delete mode 100644 src/Web/Avalonia.Web.Blazor/wwwroot/Avalonia/SizeWatcher.js delete mode 100644 src/Web/Avalonia.Web.Blazor/wwwroot/Storage/IndexedDbWrapper.js delete mode 100644 src/Web/Avalonia.Web.Blazor/wwwroot/Storage/StorageProvider.js diff --git a/src/Web/Avalonia.Web.Blazor/wwwroot/Avalonia/Avalonia.js b/src/Web/Avalonia.Web.Blazor/wwwroot/Avalonia/Avalonia.js deleted file mode 100644 index 849ea089d5..0000000000 --- a/src/Web/Avalonia.Web.Blazor/wwwroot/Avalonia/Avalonia.js +++ /dev/null @@ -1,6 +0,0 @@ -export { DpiWatcher } from "./DpiWatcher"; -export { InputHelper } from "./InputHelper"; -export { NativeControlHost } from "./NativeControlHost"; -export { SizeWatcher } from "./SizeWatcher"; -export { SKHtmlCanvas } from "./SKHtmlCanvas"; -//# sourceMappingURL=Avalonia.js.map \ No newline at end of file diff --git a/src/Web/Avalonia.Web.Blazor/wwwroot/Avalonia/DpiWatcher.js b/src/Web/Avalonia.Web.Blazor/wwwroot/Avalonia/DpiWatcher.js deleted file mode 100644 index 17b6999108..0000000000 --- a/src/Web/Avalonia.Web.Blazor/wwwroot/Avalonia/DpiWatcher.js +++ /dev/null @@ -1,26 +0,0 @@ -export class DpiWatcher { - static getDpi() { - return window.devicePixelRatio; - } - static start(callback) { - DpiWatcher.lastDpi = window.devicePixelRatio; - DpiWatcher.timerId = window.setInterval(DpiWatcher.update, 1000); - DpiWatcher.callback = callback; - return DpiWatcher.lastDpi; - } - static stop() { - 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); - } - } -} -//# sourceMappingURL=DpiWatcher.js.map \ No newline at end of file diff --git a/src/Web/Avalonia.Web.Blazor/wwwroot/Avalonia/InputHelper.js b/src/Web/Avalonia.Web.Blazor/wwwroot/Avalonia/InputHelper.js deleted file mode 100644 index 97a4500a00..0000000000 --- a/src/Web/Avalonia.Web.Blazor/wwwroot/Avalonia/InputHelper.js +++ /dev/null @@ -1,19 +0,0 @@ -export class InputHelper { - static clear(inputElement) { - inputElement.value = ""; - } - static focus(inputElement) { - inputElement.focus(); - inputElement.setSelectionRange(0, 0); - } - static setCursor(inputElement, kind) { - inputElement.style.cursor = kind; - } - static hide(inputElement) { - inputElement.style.display = 'none'; - } - static show(inputElement) { - inputElement.style.display = 'block'; - } -} -//# sourceMappingURL=InputHelper.js.map \ No newline at end of file diff --git a/src/Web/Avalonia.Web.Blazor/wwwroot/Avalonia/NativeControlHost.js b/src/Web/Avalonia.Web.Blazor/wwwroot/Avalonia/NativeControlHost.js deleted file mode 100644 index 05f804ed8c..0000000000 --- a/src/Web/Avalonia.Web.Blazor/wwwroot/Avalonia/NativeControlHost.js +++ /dev/null @@ -1,48 +0,0 @@ -export class NativeControlHost { - static CreateDefaultChild(parent) { - return document.createElement("div"); - } - static GetReference(element) { - return element; - } - static CreateAttachment() { - return new NativeControlHostTopLevelAttachment(); - } -} -class NativeControlHostTopLevelAttachment { - InitializeWithChildHandle(child) { - this._child = child; - this._child.style.position = "absolute"; - } - AttachTo(host) { - 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, y, width, height) { - 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, height) { - if (this._child) { - this._child.style.width = width + "px"; - this._child.style.height = height + "px"; - this._child.style.display = "none"; - } - } - ReleaseChild() { - if (this._child) { - this._child = undefined; - } - } -} -//# sourceMappingURL=NativeControlHost.js.map \ No newline at end of file diff --git a/src/Web/Avalonia.Web.Blazor/wwwroot/Avalonia/SKHtmlCanvas.js b/src/Web/Avalonia.Web.Blazor/wwwroot/Avalonia/SKHtmlCanvas.js deleted file mode 100644 index 5900026acd..0000000000 --- a/src/Web/Avalonia.Web.Blazor/wwwroot/Avalonia/SKHtmlCanvas.js +++ /dev/null @@ -1,172 +0,0 @@ -export class SKHtmlCanvas { - constructor(useGL, element, callback) { - this.renderLoopEnabled = false; - this.renderLoopRequest = 0; - 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; - } - 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, callback) { - var view = SKHtmlCanvas.init(true, element, elementId, callback); - if (!view || !view.glInfo) - return null; - return view.glInfo; - } - static initRaster(element, elementId, callback) { - var view = SKHtmlCanvas.init(false, element, elementId, callback); - if (!view) - return false; - return true; - } - static init(useGL, element, elementId, callback) { - var htmlCanvas = element; - 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; - } - static deinit(elementId) { - if (!elementId) - return; - const element = SKHtmlCanvas.elements.get(elementId); - SKHtmlCanvas.elements.delete(elementId); - const htmlCanvas = element; - if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas) - return; - htmlCanvas.SKHtmlCanvas.deinit(); - htmlCanvas.SKHtmlCanvas = undefined; - } - static requestAnimationFrame(element, renderLoop) { - const htmlCanvas = element; - if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas) - return; - htmlCanvas.SKHtmlCanvas.requestAnimationFrame(renderLoop); - } - static setCanvasSize(element, width, height) { - const htmlCanvas = element; - if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas) - return; - htmlCanvas.SKHtmlCanvas.setCanvasSize(width, height); - } - static setEnableRenderLoop(element, enable) { - const htmlCanvas = element; - if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas) - return; - htmlCanvas.SKHtmlCanvas.setEnableRenderLoop(enable); - } - static putImageData(element, pData, width, height) { - const htmlCanvas = element; - if (!htmlCanvas || !htmlCanvas.SKHtmlCanvas) - return; - htmlCanvas.SKHtmlCanvas.putImageData(pData, width, height); - } - deinit() { - this.setEnableRenderLoop(false); - } - setCanvasSize(width, height) { - 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) { - GL.makeContextCurrent(this.glInfo.context); - } - } - requestAnimationFrame(renderLoop) { - if (renderLoop !== undefined && this.renderLoopEnabled !== renderLoop) - this.setEnableRenderLoop(renderLoop); - if (this.renderLoopRequest !== 0) - return; - this.renderLoopRequest = window.requestAnimationFrame(() => { - if (this.glInfo) { - 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; - if (this.renderLoopEnabled) - this.requestAnimationFrame(); - }); - } - setEnableRenderLoop(enable) { - this.renderLoopEnabled = enable; - if (enable) { - this.requestAnimationFrame(); - } - else if (this.renderLoopRequest !== 0) { - window.cancelAnimationFrame(this.renderLoopRequest); - this.renderLoopRequest = 0; - } - } - putImageData(pData, width, height) { - 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; - } - this.htmlCanvas.width = width; - this.htmlCanvas.height = height; - 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) { - 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 = 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; - } -} -//# sourceMappingURL=SKHtmlCanvas.js.map \ No newline at end of file diff --git a/src/Web/Avalonia.Web.Blazor/wwwroot/Avalonia/SizeWatcher.js b/src/Web/Avalonia.Web.Blazor/wwwroot/Avalonia/SizeWatcher.js deleted file mode 100644 index 1c6ce26155..0000000000 --- a/src/Web/Avalonia.Web.Blazor/wwwroot/Avalonia/SizeWatcher.js +++ /dev/null @@ -1,39 +0,0 @@ -export class SizeWatcher { - static observe(element, elementId, callback) { - if (!element || !callback) - return; - SizeWatcher.init(); - const watcherElement = element; - watcherElement.SizeWatcher = { - callback: callback - }; - SizeWatcher.elements.set(elementId, element); - SizeWatcher.observer.observe(element); - SizeWatcher.invoke(element); - } - static unobserve(elementId) { - if (!elementId || !SizeWatcher.observer) - return; - const element = SizeWatcher.elements.get(elementId); - SizeWatcher.elements.delete(elementId); - SizeWatcher.observer.unobserve(element); - } - static init() { - if (SizeWatcher.observer) - return; - SizeWatcher.elements = new Map(); - SizeWatcher.observer = new ResizeObserver((entries) => { - for (let entry of entries) { - SizeWatcher.invoke(entry.target); - } - }); - } - static invoke(element) { - const watcherElement = element; - const instance = watcherElement.SizeWatcher; - if (!instance || !instance.callback) - return; - return instance.callback.invokeMethod('Invoke', element.clientWidth, element.clientHeight); - } -} -//# sourceMappingURL=SizeWatcher.js.map \ No newline at end of file diff --git a/src/Web/Avalonia.Web.Blazor/wwwroot/Storage/IndexedDbWrapper.js b/src/Web/Avalonia.Web.Blazor/wwwroot/Storage/IndexedDbWrapper.js deleted file mode 100644 index bf9cab01bc..0000000000 --- a/src/Web/Avalonia.Web.Blazor/wwwroot/Storage/IndexedDbWrapper.js +++ /dev/null @@ -1,72 +0,0 @@ -class InnerDbConnection { - constructor(database) { - this.database = database; - } - openStore(store, mode) { - const tx = this.database.transaction(store, mode); - return tx.objectStore(store); - } - put(store, obj, key) { - 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); - }; - }); - } - get(store, key) { - 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); - }; - }); - } - delete(store, key) { - const os = this.openStore(store, "readwrite"); - return new Promise((resolve, reject) => { - const response = os.delete(key); - response.onsuccess = () => { - resolve(); - }; - response.onerror = () => { - reject(response.error); - }; - }); - } - close() { - this.database.close(); - } -} -export class IndexedDbWrapper { - constructor(databaseName, objectStores) { - this.databaseName = databaseName; - this.objectStores = objectStores; - } - connect() { - 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); - }; - }); - } -} -//# sourceMappingURL=IndexedDbWrapper.js.map \ No newline at end of file diff --git a/src/Web/Avalonia.Web.Blazor/wwwroot/Storage/StorageProvider.js b/src/Web/Avalonia.Web.Blazor/wwwroot/Storage/StorageProvider.js deleted file mode 100644 index 042a83d177..0000000000 --- a/src/Web/Avalonia.Web.Blazor/wwwroot/Storage/StorageProvider.js +++ /dev/null @@ -1,199 +0,0 @@ -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -var __asyncValues = (this && this.__asyncValues) || function (o) { - if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined."); - var m = o[Symbol.asyncIterator], i; - return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i); - function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; } - function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); } -}; -import { IndexedDbWrapper } from "./IndexedDbWrapper"; -const fileBookmarksStore = "fileBookmarks"; -const avaloniaDb = new IndexedDbWrapper("AvaloniaDb", [ - fileBookmarksStore -]); -class StorageItem { - constructor(handle, bookmarkId) { - this.handle = handle; - this.bookmarkId = bookmarkId; - } - getName() { - return this.handle.name; - } - getKind() { - return this.handle.kind; - } - openRead() { - return __awaiter(this, void 0, void 0, function* () { - if (!(this.handle instanceof FileSystemFileHandle)) { - throw new Error("StorageItem is not a file"); - } - yield this.verityPermissions('read'); - const file = yield this.handle.getFile(); - return file; - }); - } - openWrite() { - return __awaiter(this, void 0, void 0, function* () { - if (!(this.handle instanceof FileSystemFileHandle)) { - throw new Error("StorageItem is not a file"); - } - yield this.verityPermissions('readwrite'); - return yield this.handle.createWritable({ keepExistingData: true }); - }); - } - getProperties() { - return __awaiter(this, void 0, void 0, function* () { - const file = this.handle instanceof FileSystemFileHandle - && (yield this.handle.getFile()); - if (!file) { - return null; - } - return { - Size: file.size, - LastModified: file.lastModified, - Type: file.type - }; - }); - } - getItems() { - var e_1, _a; - return __awaiter(this, void 0, void 0, function* () { - if (this.handle.kind !== "directory") { - return new StorageItems([]); - } - const items = []; - try { - for (var _b = __asyncValues(this.handle.entries()), _c; _c = yield _b.next(), !_c.done;) { - const [key, value] = _c.value; - items.push(new StorageItem(value)); - } - } - catch (e_1_1) { e_1 = { error: e_1_1 }; } - finally { - try { - if (_c && !_c.done && (_a = _b.return)) yield _a.call(_b); - } - finally { if (e_1) throw e_1.error; } - } - return new StorageItems(items); - }); - } - verityPermissions(mode) { - return __awaiter(this, void 0, void 0, function* () { - if ((yield this.handle.queryPermission({ mode })) === 'granted') { - return; - } - if ((yield this.handle.requestPermission({ mode })) === "denied") { - throw new Error("Read permissions denied"); - } - }); - } - saveBookmark() { - return __awaiter(this, void 0, void 0, function* () { - if (this.bookmarkId) { - return this.bookmarkId; - } - const connection = yield avaloniaDb.connect(); - try { - const key = yield connection.put(fileBookmarksStore, this.handle, this.generateBookmarkId()); - return key; - } - finally { - connection.close(); - } - }); - } - deleteBookmark() { - return __awaiter(this, void 0, void 0, function* () { - if (!this.bookmarkId) { - return; - } - const connection = yield avaloniaDb.connect(); - try { - const key = yield connection.delete(fileBookmarksStore, this.bookmarkId); - } - finally { - connection.close(); - } - }); - } - generateBookmarkId() { - return Date.now().toString(36) + Math.random().toString(36).substring(2); - } -} -class StorageItems { - constructor(items) { - this.items = items; - } - count() { - return this.items.length; - } - at(index) { - return this.items[index]; - } -} -export class StorageProvider { - static canOpen() { - return typeof window.showOpenFilePicker !== 'undefined'; - } - static canSave() { - return typeof window.showSaveFilePicker !== 'undefined'; - } - static canPickFolder() { - return typeof window.showDirectoryPicker !== 'undefined'; - } - static selectFolderDialog(startIn) { - return __awaiter(this, void 0, void 0, function* () { - const options = { - startIn: ((startIn === null || startIn === void 0 ? void 0 : startIn.handle) || undefined) - }; - const handle = yield window.showDirectoryPicker(options); - return new StorageItem(handle); - }); - } - static openFileDialog(startIn, multiple, types, excludeAcceptAllOption) { - return __awaiter(this, void 0, void 0, function* () { - const options = { - startIn: ((startIn === null || startIn === void 0 ? void 0 : startIn.handle) || undefined), - multiple, - excludeAcceptAllOption, - types: (types || undefined) - }; - const handles = yield window.showOpenFilePicker(options); - return new StorageItems(handles.map((handle) => new StorageItem(handle))); - }); - } - static saveFileDialog(startIn, suggestedName, types, excludeAcceptAllOption) { - return __awaiter(this, void 0, void 0, function* () { - const options = { - startIn: ((startIn === null || startIn === void 0 ? void 0 : startIn.handle) || undefined), - suggestedName: (suggestedName || undefined), - excludeAcceptAllOption, - types: (types || undefined) - }; - const handle = yield window.showSaveFilePicker(options); - return new StorageItem(handle); - }); - } - static openBookmark(key) { - return __awaiter(this, void 0, void 0, function* () { - const connection = yield avaloniaDb.connect(); - try { - const handle = yield connection.get(fileBookmarksStore, key); - return handle && new StorageItem(handle, key); - } - finally { - connection.close(); - } - }); - } -} -//# sourceMappingURL=StorageProvider.js.map \ No newline at end of file From baf29e273d3d72663453f5323221d49e71117cfe Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 7 Sep 2022 15:27:28 +0100 Subject: [PATCH 5/5] ignore wwwroot --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 0f0e153e9c..84faae1806 100644 --- a/.gitignore +++ b/.gitignore @@ -214,3 +214,4 @@ src/Web/Avalonia.Web.Blazor/wwwroot/*.js 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