From 8d6050bf97fbe47044379ca4a7fea1ed96ab66e7 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 24 Sep 2022 11:34:52 +0100 Subject: [PATCH] implement dpi and size tracking. --- .../Avalonia.Web.Sample.csproj | 1 + src/Web/Avalonia.Web.Sample/css/style.css | 10 ++ src/Web/Avalonia.Web.Sample/index.html | 28 +++-- src/Web/Avalonia.Web/AvaloniaRuntime.cs | 12 ++ src/Web/Avalonia.Web/AvaloniaView.cs | 50 ++++---- src/Web/Avalonia.Web/RazorViewTopLevelImpl.cs | 15 +-- .../webapp/modules/avalonia/canvas.ts | 110 ++++++++++++++++++ .../webapp/modules/avalonia/runtime.ts | 6 +- 8 files changed, 188 insertions(+), 44 deletions(-) create mode 100644 src/Web/Avalonia.Web.Sample/css/style.css diff --git a/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj b/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj index 1d03118993..6446bc2618 100644 --- a/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj +++ b/src/Web/Avalonia.Web.Sample/Avalonia.Web.Sample.csproj @@ -19,6 +19,7 @@ + diff --git a/src/Web/Avalonia.Web.Sample/css/style.css b/src/Web/Avalonia.Web.Sample/css/style.css new file mode 100644 index 0000000000..4c8c72c29d --- /dev/null +++ b/src/Web/Avalonia.Web.Sample/css/style.css @@ -0,0 +1,10 @@ +.avalonia-canvas { + opacity: 1; + background-color: red; + position: fixed; + width: 100vw; + height: 100vh; + top: 0px; + left: 0px; + z-index: 500; +} diff --git a/src/Web/Avalonia.Web.Sample/index.html b/src/Web/Avalonia.Web.Sample/index.html index 518abd4a15..c67096b3d2 100644 --- a/src/Web/Avalonia.Web.Sample/index.html +++ b/src/Web/Avalonia.Web.Sample/index.html @@ -4,17 +4,29 @@ - Avalonia.Web.Sample - - - - - + Avalonia.Web.Sample + + + + + -
- + +
+ diff --git a/src/Web/Avalonia.Web/AvaloniaRuntime.cs b/src/Web/Avalonia.Web/AvaloniaRuntime.cs index 13b53f6cc4..c7c0f79cab 100644 --- a/src/Web/Avalonia.Web/AvaloniaRuntime.cs +++ b/src/Web/Avalonia.Web/AvaloniaRuntime.cs @@ -64,6 +64,18 @@ public partial class AvaloniaRuntime [JSMarshalAs] Action renderFrameCallback); + [JSImport("SizeWatcher.observe", "avalonia.ts")] + public static partial JSObject ObserveSize( + JSObject canvas, + string canvasId, + [JSMarshalAs>] + Action onSizeChanged); + + [JSImport("DpiWatcher.start", "avalonia.ts")] + public static partial double ObserveDpi( + [JSMarshalAs>] + Action onDpiChanged); + [JSImport("StorageProvider.isFileApiSupported", "storage.ts")] public static partial bool IsFileApiSupported(); diff --git a/src/Web/Avalonia.Web/AvaloniaView.cs b/src/Web/Avalonia.Web/AvaloniaView.cs index 2ee4655e40..de93614162 100644 --- a/src/Web/Avalonia.Web/AvaloniaView.cs +++ b/src/Web/Avalonia.Web/AvaloniaView.cs @@ -36,8 +36,9 @@ namespace Avalonia.Web private ElementReference _inputElement; private ElementReference _containerElement; private ElementReference _nativeControlsContainer;*/ + private JSObject _canvas; private double _dpi = 1; - private SKSize _canvasSize = new(100, 100); + private Size _canvasSize = new(100.0, 100.0); private GRContext? _context; private GRGlInterface? _glInterface; @@ -50,8 +51,8 @@ namespace Avalonia.Web { var div = GetElementById("out"); - var canvas = CreateCanvas(div); - canvas.SetProperty("id", "mycanvas"); + _canvas = CreateCanvas(div); + _canvas.SetProperty("id", "mycanvas"); _topLevelImpl = new RazorViewTopLevelImpl(this); @@ -66,26 +67,17 @@ namespace Avalonia.Web (code, key, modifier) => _topLevelImpl.RawKeyboardEvent(Input.Raw.RawKeyEventType.KeyDown, code, key, (Input.RawInputModifiers)modifier), (code, key, modifier) => _topLevelImpl.RawKeyboardEvent(Input.Raw.RawKeyEventType.KeyUp, code, key, (Input.RawInputModifiers)modifier)); - //_interop = new SKHtmlCanvasInterop(_avaloniaModule, _htmlCanvas, OnRenderFrame); + var skiaOptions = AvaloniaLocator.Current.GetService(); + _dpi = ObserveDpi(OnDpiChanged); - var skiaOptions = AvaloniaLocator.Current.GetService(); - _useGL = skiaOptions?.CustomGpuFactory != null; + Console.WriteLine($"Started observing dpi: {_dpi}"); - if (_useGL) - { - _jsGlInfo = AvaloniaRuntime.InitialiseGL(canvas, OnRenderFrame); - Console.WriteLine("jsglinfo created - init gl"); - } - else - { - throw new NotImplementedException(); - //var rasterInitialized = _interop.InitRaster(); - //Console.WriteLine("raster initialized: {0}", rasterInitialized); - } + _useGL = skiaOptions?.CustomGpuFactory != null; if (_useGL) { + _jsGlInfo = AvaloniaRuntime.InitialiseGL(_canvas, OnRenderFrame); // create the SkiaSharp context if (_context == null) { @@ -93,25 +85,29 @@ namespace Avalonia.Web _glInterface = GRGlInterface.Create(); _context = GRContext.CreateGl(_glInterface); - // bump the default resource cache limit _context.SetResourceCacheLimit(skiaOptions?.MaxGpuResourceSizeBytes ?? 32 * 1024 * 1024); Console.WriteLine("glcontext created and resource limit set"); } - _topLevelImpl.Surfaces = new[] { new BlazorSkiaSurface(_context, _jsGlInfo, ColorType, new PixelSize(100, 100), 1, GRSurfaceOrigin.BottomLeft) }; + _topLevelImpl.Surfaces = new[] { new BlazorSkiaSurface(_context, _jsGlInfo, ColorType, new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi, GRSurfaceOrigin.BottomLeft) }; } else { + //var rasterInitialized = _interop.InitRaster(); + //Console.WriteLine("raster initialized: {0}", rasterInitialized); + //_topLevelImpl.SetSurface(ColorType, - // new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi, _interop.PutImageData); + // new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi, _interop.PutImageData); } - AvaloniaRuntime.SetCanvasSize(canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); + AvaloniaRuntime.SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); _topLevelImpl.SetClientSize(_canvasSize, _dpi); - RequestAnimationFrame(canvas, true); + ObserveSize(_canvas, "mycanvas", OnSizeChanged); + + RequestAnimationFrame(_canvas, true); } private void OnRenderFrame() { @@ -164,13 +160,13 @@ namespace Avalonia.Web } } - private void OnDpiChanged(double newDpi) + private void OnDpiChanged(double oldDpi, double newDpi) { if (Math.Abs(_dpi - newDpi) > 0.0001) { _dpi = newDpi; - //_interop!.SetCanvasSize((int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); + SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); _topLevelImpl.SetClientSize(_canvasSize, _dpi); @@ -178,13 +174,15 @@ namespace Avalonia.Web } } - private void OnSizeChanged(SKSize newSize) + private void OnSizeChanged(int height, int width) { + var newSize = new Size(height, width); + if (_canvasSize != newSize) { _canvasSize = newSize; - //_interop!.SetCanvasSize((int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); + SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); _topLevelImpl.SetClientSize(_canvasSize, _dpi); diff --git a/src/Web/Avalonia.Web/RazorViewTopLevelImpl.cs b/src/Web/Avalonia.Web/RazorViewTopLevelImpl.cs index 2bcc36e687..3417dd18e6 100644 --- a/src/Web/Avalonia.Web/RazorViewTopLevelImpl.cs +++ b/src/Web/Avalonia.Web/RazorViewTopLevelImpl.cs @@ -21,7 +21,6 @@ namespace Avalonia.Web.Blazor internal class RazorViewTopLevelImpl : ITopLevelImplWithTextInputMethod, ITopLevelImplWithNativeControlHost, ITopLevelImplWithStorageProvider { private Size _clientSize; - private IBlazorSkiaSurface? _currentSurface; private IInputRoot? _inputRoot; private readonly Stopwatch _sw = Stopwatch.StartNew(); private readonly AvaloniaView _avaloniaView; @@ -44,15 +43,13 @@ namespace Avalonia.Web.Blazor - public void SetClientSize(SKSize size, double dpi) + public void SetClientSize(Size newSize, double dpi) { - var newSize = new Size(size.Width, size.Height); - if (Math.Abs(RenderScaling - dpi) > 0.0001) { - if (_currentSurface is { }) + if (Surfaces.FirstOrDefault() is BlazorSkiaSurface surface) { - _currentSurface.Scaling = dpi; + surface.Scaling = dpi; } ScalingChanged?.Invoke(dpi); @@ -62,9 +59,9 @@ namespace Avalonia.Web.Blazor { _clientSize = newSize; - if (_currentSurface is { }) + if (Surfaces.FirstOrDefault() is BlazorSkiaSurface surface) { - _currentSurface.Size = new PixelSize((int)size.Width, (int)size.Height); + surface.Size = new PixelSize((int)newSize.Width, (int)newSize.Height); } Resized?.Invoke(newSize, PlatformResizeReason.User); @@ -192,7 +189,7 @@ namespace Avalonia.Web.Blazor public Size ClientSize => _clientSize; public Size? FrameSize => null; - public double RenderScaling => _currentSurface?.Scaling ?? 1; + public double RenderScaling => (Surfaces.FirstOrDefault() as BlazorSkiaSurface)?.Scaling ?? 1; public IEnumerable Surfaces { get; set; } diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts index a6e3c5acb4..423689cde9 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/canvas.ts @@ -27,6 +27,7 @@ export class Canvas { var canvas = document.createElement("canvas"); element.appendChild(canvas); + canvas.classList.add('avalonia-canvas'); return canvas; } @@ -202,3 +203,112 @@ export class Canvas { return ctx; } } + +type SizeWatcherElement = { + SizeWatcher: SizeWatcherInstance; +} & HTMLElement + +type SizeWatcherInstance = { + callback: (width: number, height: number) => void; +} + +export class SizeWatcher { + static observer: ResizeObserver; + static elements: Map; + + public static observe(element: HTMLElement, elementId: string, callback: (width: number, height: number) => void) { + if (!element || !callback) + return; + + //console.info(`Adding size watcher observation with callback ${callback._id}...`); + + SizeWatcher.init(); + + const watcherElement = element as SizeWatcherElement; + watcherElement.SizeWatcher = { + callback: callback + }; + + SizeWatcher.elements.set(elementId, element); + SizeWatcher.observer.observe(element); + + SizeWatcher.invoke(element); + } + + public static unobserve(elementId: string) { + if (!elementId || !SizeWatcher.observer) + return; + + //console.info('Removing size watcher observation...'); + + const element = SizeWatcher.elements.get(elementId)!; + + SizeWatcher.elements.delete(elementId); + SizeWatcher.observer.unobserve(element); + } + + static init() { + if (SizeWatcher.observer) + return; + + //console.info('Starting size watcher...'); + + SizeWatcher.elements = new Map(); + SizeWatcher.observer = new ResizeObserver((entries) => { + for (let entry of entries) { + SizeWatcher.invoke(entry.target); + } + }); + } + + static invoke(element: Element) { + const watcherElement = element as SizeWatcherElement; + const instance = watcherElement.SizeWatcher; + + if (!instance || !instance.callback) + return; + + return instance.callback(element.clientWidth, element.clientHeight); + } +} + +export class DpiWatcher { + static lastDpi: number; + static timerId: number; + static callback: (old: number, newdpi: number) => void; + + public static getDpi() { + return window.devicePixelRatio; + } + + public static start(callback: (old: number, newdpi: number) => void) : number { + //console.info(`Starting DPI watcher with callback ${callback._id}...`); + + DpiWatcher.lastDpi = window.devicePixelRatio; + DpiWatcher.timerId = window.setInterval(DpiWatcher.update, 1000); + DpiWatcher.callback = callback; + + return DpiWatcher.lastDpi; + } + + public static stop() { + //console.info(`Stopping DPI watcher with callback ${DpiWatcher.callback._id}...`); + + window.clearInterval(DpiWatcher.timerId); + + //DpiWatcher.callback = undefined; + } + + static update() { + if (!DpiWatcher.callback) + return; + + const currentDpi = window.devicePixelRatio; + const lastDpi = DpiWatcher.lastDpi; + DpiWatcher.lastDpi = currentDpi; + + if (Math.abs(lastDpi - currentDpi) > 0.001) { + DpiWatcher.callback(lastDpi, currentDpi); + } + } +} diff --git a/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts b/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts index 2f154aeb3f..d54583b15e 100644 --- a/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts +++ b/src/Web/Avalonia.Web/webapp/modules/avalonia/runtime.ts @@ -1,5 +1,7 @@ import { RuntimeAPI } from "../../types/dotnet"; +import { SizeWatcher } from "./canvas"; +import { DpiWatcher } from "./canvas"; import { Canvas } from "./canvas"; import { InputHelper } from "./input"; @@ -10,7 +12,9 @@ export class AvaloniaRuntime { ) { api.setModuleImports("avalonia.ts", { Canvas, - InputHelper + InputHelper, + SizeWatcher, + DpiWatcher }); } }