[web] Fix Scene clip bounds. Trigger resize on DPR Change. (flutter/engine#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 https://github.com/flutter/flutter/issues/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
This commit is contained in:
@@ -6134,6 +6134,7 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/vector_math.dart + ../../../f
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/dimensions_provider.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/full_page_dimensions_provider.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/display_dpr_stream.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/dom_manager.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/embedding_strategy/custom_element_embedding_strategy.dart + ../../../flutter/LICENSE
|
||||
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
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/dimensions_provider.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/full_page_dimensions_provider.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/display_dpr_stream.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/dom_manager.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/embedding_strategy/custom_element_embedding_strategy.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/embedding_strategy/embedding_strategy.dart
|
||||
|
||||
@@ -190,6 +190,7 @@ export 'engine/vector_math.dart';
|
||||
export 'engine/view_embedder/dimensions_provider/custom_element_dimensions_provider.dart';
|
||||
export 'engine/view_embedder/dimensions_provider/dimensions_provider.dart';
|
||||
export 'engine/view_embedder/dimensions_provider/full_page_dimensions_provider.dart';
|
||||
export 'engine/view_embedder/display_dpr_stream.dart';
|
||||
export 'engine/view_embedder/dom_manager.dart';
|
||||
export 'engine/view_embedder/embedding_strategy/custom_element_embedding_strategy.dart';
|
||||
export 'engine/view_embedder/embedding_strategy/embedding_strategy.dart';
|
||||
|
||||
@@ -45,9 +45,15 @@ class PersistedScene extends PersistedContainerSurface {
|
||||
|
||||
@override
|
||||
void recomputeTransformAndClip() {
|
||||
// The scene clip is the size of the entire window.
|
||||
final ui.Size screen = window.physicalSize;
|
||||
localClipBounds = ui.Rect.fromLTRB(0, 0, screen.width, screen.height);
|
||||
// The scene clip is the size of the entire window **in Logical pixels**.
|
||||
//
|
||||
// Even though the majority of the engine uses `physicalSize`, there are some
|
||||
// bits (like the HTML renderer, or dynamic view sizing) that are implemented
|
||||
// using CSS, and CSS operates in logical pixels.
|
||||
//
|
||||
// See also: [EngineFlutterView.resize].
|
||||
final ui.Size bounds = window.physicalSize / window.devicePixelRatio;
|
||||
localClipBounds = ui.Rect.fromLTRB(0, 0, bounds.width, bounds.height);
|
||||
projectedClip = null;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:ui/src/engine/display.dart';
|
||||
import 'package:ui/src/engine/dom.dart';
|
||||
import 'package:ui/src/engine/window.dart';
|
||||
import 'package:ui/ui.dart' as ui show Size;
|
||||
@@ -12,21 +13,36 @@ import 'dimensions_provider.dart';
|
||||
|
||||
/// This class provides observable, real-time dimensions of a host element.
|
||||
///
|
||||
/// This class needs a `Stream` of `devicePixelRatio` changes, like the one
|
||||
/// provided by [DisplayDprStream], because html resize observers do not report
|
||||
/// DPR changes.
|
||||
///
|
||||
/// All the measurements returned from this class are potentially *expensive*,
|
||||
/// and should be cached as needed. Every call to every method on this class
|
||||
/// WILL perform actual DOM measurements.
|
||||
///
|
||||
/// This broadcasts `null` size events, to match the implementation of the
|
||||
/// FullPageDimensionsProvider, but it could broadcast the size coming from the
|
||||
/// DomResizeObserverEntry. Further changes in the engine are required for this
|
||||
/// to be effective.
|
||||
class CustomElementDimensionsProvider extends DimensionsProvider {
|
||||
/// Creates a [CustomElementDimensionsProvider] from a [_hostElement].
|
||||
CustomElementDimensionsProvider(this._hostElement) {
|
||||
CustomElementDimensionsProvider(this._hostElement, {
|
||||
Stream<double>? onDprChange,
|
||||
}) {
|
||||
// Send a resize event when the page DPR changes.
|
||||
_dprChangeStreamSubscription = onDprChange?.listen((_) {
|
||||
_broadcastSize(null);
|
||||
});
|
||||
|
||||
// Hook up a resize observer on the hostElement (if supported!).
|
||||
_hostElementResizeObserver = createDomResizeObserver((
|
||||
List<DomResizeObserverEntry> entries,
|
||||
DomResizeObserver _,
|
||||
) {
|
||||
entries
|
||||
.map((DomResizeObserverEntry entry) =>
|
||||
ui.Size(entry.contentRect.width, entry.contentRect.height))
|
||||
.forEach(_broadcastSize);
|
||||
for (final DomResizeObserverEntry _ in entries) {
|
||||
_broadcastSize(null);
|
||||
}
|
||||
});
|
||||
|
||||
assert(() {
|
||||
@@ -45,11 +61,12 @@ class CustomElementDimensionsProvider extends DimensionsProvider {
|
||||
|
||||
// Handle resize events
|
||||
late DomResizeObserver? _hostElementResizeObserver;
|
||||
final StreamController<ui.Size> _onResizeStreamController =
|
||||
StreamController<ui.Size>.broadcast();
|
||||
late StreamSubscription<double>? _dprChangeStreamSubscription;
|
||||
final StreamController<ui.Size?> _onResizeStreamController =
|
||||
StreamController<ui.Size?>.broadcast();
|
||||
|
||||
// Broadcasts the last seen `Size`.
|
||||
void _broadcastSize(ui.Size size) {
|
||||
void _broadcastSize(ui.Size? size) {
|
||||
_onResizeStreamController.add(size);
|
||||
}
|
||||
|
||||
@@ -58,16 +75,17 @@ class CustomElementDimensionsProvider extends DimensionsProvider {
|
||||
super.close();
|
||||
_hostElementResizeObserver?.disconnect();
|
||||
// ignore:unawaited_futures
|
||||
_dprChangeStreamSubscription?.cancel();
|
||||
// ignore:unawaited_futures
|
||||
_onResizeStreamController.close();
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<ui.Size> get onResize => _onResizeStreamController.stream;
|
||||
Stream<ui.Size?> get onResize => _onResizeStreamController.stream;
|
||||
|
||||
@override
|
||||
ui.Size computePhysicalSize() {
|
||||
final double devicePixelRatio = getDevicePixelRatio();
|
||||
|
||||
final double devicePixelRatio = EngineFlutterDisplay.instance.devicePixelRatio;
|
||||
return ui.Size(
|
||||
_hostElement.clientWidth * devicePixelRatio,
|
||||
_hostElement.clientHeight * devicePixelRatio,
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:ui/src/engine/dom.dart';
|
||||
import 'package:ui/src/engine/view_embedder/display_dpr_stream.dart';
|
||||
import 'package:ui/src/engine/window.dart';
|
||||
import 'package:ui/ui.dart' as ui show Size;
|
||||
|
||||
import '../../display.dart';
|
||||
import '../../dom.dart';
|
||||
import 'custom_element_dimensions_provider.dart';
|
||||
import 'full_page_dimensions_provider.dart';
|
||||
|
||||
@@ -32,18 +32,15 @@ abstract class DimensionsProvider {
|
||||
/// Creates the appropriate DimensionsProvider depending on the incoming [hostElement].
|
||||
factory DimensionsProvider.create({DomElement? hostElement}) {
|
||||
if (hostElement != null) {
|
||||
return CustomElementDimensionsProvider(hostElement);
|
||||
return CustomElementDimensionsProvider(
|
||||
hostElement,
|
||||
onDprChange: DisplayDprStream.instance.dprChanged,
|
||||
);
|
||||
} else {
|
||||
return FullPageDimensionsProvider();
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the DPI reported by the browser.
|
||||
double getDevicePixelRatio() {
|
||||
// This is overridable in tests.
|
||||
return EngineFlutterDisplay.instance.devicePixelRatio;
|
||||
}
|
||||
|
||||
/// Returns the [ui.Size] of the "viewport".
|
||||
///
|
||||
/// This function is expensive. It triggers browser layout if there are
|
||||
@@ -57,6 +54,16 @@ abstract class DimensionsProvider {
|
||||
);
|
||||
|
||||
/// Returns a Stream with the changes to [ui.Size] (when cheap to get).
|
||||
///
|
||||
/// Currently this Stream always returns `null` measurements because the
|
||||
/// resize event that we use for [FullPageDimensionsProvider] does not contain
|
||||
/// the new size, so users of this Stream everywhere immediately retrieve the
|
||||
/// new `physicalSize` from the window.
|
||||
///
|
||||
/// The [CustomElementDimensionsProvider] *could* broadcast the new size, but
|
||||
/// to keep both implementations consistent (and their consumers), for now all
|
||||
/// events from this Stream are going to be `null` (until we find a performant
|
||||
/// way to retrieve the dimensions in full-page mode).
|
||||
Stream<ui.Size?> get onResize;
|
||||
|
||||
/// Whether the [DimensionsProvider] instance has been closed or not.
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:ui/src/engine/browser_detection.dart';
|
||||
import 'package:ui/src/engine/display.dart';
|
||||
import 'package:ui/src/engine/dom.dart';
|
||||
import 'package:ui/src/engine/window.dart';
|
||||
import 'package:ui/ui.dart' as ui show Size;
|
||||
@@ -67,7 +68,7 @@ class FullPageDimensionsProvider extends DimensionsProvider {
|
||||
late double windowInnerWidth;
|
||||
late double windowInnerHeight;
|
||||
final DomVisualViewport? viewport = domWindow.visualViewport;
|
||||
final double devicePixelRatio = getDevicePixelRatio();
|
||||
final double devicePixelRatio = EngineFlutterDisplay.instance.devicePixelRatio;
|
||||
|
||||
if (viewport != null) {
|
||||
if (operatingSystem == OperatingSystem.iOs) {
|
||||
@@ -102,7 +103,7 @@ class FullPageDimensionsProvider extends DimensionsProvider {
|
||||
double physicalHeight,
|
||||
bool isEditingOnMobile,
|
||||
) {
|
||||
final double devicePixelRatio = getDevicePixelRatio();
|
||||
final double devicePixelRatio = EngineFlutterDisplay.instance.devicePixelRatio;
|
||||
final DomVisualViewport? viewport = domWindow.visualViewport;
|
||||
late double windowInnerHeight;
|
||||
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:js_interop';
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:ui/src/engine/display.dart';
|
||||
import 'package:ui/src/engine/dom.dart';
|
||||
import 'package:ui/ui.dart' as ui show Display;
|
||||
|
||||
/// Provides a stream of `devicePixelRatio` changes for the given display.
|
||||
///
|
||||
/// Note that until the Window Management API is generally available, this class
|
||||
/// only monitors the global `devicePixelRatio` property, provided by the default
|
||||
/// [EngineFlutterDisplay.instance].
|
||||
///
|
||||
/// See: https://developer.mozilla.org/en-US/docs/Web/API/Window_Management_API
|
||||
class DisplayDprStream {
|
||||
DisplayDprStream(
|
||||
this._display, {
|
||||
@visibleForTesting DebugDisplayDprStreamOverrides? overrides,
|
||||
}) : _currentDpr = _display.devicePixelRatio,
|
||||
_debugOverrides = overrides {
|
||||
// Start listening to DPR changes.
|
||||
_subscribeToMediaQuery();
|
||||
}
|
||||
|
||||
/// A singleton instance of DisplayDprStream.
|
||||
static DisplayDprStream instance =
|
||||
DisplayDprStream(EngineFlutterDisplay.instance);
|
||||
|
||||
// The display object that will provide the DPR information.
|
||||
final ui.Display _display;
|
||||
|
||||
// Last reported value of DPR.
|
||||
double _currentDpr;
|
||||
|
||||
// Controls the [dprChanged] broadcast Stream.
|
||||
final StreamController<double> _dprStreamController =
|
||||
StreamController<double>.broadcast();
|
||||
|
||||
// Object that fires a `change` event for the `_currentDpr`.
|
||||
late DomEventTarget _dprMediaQuery;
|
||||
|
||||
// Creates the media query for the latest known DPR value, and adds a change listener to it.
|
||||
void _subscribeToMediaQuery() {
|
||||
if (_debugOverrides?.getMediaQuery != null) {
|
||||
_dprMediaQuery = _debugOverrides!.getMediaQuery!(_currentDpr);
|
||||
} else {
|
||||
_dprMediaQuery = domWindow.matchMedia('(resolution: ${_currentDpr}dppx)');
|
||||
}
|
||||
_dprMediaQuery.addEventListenerWithOptions(
|
||||
'change',
|
||||
createDomEventListener(_onDprMediaQueryChange),
|
||||
<String, Object>{
|
||||
// We only listen `once` because this event only triggers once when the
|
||||
// DPR changes from `_currentDpr`. Once that happens, we need a new
|
||||
// `_dprMediaQuery` that is watching the new `_currentDpr`.
|
||||
//
|
||||
// By using `once`, we don't need to worry about detaching the event
|
||||
// listener from the old mediaQuery after we're done with it.
|
||||
'once': true,
|
||||
'passive': true,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Handler of the _dprMediaQuery 'change' event.
|
||||
//
|
||||
// This calls subscribe again because events are listened to with `once: true`.
|
||||
JSVoid _onDprMediaQueryChange(DomEvent _) {
|
||||
_currentDpr = _display.devicePixelRatio;
|
||||
_dprStreamController.add(_currentDpr);
|
||||
// Re-subscribe...
|
||||
_subscribeToMediaQuery();
|
||||
}
|
||||
|
||||
/// A stream that emits the latest value of [EngineFlutterDisplay.instance.devicePixelRatio].
|
||||
Stream<double> get dprChanged => _dprStreamController.stream;
|
||||
|
||||
// The overrides object that is used for testing.
|
||||
final DebugDisplayDprStreamOverrides? _debugOverrides;
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
class DebugDisplayDprStreamOverrides {
|
||||
DebugDisplayDprStreamOverrides({
|
||||
this.getMediaQuery,
|
||||
});
|
||||
final DomEventTarget Function(double currentValue)? getMediaQuery;
|
||||
}
|
||||
@@ -2,9 +2,6 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
@TestOn('browser')
|
||||
library;
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:test/bootstrap/browser.dart';
|
||||
@@ -109,23 +106,48 @@ void doTests() {
|
||||
});
|
||||
|
||||
test('funnels resize events on sizeSource', () async {
|
||||
EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(2.7);
|
||||
|
||||
sizeSource
|
||||
..style.width = '100px'
|
||||
..style.height = '100px';
|
||||
|
||||
expect(await provider.onResize.first, const ui.Size(100, 100));
|
||||
expect(provider.onResize.first, completes);
|
||||
expect(provider.computePhysicalSize(), const ui.Size(270, 270));
|
||||
|
||||
sizeSource
|
||||
..style.width = '200px'
|
||||
..style.height = '200px';
|
||||
|
||||
expect(await provider.onResize.first, const ui.Size(200, 200));
|
||||
expect(provider.onResize.first, completes);
|
||||
expect(provider.computePhysicalSize(), const ui.Size(540, 540));
|
||||
|
||||
sizeSource
|
||||
..style.width = '300px'
|
||||
..style.height = '300px';
|
||||
|
||||
expect(await provider.onResize.first, const ui.Size(300, 300));
|
||||
expect(provider.onResize.first, completes);
|
||||
expect(provider.computePhysicalSize(), const ui.Size(810, 810));
|
||||
});
|
||||
|
||||
test('funnels DPR change events too', () async {
|
||||
// Override the source of DPR events...
|
||||
final StreamController<double> dprController =
|
||||
StreamController<double>.broadcast();
|
||||
|
||||
// Inject the dprController stream into the CustomElementDimensionsProvider.
|
||||
final CustomElementDimensionsProvider provider =
|
||||
CustomElementDimensionsProvider(
|
||||
sizeSource,
|
||||
onDprChange: dprController.stream,
|
||||
);
|
||||
|
||||
// Set and broadcast the mock DPR value
|
||||
EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(3.2);
|
||||
dprController.add(3.2);
|
||||
|
||||
expect(provider.onResize.first, completes);
|
||||
expect(provider.computePhysicalSize(), const ui.Size(32, 32));
|
||||
});
|
||||
|
||||
test('closed by onHotRestart', () async {
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
@TestOn('browser')
|
||||
library;
|
||||
|
||||
import 'package:test/bootstrap/browser.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:ui/src/engine.dart';
|
||||
@@ -31,15 +28,4 @@ void doTests() {
|
||||
expect(provider, isA<CustomElementDimensionsProvider>());
|
||||
});
|
||||
});
|
||||
|
||||
group('getDevicePixelRatio', () {
|
||||
test('Returns the correct pixelRatio', () async {
|
||||
// Override the DPI to something known, but weird...
|
||||
EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(33930);
|
||||
|
||||
final DimensionsProvider provider = DimensionsProvider.create();
|
||||
|
||||
expect(provider.getDevicePixelRatio(), 33930);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
@TestOn('browser')
|
||||
library;
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:test/bootstrap/browser.dart';
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:test/bootstrap/browser.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:ui/src/engine.dart';
|
||||
|
||||
void main() {
|
||||
internalBootstrapBrowserTest(() => doTests);
|
||||
}
|
||||
|
||||
void doTests() {
|
||||
final DomEventTarget eventTarget = createDomElement('div');
|
||||
|
||||
group('dprChanged Stream', () {
|
||||
late DisplayDprStream dprStream;
|
||||
|
||||
setUp(() async {
|
||||
dprStream = DisplayDprStream(EngineFlutterDisplay.instance,
|
||||
overrides: DebugDisplayDprStreamOverrides(
|
||||
getMediaQuery: (_) => eventTarget,
|
||||
));
|
||||
});
|
||||
|
||||
test('funnels display DPR on every mediaQuery "change" event.', () async {
|
||||
final Future<List<double>> dprs = dprStream.dprChanged
|
||||
.take(3)
|
||||
.timeout(const Duration(seconds: 1))
|
||||
.toList();
|
||||
|
||||
// Simulate the events
|
||||
EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(6.9);
|
||||
eventTarget.dispatchEvent(createDomEvent('Event', 'change'));
|
||||
EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(4.2);
|
||||
eventTarget.dispatchEvent(createDomEvent('Event', 'change'));
|
||||
EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(0.71);
|
||||
eventTarget.dispatchEvent(createDomEvent('Event', 'change'));
|
||||
|
||||
expect(await dprs, <double>[6.9, 4.2, 0.71]);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user