Skip to content

Commit d59d7b3

Browse files
yjbanovhjfreyer
authored andcommitted
[canvaskit] update CSS size of the canvas when device-pixel ratio changes (flutter#24160)
1 parent bd8cce6 commit d59d7b3

2 files changed

Lines changed: 80 additions & 25 deletions

File tree

lib/web_ui/lib/src/engine/canvaskit/surface.dart

Lines changed: 43 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ class Surface {
5959
/// due to the browser tab becoming dormant.
6060
final html.Element htmlElement = html.Element.tag('flt-canvas-container');
6161

62+
/// The underlying `<canvas>` element used for this surface.
63+
html.CanvasElement? htmlCanvas;
64+
int _pixelWidth = -1;
65+
int _pixelHeight = -1;
66+
6267
/// Specify the GPU resource cache limits.
6368
void setSkiaResourceCacheMaxBytes(int bytes) {
6469
_skiaCacheBytes = bytes;
@@ -102,6 +107,7 @@ class Surface {
102107
}
103108

104109
ui.Size? _currentSize;
110+
double _currentDevicePixelRatio = -1;
105111

106112
CkSurface _createOrUpdateSurfaces(ui.Size size) {
107113
if (size.isEmpty) {
@@ -116,9 +122,13 @@ class Surface {
116122
size.width <= previousSize.width &&
117123
size.height <= previousSize.height) {
118124
// The existing surface is still reusable.
125+
if (window.devicePixelRatio != _currentDevicePixelRatio) {
126+
_updateLogicalHtmlCanvasSize();
127+
}
119128
return _surface!;
120129
}
121130

131+
_currentDevicePixelRatio = window.devicePixelRatio;
122132
_currentSize = _currentSize == null
123133
// First frame. Allocate a canvas of the exact size as the window. The
124134
// window is frequently never resized, particularly on mobile, so using
@@ -131,36 +141,44 @@ class Surface {
131141
_surface = null;
132142
_addedToScene = false;
133143

134-
return _surface = _wrapHtmlCanvas(_currentSize!);
144+
return _surface = _createNewSurface(_currentSize!);
135145
}
136146

137-
CkSurface _wrapHtmlCanvas(ui.Size physicalSize) {
138-
// Clear the container, if it's not empty.
139-
while (htmlElement.firstChild != null) {
140-
htmlElement.firstChild!.remove();
141-
}
147+
/// Sets the CSS size of the canvas so that canvas pixels are 1:1 with device
148+
/// pixels.
149+
///
150+
/// The logical size of the canvas is not based on the size of the window
151+
/// but on the size of the canvas, which, due to `ceil()` above, may not be
152+
/// the same as the window. We do not round/floor/ceil the logical size as
153+
/// CSS pixels can contain more than one physical pixel and therefore to
154+
/// match the size of the window precisely we use the most precise floating
155+
/// point value we can get.
156+
void _updateLogicalHtmlCanvasSize() {
157+
final double logicalWidth = _pixelWidth / ui.window.devicePixelRatio;
158+
final double logicalHeight = _pixelHeight / ui.window.devicePixelRatio;
159+
htmlCanvas!.style
160+
..width = '${logicalWidth}px'
161+
..height = '${logicalHeight}px';
162+
}
163+
164+
/// This function is expensive.
165+
///
166+
/// It's better to reuse surface if possible.
167+
CkSurface _createNewSurface(ui.Size physicalSize) {
168+
// Clear the container, if it's not empty. We're going to create a new <canvas>.
169+
this.htmlCanvas?.remove();
142170

143171
// If `physicalSize` is not precise, use a slightly bigger canvas. This way
144172
// we ensure that the rendred picture covers the entire browser window.
145-
final int pixelWidth = physicalSize.width.ceil();
146-
final int pixelHeight = physicalSize.height.ceil();
173+
_pixelWidth = physicalSize.width.ceil();
174+
_pixelHeight = physicalSize.height.ceil();
147175
final html.CanvasElement htmlCanvas = html.CanvasElement(
148-
width: pixelWidth,
149-
height: pixelHeight,
176+
width: _pixelWidth,
177+
height: _pixelHeight,
150178
);
151-
152-
// The logical size of the canvas is not based on the size of the window
153-
// but on the size of the canvas, which, due to `ceil()` above, may not be
154-
// the same as the window. We do not round/floor/ceil the logical size as
155-
// CSS pixels can contain more than one physical pixel and therefore to
156-
// match the size of the window precisely we use the most precise floating
157-
// point value we can get.
158-
final double logicalWidth = pixelWidth / ui.window.devicePixelRatio;
159-
final double logicalHeight = pixelHeight / ui.window.devicePixelRatio;
160-
htmlCanvas.style
161-
..position = 'absolute'
162-
..width = '${logicalWidth}px'
163-
..height = '${logicalHeight}px';
179+
this.htmlCanvas = htmlCanvas;
180+
htmlCanvas.style.position = 'absolute';
181+
_updateLogicalHtmlCanvasSize();
164182

165183
// When the browser tab using WebGL goes dormant the browser and/or OS may
166184
// decide to clear GPU resources to let other tabs/programs use the GPU.
@@ -212,8 +230,8 @@ class Surface {
212230

213231
SkSurface? skSurface = canvasKit.MakeOnScreenGLSurface(
214232
_grContext!,
215-
pixelWidth,
216-
pixelHeight,
233+
_pixelWidth,
234+
_pixelHeight,
217235
SkColorSpaceSRGB,
218236
);
219237

lib/web_ui/test/canvaskit/surface_test.dart

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,14 @@ void testMain() {
2727
// Expect exact requested dimensions.
2828
expect(original.width(), 9);
2929
expect(original.height(), 19);
30+
expect(surface.htmlCanvas!.style.width, '9px');
31+
expect(surface.htmlCanvas!.style.height, '19px');
3032

3133
// Shrinking reuses the existing surface straight-up.
3234
final CkSurface shrunk = surface.acquireFrame(ui.Size(5, 15)).skiaSurface;
3335
expect(shrunk, same(original));
36+
expect(surface.htmlCanvas!.style.width, '9px');
37+
expect(surface.htmlCanvas!.style.height, '19px');
3438

3539
// The first increase will allocate a new surface, but will overallocate
3640
// by 40% to accommodate future increases.
@@ -40,6 +44,8 @@ void testMain() {
4044
// Expect overallocated dimensions
4145
expect(firstIncrease.width(), 14);
4246
expect(firstIncrease.height(), 28);
47+
expect(surface.htmlCanvas!.style.width, '14px');
48+
expect(surface.htmlCanvas!.style.height, '28px');
4349

4450
// Subsequent increases within 40% reuse the old surface.
4551
final CkSurface secondIncrease = surface.acquireFrame(ui.Size(11, 22)).skiaSurface;
@@ -52,6 +58,8 @@ void testMain() {
5258
// Also over-allocated
5359
expect(huge.width(), 28);
5460
expect(huge.height(), 56);
61+
expect(surface.htmlCanvas!.style.width, '28px');
62+
expect(surface.htmlCanvas!.style.height, '56px');
5563

5664
// Shrink again. Reuse the last allocated surface.
5765
final CkSurface shrunk2 = surface.acquireFrame(ui.Size(5, 15)).skiaSurface;
@@ -88,5 +96,34 @@ void testMain() {
8896
// Firefox doesn't have the WEBGL_lose_context extension.
8997
skip: isFirefox || isIosSafari,
9098
);
99+
100+
// Regression test for https://github.com/flutter/flutter/issues/75286
101+
test('updates canvas logical size when device-pixel ratio changes', () {
102+
final Surface surface = Surface(HtmlViewEmbedder());
103+
final CkSurface original = surface.acquireFrame(ui.Size(10, 16)).skiaSurface;
104+
105+
expect(original.width(), 10);
106+
expect(original.height(), 16);
107+
expect(surface.htmlCanvas!.style.width, '10px');
108+
expect(surface.htmlCanvas!.style.height, '16px');
109+
110+
// Increase device-pixel ratio: this makes CSS pixels bigger, so we need
111+
// fewer of them to cover the browser window.
112+
window.debugOverrideDevicePixelRatio(2.0);
113+
final CkSurface highDpr = surface.acquireFrame(ui.Size(10, 16)).skiaSurface;
114+
expect(highDpr.width(), 10);
115+
expect(highDpr.height(), 16);
116+
expect(surface.htmlCanvas!.style.width, '5px');
117+
expect(surface.htmlCanvas!.style.height, '8px');
118+
119+
// Decrease device-pixel ratio: this makes CSS pixels smaller, so we need
120+
// more of them to cover the browser window.
121+
window.debugOverrideDevicePixelRatio(0.5);
122+
final CkSurface lowDpr = surface.acquireFrame(ui.Size(10, 16)).skiaSurface;
123+
expect(lowDpr.width(), 10);
124+
expect(lowDpr.height(), 16);
125+
expect(surface.htmlCanvas!.style.width, '20px');
126+
expect(surface.htmlCanvas!.style.height, '32px');
127+
});
91128
}, skip: isIosSafari);
92129
}

0 commit comments

Comments
 (0)