From 9dc3495da0b09621f98062f7b18092fb87b05cd2 Mon Sep 17 00:00:00 2001 From: Johan Polson <40406620+johanpolson@users.noreply.github.com> Date: Tue, 27 Feb 2024 09:03:23 +0100 Subject: [PATCH] Fix for bugg when move window between screens with different DPI (#14641) * init * init * Rework canvas resize (#14641) --- src/Browser/Avalonia.Browser/AvaloniaView.cs | 36 ++--- .../Avalonia.Browser/BrowserTopLevelImpl.cs | 1 + .../Avalonia.Browser/Interop/DomHelper.cs | 16 +- .../Skia/BrowserSkiaGpuRenderTarget.cs | 5 +- .../Skia/BrowserSkiaSurface.cs | 5 +- .../webapp/modules/avalonia.ts | 5 +- .../webapp/modules/avalonia/canvas.ts | 150 ++++++------------ 7 files changed, 70 insertions(+), 148 deletions(-) diff --git a/src/Browser/Avalonia.Browser/AvaloniaView.cs b/src/Browser/Avalonia.Browser/AvaloniaView.cs index bba1c8bb0d..6b32abe74f 100644 --- a/src/Browser/Avalonia.Browser/AvaloniaView.cs +++ b/src/Browser/Avalonia.Browser/AvaloniaView.cs @@ -105,8 +105,6 @@ namespace Avalonia.Browser var skiaOptions = AvaloniaLocator.Current.GetService(); - _dpi = DomHelper.ObserveDpi(OnDpiChanged); - _useGL = AvaloniaLocator.Current.GetService() != null; if (_useGL) @@ -125,7 +123,7 @@ namespace Avalonia.Browser _topLevelImpl.Surfaces = new[] { new BrowserSkiaSurface(_context, _jsGlInfo, ColorType, - new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _dpi, + new PixelSize((int)_canvasSize.Width, (int)_canvasSize.Height), _canvasSize,_dpi, GRSurfaceOrigin.BottomLeft) }; } @@ -135,11 +133,7 @@ namespace Avalonia.Browser .Log(this, "[Avalonia]: Unable to initialize Canvas surface."); } - CanvasHelper.SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); - - _topLevelImpl.SetClientSize(_canvasSize, _dpi); - - DomHelper.ObserveSize(host, null, OnSizeChanged); + DomHelper.ObserveSize(host, OnSizeOrDpiChanged); CanvasHelper.RequestAnimationFrame(_canvas, true); @@ -464,31 +458,19 @@ namespace Avalonia.Browser } } - private void OnDpiChanged(double oldDpi, double newDpi) + private void OnSizeOrDpiChanged(double displayWidth, double displayHeight, double dpi) { - if (Math.Abs(_dpi - newDpi) > 0.0001) - { - _dpi = newDpi; + var newSize = new Size(displayWidth, displayHeight); - CanvasHelper.SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); - - _topLevelImpl.SetClientSize(_canvasSize, _dpi); - - ForceBlit(); - } - } - - private void OnSizeChanged(int height, int width) - { - var newSize = new Size(height, width); - - if (_canvasSize != newSize) + if (_canvasSize != newSize || _dpi != dpi) { + _dpi = dpi; + _canvasSize = newSize; - CanvasHelper.SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); + CanvasHelper.SetCanvasSize(_canvas, (int)_canvasSize.Width, (int)_canvasSize.Height); - _topLevelImpl.SetClientSize(_canvasSize, _dpi); + _topLevelImpl.SetClientSize(new(displayWidth / dpi, displayHeight / dpi), dpi); ForceBlit(); } diff --git a/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs b/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs index cda0c02a2a..3d3887f73d 100644 --- a/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs +++ b/src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs @@ -73,6 +73,7 @@ namespace Avalonia.Browser if (Surfaces.FirstOrDefault() is BrowserSkiaSurface surface) { surface.Size = new PixelSize((int)newSize.Width, (int)newSize.Height); + surface.DisplaySize = newSize; } Resized?.Invoke(newSize, WindowResizeReason.User); diff --git a/src/Browser/Avalonia.Browser/Interop/DomHelper.cs b/src/Browser/Avalonia.Browser/Interop/DomHelper.cs index c1811bb117..9cd7261994 100644 --- a/src/Browser/Avalonia.Browser/Interop/DomHelper.cs +++ b/src/Browser/Avalonia.Browser/Interop/DomHelper.cs @@ -23,18 +23,12 @@ internal static partial class DomHelper [JSImport("AvaloniaDOM.addClass", AvaloniaModule.MainModuleName)] public static partial void AddCssClass(JSObject element, string className); - [JSImport("SizeWatcher.observe", AvaloniaModule.MainModuleName)] - public static partial JSObject ObserveSize( + [JSImport("ResizeHandler.observeSize", AvaloniaModule.MainModuleName)] + public static partial void ObserveSize( JSObject canvas, - string? canvasId, - [JSMarshalAs>] - Action onSizeChanged); - - [JSImport("DpiWatcher.start", AvaloniaModule.MainModuleName)] - public static partial double ObserveDpi( - [JSMarshalAs>] - Action onDpiChanged); - + [JSMarshalAs>] + Action onSizeOrDpiChanged); + [JSImport("AvaloniaDOM.observeDarkMode", AvaloniaModule.MainModuleName)] public static partial JSObject ObserveDarkMode( [JSMarshalAs>] diff --git a/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpuRenderTarget.cs b/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpuRenderTarget.cs index 9424122ce8..9ede45a3eb 100644 --- a/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpuRenderTarget.cs +++ b/src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpuRenderTarget.cs @@ -1,3 +1,4 @@ +using System; using Avalonia.Skia; using SkiaSharp; @@ -16,8 +17,8 @@ namespace Avalonia.Browser.Skia var glFbInfo = new GRGlFramebufferInfo(browserSkiaSurface.GlInfo.FboId, browserSkiaSurface.ColorType.ToGlSizedFormat()); _browserSkiaSurface = browserSkiaSurface; _renderTarget = new GRBackendRenderTarget( - (int)(browserSkiaSurface.Size.Width * browserSkiaSurface.Scaling), - (int)(browserSkiaSurface.Size.Height * browserSkiaSurface.Scaling), + (int)Math.Round(browserSkiaSurface.DisplaySize.Width * browserSkiaSurface.Scaling), + (int)Math.Round(browserSkiaSurface.DisplaySize.Height * browserSkiaSurface.Scaling), browserSkiaSurface.GlInfo.Samples, browserSkiaSurface.GlInfo.Stencils, glFbInfo); } diff --git a/src/Browser/Avalonia.Browser/Skia/BrowserSkiaSurface.cs b/src/Browser/Avalonia.Browser/Skia/BrowserSkiaSurface.cs index 418b34a975..9170aab782 100644 --- a/src/Browser/Avalonia.Browser/Skia/BrowserSkiaSurface.cs +++ b/src/Browser/Avalonia.Browser/Skia/BrowserSkiaSurface.cs @@ -5,12 +5,13 @@ namespace Avalonia.Browser.Skia { internal class BrowserSkiaSurface : IBrowserSkiaSurface { - public BrowserSkiaSurface(GRContext context, GLInfo glInfo, SKColorType colorType, PixelSize size, double scaling, GRSurfaceOrigin origin) + public BrowserSkiaSurface(GRContext context, GLInfo glInfo, SKColorType colorType, PixelSize size, Size displaySize, double scaling, GRSurfaceOrigin origin) { Context = context; GlInfo = glInfo; ColorType = colorType; Size = size; + DisplaySize = displaySize; Scaling = scaling; Origin = origin; } @@ -19,6 +20,8 @@ namespace Avalonia.Browser.Skia public PixelSize Size { get; set; } + public Size DisplaySize { get; set; } + public GRContext Context { get; set; } public GRSurfaceOrigin Origin { get; set; } diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts index 2b69254cf2..212d67211c 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts @@ -1,4 +1,4 @@ -import { SizeWatcher, DpiWatcher, Canvas } from "./avalonia/canvas"; +import { ResizeHandler, Canvas } from "./avalonia/canvas"; import { InputHelper } from "./avalonia/input"; import { AvaloniaDOM } from "./avalonia/dom"; import { Caniuse } from "./avalonia/caniuse"; @@ -17,8 +17,7 @@ export { Caniuse, Canvas, InputHelper, - SizeWatcher, - DpiWatcher, + ResizeHandler, AvaloniaDOM, StreamHelper, NativeControlHost, diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/canvas.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/canvas.ts index 800a93a220..4058cdd31d 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/canvas.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/canvas.ts @@ -194,122 +194,64 @@ export class Canvas { } } -type SizeWatcherElement = { - SizeWatcher: SizeWatcherInstance; -} & HTMLElement; +type ResizeHandlerCallback = (displayWidth: number, displayHeight: number, dpi: number) => void; -interface SizeWatcherInstance { - callback: (width: number, height: number) => void; -} +type ResizeObserverWithCallbacks = { + callbacks: Map; +} & ResizeObserver; -export class SizeWatcher { - static observer: ResizeObserver; - static elements: Map; - private static lastMove: number; - private static timeoutHandle?: number; +export class ResizeHandler { + private static resizeObserver?: ResizeObserverWithCallbacks; - public static observe(element: HTMLElement, elementId: string | undefined, callback: (width: number, height: number) => void): void { - if (!element || !callback) { - return; + public static observeSize(element: HTMLElement, callback: ResizeHandlerCallback): (() => void) { + if (!this.resizeObserver) { + this.resizeObserver = new ResizeObserver(this.onResize) as ResizeObserverWithCallbacks; + this.resizeObserver.callbacks = new Map(); } - SizeWatcher.lastMove = Date.now(); - - callback(element.clientWidth, element.clientHeight); + this.resizeObserver.callbacks.set(element, callback); + this.resizeObserver.observe(element, { box: "content-box" }); - const handleResize = (args: UIEvent) => { - if (Date.now() - SizeWatcher.lastMove > 33) { - callback(element.clientWidth, element.clientHeight); - SizeWatcher.lastMove = Date.now(); - if (SizeWatcher.timeoutHandle) { - clearTimeout(SizeWatcher.timeoutHandle); - SizeWatcher.timeoutHandle = undefined; - } - } else { - if (SizeWatcher.timeoutHandle) { - clearTimeout(SizeWatcher.timeoutHandle); - } - - SizeWatcher.timeoutHandle = setTimeout(() => { - callback(element.clientWidth, element.clientHeight); - SizeWatcher.lastMove = Date.now(); - SizeWatcher.timeoutHandle = undefined; - }, 100); - } + return () => { + this.resizeObserver?.callbacks.delete(element); + this.resizeObserver?.unobserve(element); }; - - window.addEventListener("resize", handleResize); } - public static unobserve(elementId: string): void { - if (!elementId || !SizeWatcher.observer) { - return; - } - - const element = SizeWatcher.elements.get(elementId); - if (element) { - SizeWatcher.elements.delete(elementId); - SizeWatcher.observer.unobserve(element); - } - } - - static init(): void { - if (SizeWatcher.observer) { - return; - } - - SizeWatcher.elements = new Map(); - SizeWatcher.observer = new ResizeObserver((entries) => { - for (const entry of entries) { - SizeWatcher.invoke(entry.target); + private static onResize(entries: ResizeObserverEntry[], observer: ResizeObserver) { + for (const entry of entries) { + const callback = (observer as ResizeObserverWithCallbacks).callbacks.get(entry.target); + if (!callback) { + continue; } - }); - } - - static invoke(element: Element): void { - 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(): number { - return window.devicePixelRatio; - } - - public static start(callback: (old: number, newdpi: number) => void): number { - DpiWatcher.lastDpi = window.devicePixelRatio; - DpiWatcher.timerId = window.setInterval(DpiWatcher.update, 1000); - DpiWatcher.callback = callback; - - return DpiWatcher.lastDpi; - } - - public static stop(): void { - window.clearInterval(DpiWatcher.timerId); - } - - static update(): void { - if (!DpiWatcher.callback) { - return; - } - - const currentDpi = window.devicePixelRatio; - const lastDpi = DpiWatcher.lastDpi; - DpiWatcher.lastDpi = currentDpi; + const trueDpr = window.devicePixelRatio; + let width; + let height; + let dpr = trueDpr; + if (entry.devicePixelContentBoxSize) { + // NOTE: Only this path gives the correct answer + // The other paths are imperfect fallbacks + // for browsers that don't provide anyway to do this + width = entry.devicePixelContentBoxSize[0].inlineSize; + height = entry.devicePixelContentBoxSize[0].blockSize; + dpr = 1; // it's already in width and height + } else if (entry.contentBoxSize) { + if (entry.contentBoxSize[0]) { + width = entry.contentBoxSize[0].inlineSize; + height = entry.contentBoxSize[0].blockSize; + } else { + width = (entry.contentBoxSize as any).inlineSize; + height = (entry.contentBoxSize as any).blockSize; + } + } else { + width = entry.contentRect.width; + height = entry.contentRect.height; + } + const displayWidth = Math.round(width * dpr); + const displayHeight = Math.round(height * dpr); - if (Math.abs(lastDpi - currentDpi) > 0.001) { - DpiWatcher.callback(lastDpi, currentDpi); + callback(displayWidth, displayHeight, trueDpr); } } }