mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
[web] Add dynamic view sizing (v2) (flutter/engine#50271)
### Changes
* Introduces a new `viewConstraints` JS configuration parameter to configure max/min width/height constraints for a view. Those can have the following values:
* An integer `>= 0`: max/min size in pixels
* `Infinity` (or `Number.POSITIVE_INFINITY`): (only for max values) -> **unconstrained**.
* When any value is not set, it defaults to "tight to the current size".
* See [Understanding constraints](https://docs.flutter.dev/ui/layout/constraints).
* Computes the correct `physicalConstraints` of a view off of its `physicalSize` and its `viewConstraints` for the framework to use during layout.
* When no constraints are passed, the current behavior is preserved: the default constraints are "tight" to the `physicalSize`.
* Resizes the current view DOM when requested by the framework and updates its internal physicalSize, then continues with the render procedure.
### Example
This is how we can configure a view to "take as much vertical space as needed":
```js
flutterApp.addView({
viewConstraints: {
minHeight: 0,
maxHeight: Infinity,
},
hostElement: ...,
});
```
### TODO
* Needs actual unit tests
### Issues
* Fixes https://github.com/flutter/flutter/issues/137444
* Closes https://github.com/flutter/engine/pull/48541
[C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
This commit is contained in:
parent
a499590726
commit
45adab9726
@ -90,8 +90,6 @@ abstract class PlatformDispatcher {
|
||||
|
||||
void scheduleFrame();
|
||||
|
||||
Future<void> render(Scene scene, [FlutterView view]);
|
||||
|
||||
AccessibilityFeatures get accessibilityFeatures;
|
||||
|
||||
VoidCallback? get onAccessibilityFeaturesChanged;
|
||||
|
||||
@ -25,9 +25,38 @@ extension JsFlutterViewOptionsExtension on JsFlutterViewOptions {
|
||||
return _hostElement!;
|
||||
}
|
||||
|
||||
@JS('viewConstraints')
|
||||
external JsViewConstraints? get _viewConstraints;
|
||||
JsViewConstraints? get viewConstraints {
|
||||
return _viewConstraints;
|
||||
}
|
||||
|
||||
external JSAny? get initialData;
|
||||
}
|
||||
|
||||
/// The JS bindings for a [ViewConstraints] object.
|
||||
@JS()
|
||||
@anonymous
|
||||
@staticInterop
|
||||
class JsViewConstraints {
|
||||
external factory JsViewConstraints({
|
||||
double? minWidth,
|
||||
double? maxWidth,
|
||||
double? minHeight,
|
||||
double? maxHeight,
|
||||
});
|
||||
}
|
||||
|
||||
/// The attributes of a [JsViewConstraints] object.
|
||||
///
|
||||
/// These attributes are expressed in *logical* pixels.
|
||||
extension JsViewConstraintsExtension on JsViewConstraints {
|
||||
external double? get maxHeight;
|
||||
external double? get maxWidth;
|
||||
external double? get minHeight;
|
||||
external double? get minWidth;
|
||||
}
|
||||
|
||||
/// The public JS API of a running Flutter Web App.
|
||||
@JS()
|
||||
@anonymous
|
||||
|
||||
@ -797,27 +797,25 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
|
||||
/// scheduling of frames.
|
||||
/// * [RendererBinding], the Flutter framework class which manages layout and
|
||||
/// painting.
|
||||
@override
|
||||
Future<void> render(ui.Scene scene, [ui.FlutterView? view]) async {
|
||||
assert(view != null || implicitView != null,
|
||||
'Calling render without a FlutterView');
|
||||
if (view == null && implicitView == null) {
|
||||
final EngineFlutterView? target = (view ?? implicitView) as EngineFlutterView?;
|
||||
assert(target != null, 'Calling render without a FlutterView');
|
||||
if (target == null) {
|
||||
// If there is no view to render into, then this is a no-op.
|
||||
return;
|
||||
}
|
||||
final ui.FlutterView viewToRender = view ?? implicitView!;
|
||||
|
||||
// Only render in an `onDrawFrame` or `onBeginFrame` scope. This is checked
|
||||
// by checking if the `_viewsRenderedInCurrentFrame` is non-null and this
|
||||
// view hasn't been rendered already in this scope.
|
||||
final bool shouldRender =
|
||||
_viewsRenderedInCurrentFrame?.add(viewToRender) ?? false;
|
||||
_viewsRenderedInCurrentFrame?.add(target) ?? false;
|
||||
// TODO(harryterkelsen): HTML renderer needs to violate the render rule in
|
||||
// order to perform golden tests in Flutter framework because on the HTML
|
||||
// renderer, golden tests render to DOM and then take a browser screenshot,
|
||||
// https://github.com/flutter/flutter/issues/137073.
|
||||
if (shouldRender || renderer.rendererTag == 'html') {
|
||||
await renderer.renderScene(scene, viewToRender);
|
||||
await renderer.renderScene(scene, target);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -39,8 +39,11 @@ class FlutterViewManager {
|
||||
EngineFlutterView createAndRegisterView(
|
||||
JsFlutterViewOptions jsViewOptions,
|
||||
) {
|
||||
final EngineFlutterView view =
|
||||
EngineFlutterView(_dispatcher, jsViewOptions.hostElement);
|
||||
final EngineFlutterView view = EngineFlutterView(
|
||||
_dispatcher,
|
||||
jsViewOptions.hostElement,
|
||||
viewConstraints: jsViewOptions.viewConstraints,
|
||||
);
|
||||
registerView(view, jsViewOptions: jsViewOptions);
|
||||
return view;
|
||||
}
|
||||
|
||||
@ -15,6 +15,7 @@ import 'configuration.dart';
|
||||
import 'display.dart';
|
||||
import 'dom.dart';
|
||||
import 'initialization.dart';
|
||||
import 'js_interop/js_app.dart';
|
||||
import 'mouse/context_menu.dart';
|
||||
import 'mouse/cursor.dart';
|
||||
import 'navigation/history.dart';
|
||||
@ -50,7 +51,9 @@ base class EngineFlutterView implements ui.FlutterView {
|
||||
/// the Flutter view will be rendered.
|
||||
factory EngineFlutterView(
|
||||
EnginePlatformDispatcher platformDispatcher,
|
||||
DomElement hostElement,
|
||||
DomElement hostElement, {
|
||||
JsViewConstraints? viewConstraints,
|
||||
}
|
||||
) = _EngineFlutterViewImpl;
|
||||
|
||||
EngineFlutterView._(
|
||||
@ -59,8 +62,11 @@ base class EngineFlutterView implements ui.FlutterView {
|
||||
// This is nullable to accommodate the legacy `EngineFlutterWindow`. In
|
||||
// multi-view mode, the host element is required for each view (as reflected
|
||||
// by the public `EngineFlutterView` constructor).
|
||||
DomElement? hostElement,
|
||||
) : embeddingStrategy = EmbeddingStrategy.create(hostElement: hostElement),
|
||||
DomElement? hostElement, {
|
||||
JsViewConstraints? viewConstraints,
|
||||
}
|
||||
) : _jsViewConstraints = viewConstraints,
|
||||
embeddingStrategy = EmbeddingStrategy.create(hostElement: hostElement),
|
||||
dimensionsProvider = DimensionsProvider.create(hostElement: hostElement) {
|
||||
// The embeddingStrategy will take care of cleaning up the rootElement on
|
||||
// hot restart.
|
||||
@ -117,7 +123,9 @@ base class EngineFlutterView implements ui.FlutterView {
|
||||
@override
|
||||
void render(ui.Scene scene, {ui.Size? size}) {
|
||||
assert(!isDisposed, 'Trying to render a disposed EngineFlutterView.');
|
||||
// TODO(goderbauer): Respect the provided size when "physicalConstraints" are not always tight. See TODO on "physicalConstraints".
|
||||
if (size != null) {
|
||||
resize(size);
|
||||
}
|
||||
platformDispatcher.render(scene, this);
|
||||
}
|
||||
|
||||
@ -145,9 +153,14 @@ base class EngineFlutterView implements ui.FlutterView {
|
||||
|
||||
late final PointerBinding pointerBinding;
|
||||
|
||||
// TODO(goderbauer): Provide API to configure constraints. See also TODO in "render".
|
||||
@override
|
||||
ViewConstraints get physicalConstraints => ViewConstraints.tight(physicalSize);
|
||||
ViewConstraints get physicalConstraints {
|
||||
final double dpr = devicePixelRatio;
|
||||
final ui.Size currentLogicalSize = physicalSize / dpr;
|
||||
return ViewConstraints.fromJs(_jsViewConstraints, currentLogicalSize) * dpr;
|
||||
}
|
||||
|
||||
final JsViewConstraints? _jsViewConstraints;
|
||||
|
||||
late final EngineSemanticsOwner semantics = EngineSemanticsOwner(dom.semanticsHost);
|
||||
|
||||
@ -156,6 +169,54 @@ base class EngineFlutterView implements ui.FlutterView {
|
||||
return _physicalSize ??= _computePhysicalSize();
|
||||
}
|
||||
|
||||
/// Resizes the `rootElement` to `newPhysicalSize` by changing its CSS style.
|
||||
///
|
||||
/// This is used by the [render] method, when the framework sends new dimensions
|
||||
/// for the current Flutter View.
|
||||
///
|
||||
/// Dimensions from the framework are constrained by the [physicalConstraints]
|
||||
/// that can be configured by the user when adding a view to the app.
|
||||
///
|
||||
/// In practice, this method changes the size of the `rootElement` of the app
|
||||
/// so it can push/shrink inside its `hostElement`. That way, a Flutter app
|
||||
/// can change the layout of the container page.
|
||||
///
|
||||
/// ```
|
||||
/// <p>Some HTML content...</p>
|
||||
/// +--- (div) hostElement ------------------------------------+
|
||||
/// | +--- rootElement ---------------------+ |
|
||||
/// | | | |
|
||||
/// | | | container |
|
||||
/// | | size applied to *this* | must be able |
|
||||
/// | | | to reflow |
|
||||
/// | | | |
|
||||
/// | +-------------------------------------+ |
|
||||
/// +----------------------------------------------------------+
|
||||
/// <p>More HTML content...</p>
|
||||
/// ```
|
||||
///
|
||||
/// The `hostElement` needs to be styled in a way that allows its size to flow
|
||||
/// with its contents. Things like `max-height: 100px; overflow: hidden` will
|
||||
/// work as expected (by hiding the overflowing part of the flutter app), but
|
||||
/// if in that case flutter is not made aware of that max-height with
|
||||
/// `physicalConstraints`, it will end up rendering more pixels that are visible
|
||||
/// on the screen, with a possible hit to performance.
|
||||
///
|
||||
/// TL;DR: The `viewConstraints` of a Flutter view, must take into consideration
|
||||
/// the CSS box-model restrictions imposed on its `hostElement` (especially when
|
||||
/// hiding `overflow`). Flutter does not attempt to interpret the styles of
|
||||
/// `hostElement` to compute its `physicalConstraints`, only its current size.
|
||||
void resize(ui.Size newPhysicalSize) {
|
||||
// The browser uses CSS, and CSS operates in logical sizes.
|
||||
final ui.Size logicalSize = newPhysicalSize / devicePixelRatio;
|
||||
dom.rootElement.style
|
||||
..width = '${logicalSize.width}px'
|
||||
..height = '${logicalSize.height}px';
|
||||
|
||||
// Force an update of the physicalSize so it's ready for the renderer.
|
||||
_computePhysicalSize();
|
||||
}
|
||||
|
||||
/// Lazily populated and cleared at the end of the frame.
|
||||
ui.Size? _physicalSize;
|
||||
|
||||
@ -278,8 +339,10 @@ base class EngineFlutterView implements ui.FlutterView {
|
||||
final class _EngineFlutterViewImpl extends EngineFlutterView {
|
||||
_EngineFlutterViewImpl(
|
||||
EnginePlatformDispatcher platformDispatcher,
|
||||
DomElement hostElement,
|
||||
) : super._(_nextViewId++, platformDispatcher, hostElement);
|
||||
DomElement hostElement, {
|
||||
JsViewConstraints? viewConstraints,
|
||||
}
|
||||
) : super._(_nextViewId++, platformDispatcher, hostElement, viewConstraints: viewConstraints);
|
||||
}
|
||||
|
||||
/// The Web implementation of [ui.SingletonFlutterWindow].
|
||||
@ -708,6 +771,27 @@ class ViewConstraints implements ui.ViewConstraints {
|
||||
minHeight = size.height,
|
||||
maxHeight = size.height;
|
||||
|
||||
/// Converts JsViewConstraints into ViewConstraints.
|
||||
///
|
||||
/// Since JsViewConstraints are expressed by the user, in logical pixels, this
|
||||
/// conversion uses logical pixels for the current size as well.
|
||||
///
|
||||
/// The resulting ViewConstraints object will be multiplied by devicePixelRatio
|
||||
/// later to compute the physicalViewConstraints, which is what the framework
|
||||
/// uses.
|
||||
factory ViewConstraints.fromJs(
|
||||
JsViewConstraints? constraints, ui.Size currentLogicalSize) {
|
||||
if (constraints == null) {
|
||||
return ViewConstraints.tight(currentLogicalSize);
|
||||
}
|
||||
return ViewConstraints(
|
||||
minWidth: _computeMinConstraintValue(constraints.minWidth, currentLogicalSize.width),
|
||||
minHeight: _computeMinConstraintValue(constraints.minHeight, currentLogicalSize.height),
|
||||
maxWidth: _computeMaxConstraintValue(constraints.maxWidth, currentLogicalSize.width),
|
||||
maxHeight: _computeMaxConstraintValue(constraints.maxHeight, currentLogicalSize.height),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
final double minWidth;
|
||||
@override
|
||||
@ -726,6 +810,15 @@ class ViewConstraints implements ui.ViewConstraints {
|
||||
@override
|
||||
bool get isTight => minWidth >= maxWidth && minHeight >= maxHeight;
|
||||
|
||||
ViewConstraints operator*(double factor) {
|
||||
return ViewConstraints(
|
||||
minWidth: minWidth * factor,
|
||||
maxWidth: maxWidth * factor,
|
||||
minHeight: minHeight * factor,
|
||||
maxHeight: maxHeight * factor,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
ViewConstraints operator/(double factor) {
|
||||
return ViewConstraints(
|
||||
@ -774,3 +867,31 @@ class ViewConstraints implements ui.ViewConstraints {
|
||||
return 'ViewConstraints($width, $height)';
|
||||
}
|
||||
}
|
||||
|
||||
// Computes the "min" value for a constraint that takes into account user `desired`
|
||||
// configuration and the actual available value.
|
||||
//
|
||||
// Returns the `desired` value unless it is `null`, in which case it returns the
|
||||
// `available` value.
|
||||
double _computeMinConstraintValue(double? desired, double available) {
|
||||
assert(desired == null || desired >= 0, 'Minimum constraint must be >= 0 if set.');
|
||||
assert(desired == null || desired.isFinite, 'Minimum constraint must be finite.');
|
||||
return desired ?? available;
|
||||
}
|
||||
|
||||
// Computes the "max" value for a constraint that takes into account user `desired`
|
||||
// configuration and the `available` size.
|
||||
//
|
||||
// Returns the `desired` value unless it is `null`, in which case it returns the
|
||||
// `available` value.
|
||||
//
|
||||
// A `desired` value of `Infinity` or `Number.POSITIVE_INFINITY` (from JS) means
|
||||
// "unconstrained".
|
||||
//
|
||||
// This method allows returning values larger than `available`, so the Flutter
|
||||
// app is able to stretch its container up to a certain value, without being
|
||||
// fully unconstrained.
|
||||
double _computeMaxConstraintValue(double? desired, double available) {
|
||||
assert(desired == null || desired >= 0, 'Maximum constraint must be >= 0 if set.');
|
||||
return desired ?? available;
|
||||
}
|
||||
|
||||
@ -5,36 +5,39 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:test/test.dart';
|
||||
import 'package:ui/src/engine.dart' show EnginePlatformDispatcher;
|
||||
import 'package:ui/ui.dart' as ui;
|
||||
|
||||
/// Tests frame timings in a renderer-agnostic way.
|
||||
///
|
||||
/// See CanvasKit-specific and HTML-specific test files `frame_timings_test.dart`.
|
||||
Future<void> runFrameTimingsTest() async {
|
||||
final EnginePlatformDispatcher dispatcher = ui.PlatformDispatcher.instance as EnginePlatformDispatcher;
|
||||
|
||||
List<ui.FrameTiming>? timings;
|
||||
ui.PlatformDispatcher.instance.onReportTimings = (List<ui.FrameTiming> data) {
|
||||
dispatcher.onReportTimings = (List<ui.FrameTiming> data) {
|
||||
timings = data;
|
||||
};
|
||||
Completer<void> frameDone = Completer<void>();
|
||||
ui.PlatformDispatcher.instance.onDrawFrame = () {
|
||||
dispatcher.onDrawFrame = () {
|
||||
final ui.SceneBuilder sceneBuilder = ui.SceneBuilder();
|
||||
sceneBuilder
|
||||
..pushOffset(0, 0)
|
||||
..pop();
|
||||
ui.PlatformDispatcher.instance.render(sceneBuilder.build()).then((_) {
|
||||
dispatcher.render(sceneBuilder.build()).then((_) {
|
||||
frameDone.complete();
|
||||
});
|
||||
};
|
||||
|
||||
// Frame 1.
|
||||
ui.PlatformDispatcher.instance.scheduleFrame();
|
||||
dispatcher.scheduleFrame();
|
||||
await frameDone.future;
|
||||
expect(timings, isNull, reason: "100 ms hasn't passed yet");
|
||||
await Future<void>.delayed(const Duration(milliseconds: 150));
|
||||
|
||||
// Frame 2.
|
||||
frameDone = Completer<void>();
|
||||
ui.PlatformDispatcher.instance.scheduleFrame();
|
||||
dispatcher.scheduleFrame();
|
||||
await frameDone.future;
|
||||
expect(timings, hasLength(2), reason: '100 ms passed. 2 frames pumped.');
|
||||
for (final ui.FrameTiming timing in timings!) {
|
||||
|
||||
@ -0,0 +1,155 @@
|
||||
// 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';
|
||||
import 'package:ui/ui.dart' as ui;
|
||||
|
||||
import '../../common/matchers.dart';
|
||||
|
||||
void main() {
|
||||
internalBootstrapBrowserTest(() => testMain);
|
||||
}
|
||||
|
||||
Future<void> testMain() async {
|
||||
const ui.Size size = ui.Size(640, 480);
|
||||
|
||||
group('ViewConstraints.fromJs', () {
|
||||
test('Negative min constraints -> Assertion error.', () async {
|
||||
expect(
|
||||
() => ViewConstraints.fromJs(
|
||||
JsViewConstraints(
|
||||
minWidth: -1,
|
||||
),
|
||||
size),
|
||||
throwsAssertionError);
|
||||
expect(
|
||||
() => ViewConstraints.fromJs(
|
||||
JsViewConstraints(
|
||||
minHeight: -1,
|
||||
),
|
||||
size),
|
||||
throwsAssertionError);
|
||||
});
|
||||
|
||||
test('Infinite min constraints -> Assertion error.', () async {
|
||||
expect(
|
||||
() => ViewConstraints.fromJs(
|
||||
JsViewConstraints(
|
||||
minWidth: double.infinity,
|
||||
),
|
||||
size),
|
||||
throwsAssertionError);
|
||||
expect(
|
||||
() => ViewConstraints.fromJs(
|
||||
JsViewConstraints(
|
||||
minHeight: double.infinity,
|
||||
),
|
||||
size),
|
||||
throwsAssertionError);
|
||||
});
|
||||
|
||||
test('Negative max constraints -> Assertion error.', () async {
|
||||
expect(
|
||||
() => ViewConstraints.fromJs(
|
||||
JsViewConstraints(
|
||||
maxWidth: -1,
|
||||
),
|
||||
size),
|
||||
throwsAssertionError);
|
||||
expect(
|
||||
() => ViewConstraints.fromJs(
|
||||
JsViewConstraints(
|
||||
maxHeight: -1,
|
||||
),
|
||||
size),
|
||||
throwsAssertionError);
|
||||
});
|
||||
|
||||
test('null JS Constraints -> Tight to size', () async {
|
||||
expect(
|
||||
ViewConstraints.fromJs(null, size),
|
||||
const ViewConstraints(
|
||||
minWidth: 640, maxWidth: 640, //
|
||||
minHeight: 480, maxHeight: 480, //
|
||||
));
|
||||
});
|
||||
|
||||
test('non-null JS Constraints -> Computes sizes', () async {
|
||||
final JsViewConstraints constraints = JsViewConstraints(
|
||||
minWidth: 500, maxWidth: 600, //
|
||||
minHeight: 300, maxHeight: 400, //
|
||||
);
|
||||
expect(
|
||||
ViewConstraints.fromJs(constraints, size),
|
||||
const ViewConstraints(
|
||||
minWidth: 500, maxWidth: 600, //
|
||||
minHeight: 300, maxHeight: 400, //
|
||||
));
|
||||
});
|
||||
|
||||
test('null JS Width -> Tight to width. Computes height.', () async {
|
||||
final JsViewConstraints constraints = JsViewConstraints(
|
||||
minHeight: 200,
|
||||
maxHeight: 320,
|
||||
);
|
||||
expect(
|
||||
ViewConstraints.fromJs(constraints, size),
|
||||
const ViewConstraints(
|
||||
minWidth: 640, maxWidth: 640, //
|
||||
minHeight: 200, maxHeight: 320, //
|
||||
));
|
||||
});
|
||||
|
||||
test('null JS Height -> Tight to height. Computed width.', () async {
|
||||
final JsViewConstraints constraints = JsViewConstraints(
|
||||
minWidth: 200,
|
||||
maxWidth: 320,
|
||||
);
|
||||
expect(
|
||||
ViewConstraints.fromJs(constraints, size),
|
||||
const ViewConstraints(
|
||||
minWidth: 200, maxWidth: 320, //
|
||||
minHeight: 480, maxHeight: 480, //
|
||||
));
|
||||
});
|
||||
|
||||
test(
|
||||
'non-null JS Constraints -> Computes sizes. Max values can be greater than available size.',
|
||||
() async {
|
||||
final JsViewConstraints constraints = JsViewConstraints(
|
||||
minWidth: 500, maxWidth: 1024, //
|
||||
minHeight: 300, maxHeight: 768, //
|
||||
);
|
||||
expect(
|
||||
ViewConstraints.fromJs(constraints, size),
|
||||
const ViewConstraints(
|
||||
minWidth: 500, maxWidth: 1024, //
|
||||
minHeight: 300, maxHeight: 768, //
|
||||
));
|
||||
});
|
||||
|
||||
test(
|
||||
'non-null JS Constraints -> Computes sizes. Max values can be unconstrained.',
|
||||
() async {
|
||||
final JsViewConstraints constraints = JsViewConstraints(
|
||||
minWidth: 500,
|
||||
maxWidth: double.infinity,
|
||||
minHeight: 300,
|
||||
maxHeight: double.infinity,
|
||||
);
|
||||
expect(
|
||||
ViewConstraints.fromJs(constraints, size),
|
||||
const ViewConstraints(
|
||||
// ignore: avoid_redundant_argument_values
|
||||
minWidth: 500, maxWidth: double.infinity,
|
||||
// ignore: avoid_redundant_argument_values
|
||||
minHeight: 300, maxHeight: double.infinity,
|
||||
));
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -584,10 +584,11 @@ Future<void> testMain() async {
|
||||
..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));
|
||||
await view.onResize.first;
|
||||
|
||||
metricsChangedCount = 0;
|
||||
view.platformDispatcher.onMetricsChanged = () {
|
||||
@ -607,7 +608,7 @@ Future<void> testMain() async {
|
||||
expect(view.physicalSize, const ui.Size(25.0, 25.0));
|
||||
expect(metricsChangedCount, 0);
|
||||
|
||||
// Resize the host to 20x20.
|
||||
// Simulate the browser resizing the host to 20x20.
|
||||
host.style
|
||||
..width = '20px'
|
||||
..height = '20px';
|
||||
@ -632,5 +633,68 @@ Future<void> testMain() async {
|
||||
// The view should maintain the debugPhysicalSizeOverride.
|
||||
expect(view.physicalSize, const ui.Size(100.0, 100.0));
|
||||
});
|
||||
|
||||
test('can resize host', () async {
|
||||
// Reset host style, so it tightly wraps the rootElement of the view.
|
||||
// This style change will trigger a "onResize" event when all the DOM
|
||||
// operations settle that we must await before taking measurements.
|
||||
host.style
|
||||
..display = 'inline-block'
|
||||
..width = 'auto'
|
||||
..height = 'auto';
|
||||
|
||||
// Resize the host to 20x20 (physical pixels).
|
||||
view.resize(const ui.Size.square(50));
|
||||
|
||||
await view.onResize.first;
|
||||
|
||||
// The host tightly wraps the rootElement:
|
||||
expect(view.physicalSize, const ui.Size(50.0, 50.0));
|
||||
|
||||
// Inspect the rootElement directly:
|
||||
expect(view.dom.rootElement.clientWidth, 50 / view.devicePixelRatio);
|
||||
expect(view.dom.rootElement.clientHeight, 50 / view.devicePixelRatio);
|
||||
});
|
||||
});
|
||||
|
||||
group('physicalConstraints', () {
|
||||
const double dpr = 2.5;
|
||||
late DomHTMLDivElement host;
|
||||
late EngineFlutterView view;
|
||||
|
||||
setUp(() async {
|
||||
EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(dpr);
|
||||
host = createDomHTMLDivElement()
|
||||
..style.width = '640px'
|
||||
..style.height = '480px';
|
||||
domDocument.body!.append(host);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
host.remove();
|
||||
EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(null);
|
||||
});
|
||||
|
||||
test('JsViewConstraints are passed and used to compute physicalConstraints', () async {
|
||||
view = EngineFlutterView(
|
||||
EnginePlatformDispatcher.instance,
|
||||
host,
|
||||
viewConstraints: JsViewConstraints(
|
||||
minHeight: 320,
|
||||
maxHeight: double.infinity,
|
||||
));
|
||||
|
||||
// All the metrics until now have been expressed in logical pixels, because
|
||||
// they're coming from CSS/the browser, which works in logical pixels.
|
||||
expect(view.physicalConstraints, const ViewConstraints(
|
||||
minHeight: 320,
|
||||
// ignore: avoid_redundant_argument_values
|
||||
maxHeight: double.infinity,
|
||||
minWidth: 640,
|
||||
maxWidth: 640,
|
||||
// However the framework expects physical pixels, so we multiply our expectations
|
||||
// by the current DPR (2.5)
|
||||
) * dpr);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user