From f4286eb48316c8ef4ad368ed4a745f8613be4667 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Mon, 29 Jan 2024 17:08:21 -0800 Subject: [PATCH 01/12] [web] Scene must pass a logical size as its clip bounds. --- lib/web_ui/lib/src/engine/html/scene.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/web_ui/lib/src/engine/html/scene.dart b/lib/web_ui/lib/src/engine/html/scene.dart index c9703038b6103..117f69e0c17d2 100644 --- a/lib/web_ui/lib/src/engine/html/scene.dart +++ b/lib/web_ui/lib/src/engine/html/scene.dart @@ -45,9 +45,9 @@ 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 (Logical pixels). + final ui.Size bounds = window.physicalSize / window.devicePixelRatio; + localClipBounds = ui.Rect.fromLTRB(0, 0, bounds.width, bounds.height); projectedClip = null; } From cf027d48c2cccd57ca194496acc0e081ace974f3 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Tue, 30 Jan 2024 18:18:44 -0800 Subject: [PATCH 02/12] Add DisplayDprStream class. --- lib/web_ui/lib/src/engine.dart | 1 + .../view_embedder/display_dpr_stream.dart | 55 +++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 lib/web_ui/lib/src/engine/view_embedder/display_dpr_stream.dart diff --git a/lib/web_ui/lib/src/engine.dart b/lib/web_ui/lib/src/engine.dart index 57c7b35fafb8a..14bda2390fdcc 100644 --- a/lib/web_ui/lib/src/engine.dart +++ b/lib/web_ui/lib/src/engine.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'; diff --git a/lib/web_ui/lib/src/engine/view_embedder/display_dpr_stream.dart b/lib/web_ui/lib/src/engine/view_embedder/display_dpr_stream.dart new file mode 100644 index 0000000000000..20530efd062e9 --- /dev/null +++ b/lib/web_ui/lib/src/engine/view_embedder/display_dpr_stream.dart @@ -0,0 +1,55 @@ +// 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:ui/src/engine/display.dart'; +import 'package:ui/src/engine/dom.dart'; +import 'package:ui/ui.dart' as ui show Display; + +/// Determines if high contrast is enabled using media query 'forced-colors: active' for Windows +class DisplayDprStream { + DisplayDprStream(ui.Display display) : _currentDpr = display.devicePixelRatio { + // Start listening to DPR changes. + _subscribeToMediaQuery(); + } + + /// A singleton instance of DisplayDprObserver. + static DisplayDprStream instance = DisplayDprStream(EngineFlutterDisplay.instance); + + // Last reported value of DPR. + double _currentDpr; + + // Controls the [dprChanged] Stream. + final StreamController _dprStreamController = StreamController.broadcast(); + + // Listens for the `_currentDpr` to change. + late DomMediaQueryList _dprMediaQuery; + + // Creates the media query for the latest known DPR value, and adds a change listener to it. + void _subscribeToMediaQuery() { + _dprMediaQuery = domWindow.matchMedia('(resolution: ${_currentDpr}dppx)'); + _dprMediaQuery.addEventListenerWithOptions( + 'change', + createDomEventListener(_onDprMediaQueryChange), + { + 'once': true, + 'passive': true, + }); + } + + // Handler of the 'change' event. + // + // This calls subscribe again because events are listened to with `once: true`. + JSVoid _onDprMediaQueryChange(DomEvent _) { + _currentDpr = EngineFlutterDisplay.instance.devicePixelRatio; + _dprStreamController.add(_currentDpr); + // Re-subscribe... + _subscribeToMediaQuery(); + } + + /// A stream that emits the latest value of [EngineFlutterDisplay.instance.devicePixelRatio]. + Stream get dprChanged => _dprStreamController.stream; +} From 6124efbbc98fc25e40d9235625b4dbf8d06e3142 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Tue, 30 Jan 2024 18:19:48 -0800 Subject: [PATCH 03/12] Wire the DPR Stream to the Custom element dimensions provider. Remove DPR from this hierarchy of classes. --- .../custom_element_dimensions_provider.dart | 13 +++++++++++-- .../dimensions_provider/dimensions_provider.dart | 9 +-------- .../full_page_dimensions_provider.dart | 5 +++-- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider.dart b/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider.dart index 7b39ca908e576..10960cca9b695 100644 --- a/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider.dart +++ b/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider.dart @@ -4,7 +4,9 @@ import 'dart:async'; +import 'package:ui/src/engine/display.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; @@ -18,6 +20,11 @@ import 'dimensions_provider.dart'; class CustomElementDimensionsProvider extends DimensionsProvider { /// Creates a [CustomElementDimensionsProvider] from a [_hostElement]. CustomElementDimensionsProvider(this._hostElement) { + // Send a resize event when the page DPR changes. + _dprChangeStreamSubscription = DisplayDprStream.instance.dprChanged.listen((_) { + _broadcastSize(computePhysicalSize()); + }); + // Hook up a resize observer on the hostElement (if supported!). _hostElementResizeObserver = createDomResizeObserver(( List entries, @@ -45,6 +52,7 @@ class CustomElementDimensionsProvider extends DimensionsProvider { // Handle resize events late DomResizeObserver? _hostElementResizeObserver; + late StreamSubscription? _dprChangeStreamSubscription; final StreamController _onResizeStreamController = StreamController.broadcast(); @@ -58,6 +66,8 @@ class CustomElementDimensionsProvider extends DimensionsProvider { super.close(); _hostElementResizeObserver?.disconnect(); // ignore:unawaited_futures + _dprChangeStreamSubscription?.cancel(); + // ignore:unawaited_futures _onResizeStreamController.close(); } @@ -66,8 +76,7 @@ class CustomElementDimensionsProvider extends DimensionsProvider { @override ui.Size computePhysicalSize() { - final double devicePixelRatio = getDevicePixelRatio(); - + final double devicePixelRatio = EngineFlutterDisplay.instance.devicePixelRatio; return ui.Size( _hostElement.clientWidth * devicePixelRatio, _hostElement.clientHeight * devicePixelRatio, diff --git a/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/dimensions_provider.dart b/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/dimensions_provider.dart index 2fcf44834395e..3aaad364671d2 100644 --- a/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/dimensions_provider.dart +++ b/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/dimensions_provider.dart @@ -5,11 +5,10 @@ import 'dart:async'; import 'package:meta/meta.dart'; +import 'package:ui/src/engine/dom.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'; @@ -38,12 +37,6 @@ abstract class DimensionsProvider { } } - /// 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 diff --git a/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/full_page_dimensions_provider.dart b/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/full_page_dimensions_provider.dart index 8221d95457aef..bcacba4c869f3 100644 --- a/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/full_page_dimensions_provider.dart +++ b/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/full_page_dimensions_provider.dart @@ -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; From 4246857f90c5527fdf443ac414b7402789ec5da0 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Wed, 31 Jan 2024 10:29:13 -0800 Subject: [PATCH 04/12] Address some PR comments. --- ci/licenses_golden/licenses_flutter | 2 ++ .../view_embedder/display_dpr_stream.dart | 17 +++++++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 761d35bb89d9a..5cc3c12bf46b1 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -6136,6 +6136,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 @@ -9001,6 +9002,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 diff --git a/lib/web_ui/lib/src/engine/view_embedder/display_dpr_stream.dart b/lib/web_ui/lib/src/engine/view_embedder/display_dpr_stream.dart index 20530efd062e9..fa1f30c7040d6 100644 --- a/lib/web_ui/lib/src/engine/view_embedder/display_dpr_stream.dart +++ b/lib/web_ui/lib/src/engine/view_embedder/display_dpr_stream.dart @@ -11,7 +11,7 @@ import 'package:ui/ui.dart' as ui show Display; /// Determines if high contrast is enabled using media query 'forced-colors: active' for Windows class DisplayDprStream { - DisplayDprStream(ui.Display display) : _currentDpr = display.devicePixelRatio { + DisplayDprStream(ui.Display display) : _display = display, _currentDpr = display.devicePixelRatio { // Start listening to DPR changes. _subscribeToMediaQuery(); } @@ -19,6 +19,9 @@ class DisplayDprStream { /// A singleton instance of DisplayDprObserver. 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; @@ -34,17 +37,23 @@ class DisplayDprStream { _dprMediaQuery.addEventListenerWithOptions( 'change', createDomEventListener(_onDprMediaQueryChange), - { + { + // 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 'change' event. + // Handler of the _dprMediaQuery 'change' event. // // This calls subscribe again because events are listened to with `once: true`. JSVoid _onDprMediaQueryChange(DomEvent _) { - _currentDpr = EngineFlutterDisplay.instance.devicePixelRatio; + _currentDpr = _display.devicePixelRatio; _dprStreamController.add(_currentDpr); // Re-subscribe... _subscribeToMediaQuery(); From f0348c9ca2a5f89c72431157f431390cb89bda1d Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Wed, 31 Jan 2024 10:36:11 -0800 Subject: [PATCH 05/12] Remove test of deleted method. --- .../dimensions_provider/dimensions_provider_test.dart | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/lib/web_ui/test/engine/view_embedder/dimensions_provider/dimensions_provider_test.dart b/lib/web_ui/test/engine/view_embedder/dimensions_provider/dimensions_provider_test.dart index ba81aa32f01e3..ce1aea48def10 100644 --- a/lib/web_ui/test/engine/view_embedder/dimensions_provider/dimensions_provider_test.dart +++ b/lib/web_ui/test/engine/view_embedder/dimensions_provider/dimensions_provider_test.dart @@ -31,15 +31,4 @@ void doTests() { expect(provider, isA()); }); }); - - 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); - }); - }); } From fdd3d5ca9c15682c254aabefe77fe275624917e4 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Wed, 31 Jan 2024 11:01:16 -0800 Subject: [PATCH 06/12] Some more PR comments. --- .../engine/view_embedder/display_dpr_stream.dart | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/web_ui/lib/src/engine/view_embedder/display_dpr_stream.dart b/lib/web_ui/lib/src/engine/view_embedder/display_dpr_stream.dart index fa1f30c7040d6..494c434a4b8d8 100644 --- a/lib/web_ui/lib/src/engine/view_embedder/display_dpr_stream.dart +++ b/lib/web_ui/lib/src/engine/view_embedder/display_dpr_stream.dart @@ -9,14 +9,20 @@ import 'package:ui/src/engine/display.dart'; import 'package:ui/src/engine/dom.dart'; import 'package:ui/ui.dart' as ui show Display; -/// Determines if high contrast is enabled using media query 'forced-colors: active' for Windows +/// 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(ui.Display display) : _display = display, _currentDpr = display.devicePixelRatio { + DisplayDprStream(this._display) : _currentDpr = _display.devicePixelRatio { // Start listening to DPR changes. _subscribeToMediaQuery(); } - /// A singleton instance of DisplayDprObserver. + /// A singleton instance of DisplayDprStream. static DisplayDprStream instance = DisplayDprStream(EngineFlutterDisplay.instance); // The display object that will provide the DPR information. @@ -25,10 +31,10 @@ class DisplayDprStream { // Last reported value of DPR. double _currentDpr; - // Controls the [dprChanged] Stream. + // Controls the [dprChanged] broadcast Stream. final StreamController _dprStreamController = StreamController.broadcast(); - // Listens for the `_currentDpr` to change. + // Provides a `change` event for the `_currentDpr`. late DomMediaQueryList _dprMediaQuery; // Creates the media query for the latest known DPR value, and adds a change listener to it. From 96b0ff6b065e19fd28d27c3504c603b8f422eadf Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Wed, 31 Jan 2024 14:49:28 -0800 Subject: [PATCH 07/12] Add a couple of happy case tests. --- .../view_embedder/display_dpr_stream.dart | 39 +++++++++++---- ...stom_element_dimensions_provider_test.dart | 22 +++++++++ .../display_dpr_stream_test.dart | 48 +++++++++++++++++++ 3 files changed, 101 insertions(+), 8 deletions(-) create mode 100644 lib/web_ui/test/engine/view_embedder/display_dpr_stream_test.dart diff --git a/lib/web_ui/lib/src/engine/view_embedder/display_dpr_stream.dart b/lib/web_ui/lib/src/engine/view_embedder/display_dpr_stream.dart index 494c434a4b8d8..d73eeb5aec244 100644 --- a/lib/web_ui/lib/src/engine/view_embedder/display_dpr_stream.dart +++ b/lib/web_ui/lib/src/engine/view_embedder/display_dpr_stream.dart @@ -5,6 +5,7 @@ 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; @@ -17,13 +18,18 @@ import 'package:ui/ui.dart' as ui show Display; /// /// See: https://developer.mozilla.org/en-US/docs/Web/API/Window_Management_API class DisplayDprStream { - DisplayDprStream(this._display) : _currentDpr = _display.devicePixelRatio { + 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); + static DisplayDprStream instance = + DisplayDprStream(EngineFlutterDisplay.instance); // The display object that will provide the DPR information. final ui.Display _display; @@ -32,18 +38,23 @@ class DisplayDprStream { double _currentDpr; // Controls the [dprChanged] broadcast Stream. - final StreamController _dprStreamController = StreamController.broadcast(); + final StreamController _dprStreamController = + StreamController.broadcast(); - // Provides a `change` event for the `_currentDpr`. - late DomMediaQueryList _dprMediaQuery; + // 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() { - _dprMediaQuery = domWindow.matchMedia('(resolution: ${_currentDpr}dppx)'); + if (_debugOverrides?.getMediaQuery != null) { + _dprMediaQuery = _debugOverrides!.getMediaQuery!(_currentDpr); + } else { + _dprMediaQuery = domWindow.matchMedia('(resolution: ${_currentDpr}dppx)'); + } _dprMediaQuery.addEventListenerWithOptions( 'change', createDomEventListener(_onDprMediaQueryChange), - { + { // 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`. @@ -52,7 +63,8 @@ class DisplayDprStream { // listener from the old mediaQuery after we're done with it. 'once': true, 'passive': true, - }); + }, + ); } // Handler of the _dprMediaQuery 'change' event. @@ -67,4 +79,15 @@ class DisplayDprStream { /// A stream that emits the latest value of [EngineFlutterDisplay.instance.devicePixelRatio]. Stream 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; } diff --git a/lib/web_ui/test/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider_test.dart b/lib/web_ui/test/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider_test.dart index 9ee7a95083c05..b66e03eb2bccf 100644 --- a/lib/web_ui/test/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider_test.dart +++ b/lib/web_ui/test/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider_test.dart @@ -128,6 +128,28 @@ void doTests() { expect(await provider.onResize.first, const ui.Size(300, 300)); }); + test('funnels DPR change events too', () async { + // Override the source of DPR events... + final DomEventTarget eventTarget = createDomElement('div'); + DisplayDprStream.instance = DisplayDprStream( + EngineFlutterDisplay.instance, + overrides: DebugDisplayDprStreamOverrides( + getMediaQuery: (_) => eventTarget, + ) + ); + final CustomElementDimensionsProvider provider = CustomElementDimensionsProvider(sizeSource); + + // The size that will be emitted by the provider eventually + final Future newSize = provider.onResize.first; + + // Set the mock DPR value + EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(3.2); + // Simulate the mediaQuery change event from the browser + eventTarget.dispatchEvent(createDomEvent('Event', 'change')); + + expect(await newSize, const ui.Size(10, 10) * 3.2); + }); + test('closed by onHotRestart', () async { // Register an onDone listener for the stream final Completer completer = Completer(); diff --git a/lib/web_ui/test/engine/view_embedder/display_dpr_stream_test.dart b/lib/web_ui/test/engine/view_embedder/display_dpr_stream_test.dart new file mode 100644 index 0000000000000..eef57d4ccf0f0 --- /dev/null +++ b/lib/web_ui/test/engine/view_embedder/display_dpr_stream_test.dart @@ -0,0 +1,48 @@ +// 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. + +@TestOn('browser') +library; + +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> 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, [6.9, 4.2, 0.71]); + }); + }); +} From 8f1a7e8b76530f6d4032f848fc4b55c1de3c1585 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Thu, 1 Feb 2024 14:14:15 -0800 Subject: [PATCH 08/12] Make custom element dimensions provider broadcast null events, similar to the full screen one. --- .../custom_element_dimensions_provider.dart | 22 +++++++++++-------- ...stom_element_dimensions_provider_test.dart | 16 +++++++++----- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider.dart b/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider.dart index 10960cca9b695..39f67a6a60226 100644 --- a/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider.dart +++ b/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider.dart @@ -17,12 +17,17 @@ import 'dimensions_provider.dart'; /// 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) { // Send a resize event when the page DPR changes. _dprChangeStreamSubscription = DisplayDprStream.instance.dprChanged.listen((_) { - _broadcastSize(computePhysicalSize()); + _broadcastSize(null); }); // Hook up a resize observer on the hostElement (if supported!). @@ -30,10 +35,9 @@ class CustomElementDimensionsProvider extends DimensionsProvider { List entries, DomResizeObserver _, ) { - entries - .map((DomResizeObserverEntry entry) => - ui.Size(entry.contentRect.width, entry.contentRect.height)) - .forEach(_broadcastSize); + for (final DomResizeObserverEntry _ in entries) { + _broadcastSize(null); + } }); assert(() { @@ -53,11 +57,11 @@ class CustomElementDimensionsProvider extends DimensionsProvider { // Handle resize events late DomResizeObserver? _hostElementResizeObserver; late StreamSubscription? _dprChangeStreamSubscription; - final StreamController _onResizeStreamController = - StreamController.broadcast(); + final StreamController _onResizeStreamController = + StreamController.broadcast(); // Broadcasts the last seen `Size`. - void _broadcastSize(ui.Size size) { + void _broadcastSize(ui.Size? size) { _onResizeStreamController.add(size); } @@ -72,7 +76,7 @@ class CustomElementDimensionsProvider extends DimensionsProvider { } @override - Stream get onResize => _onResizeStreamController.stream; + Stream get onResize => _onResizeStreamController.stream; @override ui.Size computePhysicalSize() { diff --git a/lib/web_ui/test/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider_test.dart b/lib/web_ui/test/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider_test.dart index b66e03eb2bccf..09a30c0d4267f 100644 --- a/lib/web_ui/test/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider_test.dart +++ b/lib/web_ui/test/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider_test.dart @@ -109,23 +109,28 @@ 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 { @@ -140,14 +145,15 @@ void doTests() { final CustomElementDimensionsProvider provider = CustomElementDimensionsProvider(sizeSource); // The size that will be emitted by the provider eventually - final Future newSize = provider.onResize.first; + final Future newSize = provider.onResize.first; // Set the mock DPR value EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(3.2); // Simulate the mediaQuery change event from the browser eventTarget.dispatchEvent(createDomEvent('Event', 'change')); - expect(await newSize, const ui.Size(10, 10) * 3.2); + expect(newSize, completes); + expect(provider.computePhysicalSize(), const ui.Size(32, 32)); }); test('closed by onHotRestart', () async { From 90c71e4979912e9b1a600b0a16e7aed4092c878c Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Thu, 1 Feb 2024 17:23:01 -0800 Subject: [PATCH 09/12] Remove TestOn annotation from test files. --- .../custom_element_dimensions_provider_test.dart | 3 --- .../dimensions_provider/dimensions_provider_test.dart | 3 --- .../full_page_dimensions_provider_test.dart | 3 --- .../test/engine/view_embedder/display_dpr_stream_test.dart | 3 --- 4 files changed, 12 deletions(-) diff --git a/lib/web_ui/test/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider_test.dart b/lib/web_ui/test/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider_test.dart index 09a30c0d4267f..7effcff97777c 100644 --- a/lib/web_ui/test/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider_test.dart +++ b/lib/web_ui/test/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider_test.dart @@ -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'; diff --git a/lib/web_ui/test/engine/view_embedder/dimensions_provider/dimensions_provider_test.dart b/lib/web_ui/test/engine/view_embedder/dimensions_provider/dimensions_provider_test.dart index ce1aea48def10..7c355f94fdd17 100644 --- a/lib/web_ui/test/engine/view_embedder/dimensions_provider/dimensions_provider_test.dart +++ b/lib/web_ui/test/engine/view_embedder/dimensions_provider/dimensions_provider_test.dart @@ -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'; diff --git a/lib/web_ui/test/engine/view_embedder/dimensions_provider/full_page_dimensions_provider_test.dart b/lib/web_ui/test/engine/view_embedder/dimensions_provider/full_page_dimensions_provider_test.dart index 88a037c053e45..cd633da01526d 100644 --- a/lib/web_ui/test/engine/view_embedder/dimensions_provider/full_page_dimensions_provider_test.dart +++ b/lib/web_ui/test/engine/view_embedder/dimensions_provider/full_page_dimensions_provider_test.dart @@ -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'; diff --git a/lib/web_ui/test/engine/view_embedder/display_dpr_stream_test.dart b/lib/web_ui/test/engine/view_embedder/display_dpr_stream_test.dart index eef57d4ccf0f0..a32ff78620fcf 100644 --- a/lib/web_ui/test/engine/view_embedder/display_dpr_stream_test.dart +++ b/lib/web_ui/test/engine/view_embedder/display_dpr_stream_test.dart @@ -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'; From b061adaeaf98cd09625e53b490d18c503c1dcb89 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Mon, 5 Feb 2024 12:46:38 -0800 Subject: [PATCH 10/12] Clarify Logical Pixels explanation. --- lib/web_ui/lib/src/engine/html/scene.dart | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/web_ui/lib/src/engine/html/scene.dart b/lib/web_ui/lib/src/engine/html/scene.dart index 117f69e0c17d2..b9f69996b1097 100644 --- a/lib/web_ui/lib/src/engine/html/scene.dart +++ b/lib/web_ui/lib/src/engine/html/scene.dart @@ -45,7 +45,13 @@ class PersistedScene extends PersistedContainerSurface { @override void recomputeTransformAndClip() { - // The scene clip is the size of the entire window (Logical pixels). + // 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; From c4933d186ad9b71fb9cd5c824b942b2e2c28e0db Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Mon, 5 Feb 2024 16:32:07 -0800 Subject: [PATCH 11/12] Clarify why onResize emits null events. --- .../dimensions_provider/dimensions_provider.dart | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/dimensions_provider.dart b/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/dimensions_provider.dart index 3aaad364671d2..9ee403a2b3b83 100644 --- a/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/dimensions_provider.dart +++ b/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/dimensions_provider.dart @@ -50,6 +50,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 get onResize; /// Whether the [DimensionsProvider] instance has been closed or not. From 3f19731ee4e08bc3cf8daad6ca9573382d21fa41 Mon Sep 17 00:00:00 2001 From: David Iglesias Teixeira Date: Mon, 5 Feb 2024 17:02:55 -0800 Subject: [PATCH 12/12] Inject the dprChanged Stream from the constructor, instead of grabbing a static field directly. --- .../custom_element_dimensions_provider.dart | 11 +++++--- .../dimensions_provider.dart | 6 ++++- ...stom_element_dimensions_provider_test.dart | 25 ++++++++----------- 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider.dart b/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider.dart index 39f67a6a60226..25f77afdd5038 100644 --- a/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider.dart +++ b/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider.dart @@ -6,7 +6,6 @@ import 'dart:async'; import 'package:ui/src/engine/display.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; @@ -14,6 +13,10 @@ 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. @@ -24,9 +27,11 @@ import 'dimensions_provider.dart'; /// to be effective. class CustomElementDimensionsProvider extends DimensionsProvider { /// Creates a [CustomElementDimensionsProvider] from a [_hostElement]. - CustomElementDimensionsProvider(this._hostElement) { + CustomElementDimensionsProvider(this._hostElement, { + Stream? onDprChange, + }) { // Send a resize event when the page DPR changes. - _dprChangeStreamSubscription = DisplayDprStream.instance.dprChanged.listen((_) { + _dprChangeStreamSubscription = onDprChange?.listen((_) { _broadcastSize(null); }); diff --git a/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/dimensions_provider.dart b/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/dimensions_provider.dart index 9ee403a2b3b83..469f3a197d25b 100644 --- a/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/dimensions_provider.dart +++ b/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/dimensions_provider.dart @@ -6,6 +6,7 @@ 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; @@ -31,7 +32,10 @@ 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(); } diff --git a/lib/web_ui/test/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider_test.dart b/lib/web_ui/test/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider_test.dart index 7effcff97777c..4416aa59bc56c 100644 --- a/lib/web_ui/test/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider_test.dart +++ b/lib/web_ui/test/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider_test.dart @@ -132,24 +132,21 @@ void doTests() { test('funnels DPR change events too', () async { // Override the source of DPR events... - final DomEventTarget eventTarget = createDomElement('div'); - DisplayDprStream.instance = DisplayDprStream( - EngineFlutterDisplay.instance, - overrides: DebugDisplayDprStreamOverrides( - getMediaQuery: (_) => eventTarget, - ) + final StreamController dprController = + StreamController.broadcast(); + + // Inject the dprController stream into the CustomElementDimensionsProvider. + final CustomElementDimensionsProvider provider = + CustomElementDimensionsProvider( + sizeSource, + onDprChange: dprController.stream, ); - final CustomElementDimensionsProvider provider = CustomElementDimensionsProvider(sizeSource); - // The size that will be emitted by the provider eventually - final Future newSize = provider.onResize.first; - - // Set the mock DPR value + // Set and broadcast the mock DPR value EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(3.2); - // Simulate the mediaQuery change event from the browser - eventTarget.dispatchEvent(createDomEvent('Event', 'change')); + dprController.add(3.2); - expect(newSize, completes); + expect(provider.onResize.first, completes); expect(provider.computePhysicalSize(), const ui.Size(32, 32)); });