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

[web] Handle resizes at the view level #48892

Merged
merged 3 commits into from
Dec 12, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 0 additions & 35 deletions lib/web_ui/lib/src/engine/embedder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,10 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:ui/ui.dart' as ui;

import '../engine.dart' show buildMode, renderer;
import 'browser_detection.dart';
import 'configuration.dart';
import 'dom.dart';
import 'platform_dispatcher.dart';
import 'text_editing/text_editing.dart';
import 'view_embedder/style_manager.dart';
import 'window.dart';

/// Controls the placement and lifecycle of a Flutter view on the web page.
Expand Down Expand Up @@ -41,8 +36,6 @@ class FlutterViewEmbedder {
/// global resources such svg filters and clip paths when using webkit.
DomElement? _resourcesHost;

DomElement get _semanticsHostElement => window.dom.semanticsHost;

DomElement get _flutterViewElement => window.dom.rootElement;
DomShadowRoot get _glassPaneShadow => window.dom.renderingHost;

Expand All @@ -64,34 +57,6 @@ class FlutterViewEmbedder {
);

renderer.reset(this);

window.onResize.listen(_metricsDidChange);
}

/// Called immediately after browser window metrics change.
///
/// When there is a text editing going on in mobile devices, do not change
/// the physicalSize, change the [window.viewInsets]. See:
/// https://api.flutter.dev/flutter/dart-ui/FlutterView/viewInsets.html
/// https://api.flutter.dev/flutter/dart-ui/FlutterView/physicalSize.html
///
/// Note: always check for rotations for a mobile device. Update the physical
/// size if the change is caused by a rotation.
void _metricsDidChange(ui.Size? newSize) {
StyleManager.scaleSemanticsHost(
_semanticsHostElement,
window.devicePixelRatio,
);
// TODO(dit): Do not computePhysicalSize twice, https://github.com/flutter/flutter/issues/117036
if (isMobile && !window.isRotation() && textEditing.isEditing) {
window.computeOnScreenKeyboardInsets(true);
EnginePlatformDispatcher.instance.invokeOnMetricsChanged();
} else {
window.computePhysicalSize();
// When physical size changes this value has to be recalculated.
window.computeOnScreenKeyboardInsets(false);
EnginePlatformDispatcher.instance.invokeOnMetricsChanged();
}
}

/// Add an element as a global resource to be referenced by CSS.
Expand Down
130 changes: 74 additions & 56 deletions lib/web_ui/lib/src/engine/window.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'package:ui/ui.dart' as ui;
import 'package:ui/ui_web/src/ui_web.dart' as ui_web;

import '../engine.dart' show DimensionsProvider, registerHotRestartListener, renderer;
import 'browser_detection.dart';
import 'display.dart';
import 'dom.dart';
import 'mouse/context_menu.dart';
Expand All @@ -20,9 +21,11 @@ import 'platform_views/message_handler.dart';
import 'pointer_binding.dart';
import 'semantics.dart';
import 'services.dart';
import 'text_editing/text_editing.dart';
import 'util.dart';
import 'view_embedder/dom_manager.dart';
import 'view_embedder/embedding_strategy/embedding_strategy.dart';
import 'view_embedder/style_manager.dart';

typedef _HandleMessageCallBack = Future<bool> Function();

Expand Down Expand Up @@ -61,6 +64,7 @@ base class EngineFlutterView implements ui.FlutterView {
// hot restart.
embeddingStrategy.attachViewRoot(dom.rootElement);
pointerBinding = PointerBinding(this);
onResize.listen(_didResize);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to stop listening on dispose of the view?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure, since we are closing the stream on dispose anyway. I'll remove the listener too just to be safe.

registerHotRestartListener(dispose);
}

Expand Down Expand Up @@ -136,41 +140,32 @@ base class EngineFlutterView implements ui.FlutterView {

@override
ui.Size get physicalSize {
if (_physicalSize == null) {
computePhysicalSize();
}
assert(_physicalSize != null);
return _physicalSize!;
return _physicalSize ??= _computePhysicalSize();
}

/// Lazily populated and cleared at the end of the frame.
ui.Size? _physicalSize;

ui.Size? debugPhysicalSizeOverride;

/// Computes the physical size of the screen from [domWindow].
/// Computes the physical size of the view.
///
/// This function is expensive. It triggers browser layout if there are
/// pending DOM writes.
void computePhysicalSize() {
bool override = false;
ui.Size _computePhysicalSize() {
ui.Size? physicalSizeOverride;

assert(() {
if (debugPhysicalSizeOverride != null) {
_physicalSize = debugPhysicalSizeOverride;
override = true;
}
physicalSizeOverride = debugPhysicalSizeOverride;
return true;
}());

if (!override) {
_physicalSize = dimensionsProvider.computePhysicalSize();
}
return physicalSizeOverride ?? dimensionsProvider.computePhysicalSize();
}

/// Forces the view to recompute its physical size. Useful for tests.
void debugForceResize() {
computePhysicalSize();
_physicalSize = _computePhysicalSize();
}

@override
Expand Down Expand Up @@ -202,6 +197,69 @@ base class EngineFlutterView implements ui.FlutterView {
final DimensionsProvider dimensionsProvider;

Stream<ui.Size?> get onResize => dimensionsProvider.onResize;

/// Called immediately after the view has been resized.
///
/// When there is a text editing going on in mobile devices, do not change
/// the physicalSize, change the [window.viewInsets]. See:
/// https://api.flutter.dev/flutter/dart-ui/FlutterView/viewInsets.html
/// https://api.flutter.dev/flutter/dart-ui/FlutterView/physicalSize.html
///
/// Note: always check for rotations for a mobile device. Update the physical
/// size if the change is caused by a rotation.
void _didResize(ui.Size? newSize) {
StyleManager.scaleSemanticsHost(dom.semanticsHost, devicePixelRatio);
final ui.Size newPhysicalSize = _computePhysicalSize();
final bool isEditingOnMobile =
isMobile && !_isRotation(newPhysicalSize) && textEditing.isEditing;
if (isEditingOnMobile) {
_computeOnScreenKeyboardInsets(true);
} else {
_physicalSize = newPhysicalSize;
// When physical size changes this value has to be recalculated.
_computeOnScreenKeyboardInsets(false);
}
Comment on lines +216 to +224
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm 99% sure this bit only matters when the app is embedded in full-screen mode (the only one that really computes keyboard insets).

Can we make this didResize something that is overridable in the Implicit view + full-screen implementation, and move this version of the logic there?

The case of embedded views in custom elements shouldn't worry about this. WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What you are suggesting makes sense. But I would like to do it in a separate PR when I have the time to refactor it and test it properly.

platformDispatcher.invokeOnMetricsChanged();
}

/// Uses the previous physical size and current innerHeight/innerWidth
/// values to decide if a device is rotating.
///
/// During a rotation the height and width values will (almost) swap place.
/// Values can slightly differ due to space occupied by the browser header.
/// For example the following values are collected for Pixel 3 rotation:
///
/// height: 658 width: 393
/// new height: 313 new width: 738
///
/// The following values are from a changed caused by virtual keyboard.
///
/// height: 658 width: 393
/// height: 368 width: 393
bool _isRotation(ui.Size newPhysicalSize) {
// This method compares the new dimensions with the previous ones.
// Return false if the previous dimensions are not set.
if (_physicalSize != null) {
// First confirm both height and width are effected.
if (_physicalSize!.height != newPhysicalSize.height && _physicalSize!.width != newPhysicalSize.width) {
// If prior to rotation height is bigger than width it should be the
// opposite after the rotation and vice versa.
if ((_physicalSize!.height > _physicalSize!.width && newPhysicalSize.height < newPhysicalSize.width) ||
(_physicalSize!.width > _physicalSize!.height && newPhysicalSize.width < newPhysicalSize.height)) {
// Rotation detected
return true;
}
}
}
return false;
}

void _computeOnScreenKeyboardInsets(bool isEditingOnMobile) {
_viewInsets = dimensionsProvider.computeKeyboardInsets(
_physicalSize!.height,
isEditingOnMobile,
);
}
}

final class _EngineFlutterViewImpl extends EngineFlutterView {
Expand Down Expand Up @@ -543,46 +601,6 @@ final class EngineFlutterWindow extends EngineFlutterView implements ui.Singleto
display.debugOverrideDevicePixelRatio(value);
}

void computeOnScreenKeyboardInsets(bool isEditingOnMobile) {
_viewInsets = dimensionsProvider.computeKeyboardInsets(
_physicalSize!.height,
isEditingOnMobile,
);
}

/// Uses the previous physical size and current innerHeight/innerWidth
/// values to decide if a device is rotating.
///
/// During a rotation the height and width values will (almost) swap place.
/// Values can slightly differ due to space occupied by the browser header.
/// For example the following values are collected for Pixel 3 rotation:
///
/// height: 658 width: 393
/// new height: 313 new width: 738
///
/// The following values are from a changed caused by virtual keyboard.
///
/// height: 658 width: 393
/// height: 368 width: 393
bool isRotation() {
// This method compares the new dimensions with the previous ones.
// Return false if the previous dimensions are not set.
if (_physicalSize != null) {
final ui.Size current = dimensionsProvider.computePhysicalSize();
// First confirm both height and width are effected.
if (_physicalSize!.height != current.height && _physicalSize!.width != current.width) {
// If prior to rotation height is bigger than width it should be the
// opposite after the rotation and vice versa.
if ((_physicalSize!.height > _physicalSize!.width && current.height < current.width) ||
(_physicalSize!.width > _physicalSize!.height && current.width < current.height)) {
// Rotation detected
return true;
}
}
}
return false;
}

// TODO(mdebbar): Deprecate this and remove it.
// https://github.com/flutter/flutter/issues/127395
ui.Size? get webOnlyDebugPhysicalSizeOverride {
Expand Down
64 changes: 64 additions & 0 deletions lib/web_ui/test/engine/window_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -536,4 +536,68 @@ Future<void> testMain() async {
throwsAssertionError,
);
});

group('resizing', () {
late DomHTMLDivElement host;
late EngineFlutterView view;
late int metricsChangedCount;

setUp(() async {
EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(2.5);
host = createDomHTMLDivElement();
view = EngineFlutterView(EnginePlatformDispatcher.instance, host);

host.style
..width = '10px'
..height = '10px';
domDocument.body!.append(host);
// Let the DOM settle before starting the test, so we don't get the first
// 10,10 Size in the test. Otherwise, the ResizeObserver may trigger
// unexpectedly after the test has started, and break our "first" result.
await Future<void>.delayed(const Duration(milliseconds: 250));

metricsChangedCount = 0;
view.platformDispatcher.onMetricsChanged = () {
metricsChangedCount++;
};
});

tearDown(() {
view.dispose();
host.remove();
EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(null);
view.platformDispatcher.onMetricsChanged = null;
});

test('listens to resize', () async {
// Initial size is 10x10, with a 2.5 dpr, is equal to 25x25 physical pixels.
expect(view.physicalSize, const ui.Size(25.0, 25.0));
expect(metricsChangedCount, 0);

// Resize the host to 20x20.
host.style
..width = '20px'
..height = '20px';
await view.onResize.first;
expect(view.physicalSize, const ui.Size(50.0, 50.0));
expect(metricsChangedCount, 1);
});

test('maintains debugPhysicalSizeOverride', () async {
// Initial size is 10x10, with a 2.5 dpr, is equal to 25x25 physical pixels.
expect(view.physicalSize, const ui.Size(25.0, 25.0));

view.debugPhysicalSizeOverride = const ui.Size(100.0, 100.0);
view.debugForceResize();
expect(view.physicalSize, const ui.Size(100.0, 100.0));

// Resize the host to 20x20.
host.style
..width = '20px'
..height = '20px';
await view.onResize.first;
// The view should maintain the debugPhysicalSizeOverride.
expect(view.physicalSize, const ui.Size(100.0, 100.0));
});
});
}