Browse Source

Fix for bugg when move window between screens with different DPI (#14641)

* init

* init

* Rework canvas resize (#14641)
pull/14744/head
Johan Polson 2 years ago
committed by GitHub
parent
commit
9dc3495da0
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 36
      src/Browser/Avalonia.Browser/AvaloniaView.cs
  2. 1
      src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs
  3. 16
      src/Browser/Avalonia.Browser/Interop/DomHelper.cs
  4. 5
      src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpuRenderTarget.cs
  5. 5
      src/Browser/Avalonia.Browser/Skia/BrowserSkiaSurface.cs
  6. 5
      src/Browser/Avalonia.Browser/webapp/modules/avalonia.ts
  7. 150
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/canvas.ts

36
src/Browser/Avalonia.Browser/AvaloniaView.cs

@ -105,8 +105,6 @@ namespace Avalonia.Browser
var skiaOptions = AvaloniaLocator.Current.GetService<SkiaOptions>(); var skiaOptions = AvaloniaLocator.Current.GetService<SkiaOptions>();
_dpi = DomHelper.ObserveDpi(OnDpiChanged);
_useGL = AvaloniaLocator.Current.GetService<IPlatformGraphics>() != null; _useGL = AvaloniaLocator.Current.GetService<IPlatformGraphics>() != null;
if (_useGL) if (_useGL)
@ -125,7 +123,7 @@ namespace Avalonia.Browser
_topLevelImpl.Surfaces = new[] _topLevelImpl.Surfaces = new[]
{ {
new BrowserSkiaSurface(_context, _jsGlInfo, ColorType, 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) GRSurfaceOrigin.BottomLeft)
}; };
} }
@ -135,11 +133,7 @@ namespace Avalonia.Browser
.Log(this, "[Avalonia]: Unable to initialize Canvas surface."); .Log(this, "[Avalonia]: Unable to initialize Canvas surface.");
} }
CanvasHelper.SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); DomHelper.ObserveSize(host, OnSizeOrDpiChanged);
_topLevelImpl.SetClientSize(_canvasSize, _dpi);
DomHelper.ObserveSize(host, null, OnSizeChanged);
CanvasHelper.RequestAnimationFrame(_canvas, true); 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) var newSize = new Size(displayWidth, displayHeight);
{
_dpi = newDpi;
CanvasHelper.SetCanvasSize(_canvas, (int)(_canvasSize.Width * _dpi), (int)(_canvasSize.Height * _dpi)); if (_canvasSize != newSize || _dpi != dpi)
_topLevelImpl.SetClientSize(_canvasSize, _dpi);
ForceBlit();
}
}
private void OnSizeChanged(int height, int width)
{
var newSize = new Size(height, width);
if (_canvasSize != newSize)
{ {
_dpi = dpi;
_canvasSize = newSize; _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(); ForceBlit();
} }

1
src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs

@ -73,6 +73,7 @@ namespace Avalonia.Browser
if (Surfaces.FirstOrDefault() is BrowserSkiaSurface surface) if (Surfaces.FirstOrDefault() is BrowserSkiaSurface surface)
{ {
surface.Size = new PixelSize((int)newSize.Width, (int)newSize.Height); surface.Size = new PixelSize((int)newSize.Width, (int)newSize.Height);
surface.DisplaySize = newSize;
} }
Resized?.Invoke(newSize, WindowResizeReason.User); Resized?.Invoke(newSize, WindowResizeReason.User);

16
src/Browser/Avalonia.Browser/Interop/DomHelper.cs

@ -23,18 +23,12 @@ internal static partial class DomHelper
[JSImport("AvaloniaDOM.addClass", AvaloniaModule.MainModuleName)] [JSImport("AvaloniaDOM.addClass", AvaloniaModule.MainModuleName)]
public static partial void AddCssClass(JSObject element, string className); public static partial void AddCssClass(JSObject element, string className);
[JSImport("SizeWatcher.observe", AvaloniaModule.MainModuleName)] [JSImport("ResizeHandler.observeSize", AvaloniaModule.MainModuleName)]
public static partial JSObject ObserveSize( public static partial void ObserveSize(
JSObject canvas, JSObject canvas,
string? canvasId, [JSMarshalAs<JSType.Function<JSType.Number, JSType.Number, JSType.Number>>]
[JSMarshalAs<JSType.Function<JSType.Number, JSType.Number>>] Action<double, double, double> onSizeOrDpiChanged);
Action<int, int> onSizeChanged);
[JSImport("DpiWatcher.start", AvaloniaModule.MainModuleName)]
public static partial double ObserveDpi(
[JSMarshalAs<JSType.Function<JSType.Number, JSType.Number>>]
Action<double, double> onDpiChanged);
[JSImport("AvaloniaDOM.observeDarkMode", AvaloniaModule.MainModuleName)] [JSImport("AvaloniaDOM.observeDarkMode", AvaloniaModule.MainModuleName)]
public static partial JSObject ObserveDarkMode( public static partial JSObject ObserveDarkMode(
[JSMarshalAs<JSType.Function<JSType.Boolean, JSType.Boolean>>] [JSMarshalAs<JSType.Function<JSType.Boolean, JSType.Boolean>>]

5
src/Browser/Avalonia.Browser/Skia/BrowserSkiaGpuRenderTarget.cs

@ -1,3 +1,4 @@
using System;
using Avalonia.Skia; using Avalonia.Skia;
using SkiaSharp; using SkiaSharp;
@ -16,8 +17,8 @@ namespace Avalonia.Browser.Skia
var glFbInfo = new GRGlFramebufferInfo(browserSkiaSurface.GlInfo.FboId, browserSkiaSurface.ColorType.ToGlSizedFormat()); var glFbInfo = new GRGlFramebufferInfo(browserSkiaSurface.GlInfo.FboId, browserSkiaSurface.ColorType.ToGlSizedFormat());
_browserSkiaSurface = browserSkiaSurface; _browserSkiaSurface = browserSkiaSurface;
_renderTarget = new GRBackendRenderTarget( _renderTarget = new GRBackendRenderTarget(
(int)(browserSkiaSurface.Size.Width * browserSkiaSurface.Scaling), (int)Math.Round(browserSkiaSurface.DisplaySize.Width * browserSkiaSurface.Scaling),
(int)(browserSkiaSurface.Size.Height * browserSkiaSurface.Scaling), (int)Math.Round(browserSkiaSurface.DisplaySize.Height * browserSkiaSurface.Scaling),
browserSkiaSurface.GlInfo.Samples, browserSkiaSurface.GlInfo.Samples,
browserSkiaSurface.GlInfo.Stencils, glFbInfo); browserSkiaSurface.GlInfo.Stencils, glFbInfo);
} }

5
src/Browser/Avalonia.Browser/Skia/BrowserSkiaSurface.cs

@ -5,12 +5,13 @@ namespace Avalonia.Browser.Skia
{ {
internal class BrowserSkiaSurface : IBrowserSkiaSurface 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; Context = context;
GlInfo = glInfo; GlInfo = glInfo;
ColorType = colorType; ColorType = colorType;
Size = size; Size = size;
DisplaySize = displaySize;
Scaling = scaling; Scaling = scaling;
Origin = origin; Origin = origin;
} }
@ -19,6 +20,8 @@ namespace Avalonia.Browser.Skia
public PixelSize Size { get; set; } public PixelSize Size { get; set; }
public Size DisplaySize { get; set; }
public GRContext Context { get; set; } public GRContext Context { get; set; }
public GRSurfaceOrigin Origin { get; set; } public GRSurfaceOrigin Origin { get; set; }

5
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 { InputHelper } from "./avalonia/input";
import { AvaloniaDOM } from "./avalonia/dom"; import { AvaloniaDOM } from "./avalonia/dom";
import { Caniuse } from "./avalonia/caniuse"; import { Caniuse } from "./avalonia/caniuse";
@ -17,8 +17,7 @@ export {
Caniuse, Caniuse,
Canvas, Canvas,
InputHelper, InputHelper,
SizeWatcher, ResizeHandler,
DpiWatcher,
AvaloniaDOM, AvaloniaDOM,
StreamHelper, StreamHelper,
NativeControlHost, NativeControlHost,

150
src/Browser/Avalonia.Browser/webapp/modules/avalonia/canvas.ts

@ -194,122 +194,64 @@ export class Canvas {
} }
} }
type SizeWatcherElement = { type ResizeHandlerCallback = (displayWidth: number, displayHeight: number, dpi: number) => void;
SizeWatcher: SizeWatcherInstance;
} & HTMLElement;
interface SizeWatcherInstance { type ResizeObserverWithCallbacks = {
callback: (width: number, height: number) => void; callbacks: Map<Element, ResizeHandlerCallback>;
} } & ResizeObserver;
export class SizeWatcher { export class ResizeHandler {
static observer: ResizeObserver; private static resizeObserver?: ResizeObserverWithCallbacks;
static elements: Map<string, HTMLElement>;
private static lastMove: number;
private static timeoutHandle?: number;
public static observe(element: HTMLElement, elementId: string | undefined, callback: (width: number, height: number) => void): void { public static observeSize(element: HTMLElement, callback: ResizeHandlerCallback): (() => void) {
if (!element || !callback) { if (!this.resizeObserver) {
return; this.resizeObserver = new ResizeObserver(this.onResize) as ResizeObserverWithCallbacks;
this.resizeObserver.callbacks = new Map<Element, ResizeHandlerCallback>();
} }
SizeWatcher.lastMove = Date.now(); this.resizeObserver.callbacks.set(element, callback);
this.resizeObserver.observe(element, { box: "content-box" });
callback(element.clientWidth, element.clientHeight);
const handleResize = (args: UIEvent) => { return () => {
if (Date.now() - SizeWatcher.lastMove > 33) { this.resizeObserver?.callbacks.delete(element);
callback(element.clientWidth, element.clientHeight); this.resizeObserver?.unobserve(element);
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);
}
}; };
window.addEventListener("resize", handleResize);
} }
public static unobserve(elementId: string): void { private static onResize(entries: ResizeObserverEntry[], observer: ResizeObserver) {
if (!elementId || !SizeWatcher.observer) { for (const entry of entries) {
return; const callback = (observer as ResizeObserverWithCallbacks).callbacks.get(entry.target);
} if (!callback) {
continue;
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<string, HTMLElement>();
SizeWatcher.observer = new ResizeObserver((entries) => {
for (const entry of entries) {
SizeWatcher.invoke(entry.target);
} }
});
}
static invoke(element: Element): void {
const watcherElement = element as SizeWatcherElement;
const instance = watcherElement.SizeWatcher;
if (!instance || !instance.callback) { const trueDpr = window.devicePixelRatio;
return; let width;
} let height;
let dpr = trueDpr;
return instance.callback(element.clientWidth, element.clientHeight); 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
export class DpiWatcher { width = entry.devicePixelContentBoxSize[0].inlineSize;
static lastDpi: number; height = entry.devicePixelContentBoxSize[0].blockSize;
static timerId: number; dpr = 1; // it's already in width and height
static callback: (old: number, newdpi: number) => void; } else if (entry.contentBoxSize) {
if (entry.contentBoxSize[0]) {
public static getDpi(): number { width = entry.contentBoxSize[0].inlineSize;
return window.devicePixelRatio; height = entry.contentBoxSize[0].blockSize;
} } else {
width = (entry.contentBoxSize as any).inlineSize;
public static start(callback: (old: number, newdpi: number) => void): number { height = (entry.contentBoxSize as any).blockSize;
DpiWatcher.lastDpi = window.devicePixelRatio; }
DpiWatcher.timerId = window.setInterval(DpiWatcher.update, 1000); } else {
DpiWatcher.callback = callback; width = entry.contentRect.width;
height = entry.contentRect.height;
return DpiWatcher.lastDpi; }
} const displayWidth = Math.round(width * dpr);
const displayHeight = Math.round(height * dpr);
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;
if (Math.abs(lastDpi - currentDpi) > 0.001) { callback(displayWidth, displayHeight, trueDpr);
DpiWatcher.callback(lastDpi, currentDpi);
} }
} }
} }

Loading…
Cancel
Save