Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 880bf52

Browse files
authored
[web] Fix Scene clip bounds. Trigger resize on DPR Change. (#50161)
The Scene of the HTML renderer is passing incorrect size information to the engine, and when DPR<1, it can result in elements being culled off of the viewport. In addition to that, when an app is embedded, not all changes in DPR cause a Resize event (only those some of the dimensions fails by a rounding error!), so this PR ensures that all DPR events in embedded trigger a resize event. ### Issues Fixes flutter/flutter#129182 ### Testing Looking good at: https://dit-astral-test.web.app [C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
1 parent 23cada2 commit 880bf52

File tree

11 files changed

+226
-48
lines changed

11 files changed

+226
-48
lines changed

ci/licenses_golden/licenses_flutter

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6134,6 +6134,7 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/vector_math.dart + ../../../f
61346134
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider.dart + ../../../flutter/LICENSE
61356135
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/dimensions_provider.dart + ../../../flutter/LICENSE
61366136
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/full_page_dimensions_provider.dart + ../../../flutter/LICENSE
6137+
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/display_dpr_stream.dart + ../../../flutter/LICENSE
61376138
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/dom_manager.dart + ../../../flutter/LICENSE
61386139
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/embedding_strategy/custom_element_embedding_strategy.dart + ../../../flutter/LICENSE
61396140
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/embedding_strategy/embedding_strategy.dart + ../../../flutter/LICENSE
@@ -8997,6 +8998,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/vector_math.dart
89978998
FILE: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider.dart
89988999
FILE: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/dimensions_provider.dart
89999000
FILE: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/full_page_dimensions_provider.dart
9001+
FILE: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/display_dpr_stream.dart
90009002
FILE: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/dom_manager.dart
90019003
FILE: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/embedding_strategy/custom_element_embedding_strategy.dart
90029004
FILE: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/embedding_strategy/embedding_strategy.dart

lib/web_ui/lib/src/engine.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ export 'engine/vector_math.dart';
190190
export 'engine/view_embedder/dimensions_provider/custom_element_dimensions_provider.dart';
191191
export 'engine/view_embedder/dimensions_provider/dimensions_provider.dart';
192192
export 'engine/view_embedder/dimensions_provider/full_page_dimensions_provider.dart';
193+
export 'engine/view_embedder/display_dpr_stream.dart';
193194
export 'engine/view_embedder/dom_manager.dart';
194195
export 'engine/view_embedder/embedding_strategy/custom_element_embedding_strategy.dart';
195196
export 'engine/view_embedder/embedding_strategy/embedding_strategy.dart';

lib/web_ui/lib/src/engine/html/scene.dart

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,15 @@ class PersistedScene extends PersistedContainerSurface {
4545

4646
@override
4747
void recomputeTransformAndClip() {
48-
// The scene clip is the size of the entire window.
49-
final ui.Size screen = window.physicalSize;
50-
localClipBounds = ui.Rect.fromLTRB(0, 0, screen.width, screen.height);
48+
// The scene clip is the size of the entire window **in Logical pixels**.
49+
//
50+
// Even though the majority of the engine uses `physicalSize`, there are some
51+
// bits (like the HTML renderer, or dynamic view sizing) that are implemented
52+
// using CSS, and CSS operates in logical pixels.
53+
//
54+
// See also: [EngineFlutterView.resize].
55+
final ui.Size bounds = window.physicalSize / window.devicePixelRatio;
56+
localClipBounds = ui.Rect.fromLTRB(0, 0, bounds.width, bounds.height);
5157
projectedClip = null;
5258
}
5359

lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider.dart

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import 'dart:async';
66

7+
import 'package:ui/src/engine/display.dart';
78
import 'package:ui/src/engine/dom.dart';
89
import 'package:ui/src/engine/window.dart';
910
import 'package:ui/ui.dart' as ui show Size;
@@ -12,21 +13,36 @@ import 'dimensions_provider.dart';
1213

1314
/// This class provides observable, real-time dimensions of a host element.
1415
///
16+
/// This class needs a `Stream` of `devicePixelRatio` changes, like the one
17+
/// provided by [DisplayDprStream], because html resize observers do not report
18+
/// DPR changes.
19+
///
1520
/// All the measurements returned from this class are potentially *expensive*,
1621
/// and should be cached as needed. Every call to every method on this class
1722
/// WILL perform actual DOM measurements.
23+
///
24+
/// This broadcasts `null` size events, to match the implementation of the
25+
/// FullPageDimensionsProvider, but it could broadcast the size coming from the
26+
/// DomResizeObserverEntry. Further changes in the engine are required for this
27+
/// to be effective.
1828
class CustomElementDimensionsProvider extends DimensionsProvider {
1929
/// Creates a [CustomElementDimensionsProvider] from a [_hostElement].
20-
CustomElementDimensionsProvider(this._hostElement) {
30+
CustomElementDimensionsProvider(this._hostElement, {
31+
Stream<double>? onDprChange,
32+
}) {
33+
// Send a resize event when the page DPR changes.
34+
_dprChangeStreamSubscription = onDprChange?.listen((_) {
35+
_broadcastSize(null);
36+
});
37+
2138
// Hook up a resize observer on the hostElement (if supported!).
2239
_hostElementResizeObserver = createDomResizeObserver((
2340
List<DomResizeObserverEntry> entries,
2441
DomResizeObserver _,
2542
) {
26-
entries
27-
.map((DomResizeObserverEntry entry) =>
28-
ui.Size(entry.contentRect.width, entry.contentRect.height))
29-
.forEach(_broadcastSize);
43+
for (final DomResizeObserverEntry _ in entries) {
44+
_broadcastSize(null);
45+
}
3046
});
3147

3248
assert(() {
@@ -45,11 +61,12 @@ class CustomElementDimensionsProvider extends DimensionsProvider {
4561

4662
// Handle resize events
4763
late DomResizeObserver? _hostElementResizeObserver;
48-
final StreamController<ui.Size> _onResizeStreamController =
49-
StreamController<ui.Size>.broadcast();
64+
late StreamSubscription<double>? _dprChangeStreamSubscription;
65+
final StreamController<ui.Size?> _onResizeStreamController =
66+
StreamController<ui.Size?>.broadcast();
5067

5168
// Broadcasts the last seen `Size`.
52-
void _broadcastSize(ui.Size size) {
69+
void _broadcastSize(ui.Size? size) {
5370
_onResizeStreamController.add(size);
5471
}
5572

@@ -58,16 +75,17 @@ class CustomElementDimensionsProvider extends DimensionsProvider {
5875
super.close();
5976
_hostElementResizeObserver?.disconnect();
6077
// ignore:unawaited_futures
78+
_dprChangeStreamSubscription?.cancel();
79+
// ignore:unawaited_futures
6180
_onResizeStreamController.close();
6281
}
6382

6483
@override
65-
Stream<ui.Size> get onResize => _onResizeStreamController.stream;
84+
Stream<ui.Size?> get onResize => _onResizeStreamController.stream;
6685

6786
@override
6887
ui.Size computePhysicalSize() {
69-
final double devicePixelRatio = getDevicePixelRatio();
70-
88+
final double devicePixelRatio = EngineFlutterDisplay.instance.devicePixelRatio;
7189
return ui.Size(
7290
_hostElement.clientWidth * devicePixelRatio,
7391
_hostElement.clientHeight * devicePixelRatio,

lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/dimensions_provider.dart

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55
import 'dart:async';
66

77
import 'package:meta/meta.dart';
8+
import 'package:ui/src/engine/dom.dart';
9+
import 'package:ui/src/engine/view_embedder/display_dpr_stream.dart';
810
import 'package:ui/src/engine/window.dart';
911
import 'package:ui/ui.dart' as ui show Size;
1012

11-
import '../../display.dart';
12-
import '../../dom.dart';
1313
import 'custom_element_dimensions_provider.dart';
1414
import 'full_page_dimensions_provider.dart';
1515

@@ -32,18 +32,15 @@ abstract class DimensionsProvider {
3232
/// Creates the appropriate DimensionsProvider depending on the incoming [hostElement].
3333
factory DimensionsProvider.create({DomElement? hostElement}) {
3434
if (hostElement != null) {
35-
return CustomElementDimensionsProvider(hostElement);
35+
return CustomElementDimensionsProvider(
36+
hostElement,
37+
onDprChange: DisplayDprStream.instance.dprChanged,
38+
);
3639
} else {
3740
return FullPageDimensionsProvider();
3841
}
3942
}
4043

41-
/// Returns the DPI reported by the browser.
42-
double getDevicePixelRatio() {
43-
// This is overridable in tests.
44-
return EngineFlutterDisplay.instance.devicePixelRatio;
45-
}
46-
4744
/// Returns the [ui.Size] of the "viewport".
4845
///
4946
/// This function is expensive. It triggers browser layout if there are
@@ -57,6 +54,16 @@ abstract class DimensionsProvider {
5754
);
5855

5956
/// Returns a Stream with the changes to [ui.Size] (when cheap to get).
57+
///
58+
/// Currently this Stream always returns `null` measurements because the
59+
/// resize event that we use for [FullPageDimensionsProvider] does not contain
60+
/// the new size, so users of this Stream everywhere immediately retrieve the
61+
/// new `physicalSize` from the window.
62+
///
63+
/// The [CustomElementDimensionsProvider] *could* broadcast the new size, but
64+
/// to keep both implementations consistent (and their consumers), for now all
65+
/// events from this Stream are going to be `null` (until we find a performant
66+
/// way to retrieve the dimensions in full-page mode).
6067
Stream<ui.Size?> get onResize;
6168

6269
/// Whether the [DimensionsProvider] instance has been closed or not.

lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/full_page_dimensions_provider.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import 'dart:async';
66

77
import 'package:ui/src/engine/browser_detection.dart';
8+
import 'package:ui/src/engine/display.dart';
89
import 'package:ui/src/engine/dom.dart';
910
import 'package:ui/src/engine/window.dart';
1011
import 'package:ui/ui.dart' as ui show Size;
@@ -67,7 +68,7 @@ class FullPageDimensionsProvider extends DimensionsProvider {
6768
late double windowInnerWidth;
6869
late double windowInnerHeight;
6970
final DomVisualViewport? viewport = domWindow.visualViewport;
70-
final double devicePixelRatio = getDevicePixelRatio();
71+
final double devicePixelRatio = EngineFlutterDisplay.instance.devicePixelRatio;
7172

7273
if (viewport != null) {
7374
if (operatingSystem == OperatingSystem.iOs) {
@@ -102,7 +103,7 @@ class FullPageDimensionsProvider extends DimensionsProvider {
102103
double physicalHeight,
103104
bool isEditingOnMobile,
104105
) {
105-
final double devicePixelRatio = getDevicePixelRatio();
106+
final double devicePixelRatio = EngineFlutterDisplay.instance.devicePixelRatio;
106107
final DomVisualViewport? viewport = domWindow.visualViewport;
107108
late double windowInnerHeight;
108109

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:async';
6+
import 'dart:js_interop';
7+
8+
import 'package:meta/meta.dart';
9+
import 'package:ui/src/engine/display.dart';
10+
import 'package:ui/src/engine/dom.dart';
11+
import 'package:ui/ui.dart' as ui show Display;
12+
13+
/// Provides a stream of `devicePixelRatio` changes for the given display.
14+
///
15+
/// Note that until the Window Management API is generally available, this class
16+
/// only monitors the global `devicePixelRatio` property, provided by the default
17+
/// [EngineFlutterDisplay.instance].
18+
///
19+
/// See: https://developer.mozilla.org/en-US/docs/Web/API/Window_Management_API
20+
class DisplayDprStream {
21+
DisplayDprStream(
22+
this._display, {
23+
@visibleForTesting DebugDisplayDprStreamOverrides? overrides,
24+
}) : _currentDpr = _display.devicePixelRatio,
25+
_debugOverrides = overrides {
26+
// Start listening to DPR changes.
27+
_subscribeToMediaQuery();
28+
}
29+
30+
/// A singleton instance of DisplayDprStream.
31+
static DisplayDprStream instance =
32+
DisplayDprStream(EngineFlutterDisplay.instance);
33+
34+
// The display object that will provide the DPR information.
35+
final ui.Display _display;
36+
37+
// Last reported value of DPR.
38+
double _currentDpr;
39+
40+
// Controls the [dprChanged] broadcast Stream.
41+
final StreamController<double> _dprStreamController =
42+
StreamController<double>.broadcast();
43+
44+
// Object that fires a `change` event for the `_currentDpr`.
45+
late DomEventTarget _dprMediaQuery;
46+
47+
// Creates the media query for the latest known DPR value, and adds a change listener to it.
48+
void _subscribeToMediaQuery() {
49+
if (_debugOverrides?.getMediaQuery != null) {
50+
_dprMediaQuery = _debugOverrides!.getMediaQuery!(_currentDpr);
51+
} else {
52+
_dprMediaQuery = domWindow.matchMedia('(resolution: ${_currentDpr}dppx)');
53+
}
54+
_dprMediaQuery.addEventListenerWithOptions(
55+
'change',
56+
createDomEventListener(_onDprMediaQueryChange),
57+
<String, Object>{
58+
// We only listen `once` because this event only triggers once when the
59+
// DPR changes from `_currentDpr`. Once that happens, we need a new
60+
// `_dprMediaQuery` that is watching the new `_currentDpr`.
61+
//
62+
// By using `once`, we don't need to worry about detaching the event
63+
// listener from the old mediaQuery after we're done with it.
64+
'once': true,
65+
'passive': true,
66+
},
67+
);
68+
}
69+
70+
// Handler of the _dprMediaQuery 'change' event.
71+
//
72+
// This calls subscribe again because events are listened to with `once: true`.
73+
JSVoid _onDprMediaQueryChange(DomEvent _) {
74+
_currentDpr = _display.devicePixelRatio;
75+
_dprStreamController.add(_currentDpr);
76+
// Re-subscribe...
77+
_subscribeToMediaQuery();
78+
}
79+
80+
/// A stream that emits the latest value of [EngineFlutterDisplay.instance.devicePixelRatio].
81+
Stream<double> get dprChanged => _dprStreamController.stream;
82+
83+
// The overrides object that is used for testing.
84+
final DebugDisplayDprStreamOverrides? _debugOverrides;
85+
}
86+
87+
@visibleForTesting
88+
class DebugDisplayDprStreamOverrides {
89+
DebugDisplayDprStreamOverrides({
90+
this.getMediaQuery,
91+
});
92+
final DomEventTarget Function(double currentValue)? getMediaQuery;
93+
}

lib/web_ui/test/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider_test.dart

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5-
@TestOn('browser')
6-
library;
7-
85
import 'dart:async';
96

107
import 'package:test/bootstrap/browser.dart';
@@ -109,23 +106,48 @@ void doTests() {
109106
});
110107

111108
test('funnels resize events on sizeSource', () async {
109+
EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(2.7);
110+
112111
sizeSource
113112
..style.width = '100px'
114113
..style.height = '100px';
115114

116-
expect(await provider.onResize.first, const ui.Size(100, 100));
115+
expect(provider.onResize.first, completes);
116+
expect(provider.computePhysicalSize(), const ui.Size(270, 270));
117117

118118
sizeSource
119119
..style.width = '200px'
120120
..style.height = '200px';
121121

122-
expect(await provider.onResize.first, const ui.Size(200, 200));
122+
expect(provider.onResize.first, completes);
123+
expect(provider.computePhysicalSize(), const ui.Size(540, 540));
123124

124125
sizeSource
125126
..style.width = '300px'
126127
..style.height = '300px';
127128

128-
expect(await provider.onResize.first, const ui.Size(300, 300));
129+
expect(provider.onResize.first, completes);
130+
expect(provider.computePhysicalSize(), const ui.Size(810, 810));
131+
});
132+
133+
test('funnels DPR change events too', () async {
134+
// Override the source of DPR events...
135+
final StreamController<double> dprController =
136+
StreamController<double>.broadcast();
137+
138+
// Inject the dprController stream into the CustomElementDimensionsProvider.
139+
final CustomElementDimensionsProvider provider =
140+
CustomElementDimensionsProvider(
141+
sizeSource,
142+
onDprChange: dprController.stream,
143+
);
144+
145+
// Set and broadcast the mock DPR value
146+
EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(3.2);
147+
dprController.add(3.2);
148+
149+
expect(provider.onResize.first, completes);
150+
expect(provider.computePhysicalSize(), const ui.Size(32, 32));
129151
});
130152

131153
test('closed by onHotRestart', () async {

lib/web_ui/test/engine/view_embedder/dimensions_provider/dimensions_provider_test.dart

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5-
@TestOn('browser')
6-
library;
7-
85
import 'package:test/bootstrap/browser.dart';
96
import 'package:test/test.dart';
107
import 'package:ui/src/engine.dart';
@@ -31,15 +28,4 @@ void doTests() {
3128
expect(provider, isA<CustomElementDimensionsProvider>());
3229
});
3330
});
34-
35-
group('getDevicePixelRatio', () {
36-
test('Returns the correct pixelRatio', () async {
37-
// Override the DPI to something known, but weird...
38-
EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(33930);
39-
40-
final DimensionsProvider provider = DimensionsProvider.create();
41-
42-
expect(provider.getDevicePixelRatio(), 33930);
43-
});
44-
});
4531
}

0 commit comments

Comments
 (0)