blob: 59c6f37cc4b280b9bd973561613d20fe093b3fe9 [file] [log] [blame]
// 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:typed_data';
import 'package:meta/meta.dart';
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 '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';
import 'platform_dispatcher.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/global_html_attributes.dart';
import 'view_embedder/style_manager.dart';
typedef _HandleMessageCallBack = Future<bool> Function();
/// When set to true, all platform messages will be printed to the console.
const bool debugPrintPlatformMessages = false;
/// The view ID for the implicit flutter view provided by the platform.
const int kImplicitViewId = 0;
int _nextViewId = kImplicitViewId + 1;
/// Represents all views in the Flutter Web Engine.
///
/// In addition to everything defined in [ui.FlutterView], this class adds
/// a few web-specific properties.
base class EngineFlutterView implements ui.FlutterView {
/// Creates a [ui.FlutterView] that can be used in multi-view mode.
///
/// The [hostElement] parameter specifies the container in the DOM into which
/// the Flutter view will be rendered.
factory EngineFlutterView(
EnginePlatformDispatcher platformDispatcher,
DomElement hostElement, {
JsViewConstraints? viewConstraints,
}
) = _EngineFlutterViewImpl;
EngineFlutterView._(
this.viewId,
this.platformDispatcher,
// 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, {
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.
embeddingStrategy.attachViewRoot(dom.rootElement);
pointerBinding = PointerBinding(this);
_resizeSubscription = onResize.listen(_didResize);
_globalHtmlAttributes.applyAttributes(
viewId: viewId,
autoDetectRenderer: FlutterConfiguration.flutterWebAutoDetect,
rendererTag: renderer.rendererTag,
buildMode: buildMode,
);
registerHotRestartListener(dispose);
}
static EngineFlutterWindow implicit(
EnginePlatformDispatcher platformDispatcher,
DomElement? hostElement,
) => EngineFlutterWindow._(platformDispatcher, hostElement);
@override
final int viewId;
@override
final EnginePlatformDispatcher platformDispatcher;
/// Abstracts all the DOM manipulations required to embed a Flutter view in a user-supplied `hostElement`.
final EmbeddingStrategy embeddingStrategy;
late final StreamSubscription<ui.Size?> _resizeSubscription;
final ViewConfiguration _viewConfiguration = const ViewConfiguration();
/// Whether this [EngineFlutterView] has been disposed or not.
bool isDisposed = false;
/// Disposes of the [EngineFlutterView] instance and undoes all of its DOM
/// tree and any event listeners.
@mustCallSuper
void dispose() {
if (isDisposed) {
return;
}
isDisposed = true;
_resizeSubscription.cancel();
dimensionsProvider.close();
pointerBinding.dispose();
dom.rootElement.remove();
// TODO(harryterkelsen): What should we do about this in multi-view?
renderer.clearFragmentProgramCache();
semantics.reset();
}
@override
void render(ui.Scene scene, {ui.Size? size}) {
assert(!isDisposed, 'Trying to render a disposed EngineFlutterView.');
if (size != null) {
resize(size);
}
platformDispatcher.render(scene, this);
}
@override
void updateSemantics(ui.SemanticsUpdate update) {
assert(!isDisposed, 'Trying to update semantics on a disposed EngineFlutterView.');
semantics.updateSemantics(update);
}
late final GlobalHtmlAttributes _globalHtmlAttributes = GlobalHtmlAttributes(
rootElement: dom.rootElement,
hostElement: embeddingStrategy.hostElement,
);
late final MouseCursor mouseCursor = MouseCursor(dom.rootElement);
late final ContextMenu contextMenu = ContextMenu(dom.rootElement);
late final DomManager dom = DomManager(devicePixelRatio: devicePixelRatio);
late final PointerBinding pointerBinding;
@override
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);
@override
ui.Size get physicalSize {
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.
///
/// ```none
/// <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;
ui.Size? debugPhysicalSizeOverride;
/// Computes the physical size of the view.
///
/// This function is expensive. It triggers browser layout if there are
/// pending DOM writes.
ui.Size _computePhysicalSize() {
ui.Size? physicalSizeOverride;
assert(() {
physicalSizeOverride = debugPhysicalSizeOverride;
return true;
}());
return physicalSizeOverride ?? dimensionsProvider.computePhysicalSize();
}
/// Forces the view to recompute its physical size. Useful for tests.
void debugForceResize() {
_physicalSize = _computePhysicalSize();
}
@override
ViewPadding get viewInsets => _viewInsets;
ViewPadding _viewInsets = ui.ViewPadding.zero as ViewPadding;
@override
ViewPadding get viewPadding => _viewConfiguration.viewPadding;
@override
ViewPadding get systemGestureInsets => _viewConfiguration.systemGestureInsets;
@override
ViewPadding get padding => _viewConfiguration.padding;
@override
ui.GestureSettings get gestureSettings => _viewConfiguration.gestureSettings;
@override
List<ui.DisplayFeature> get displayFeatures => _viewConfiguration.displayFeatures;
@override
EngineFlutterDisplay get display => EngineFlutterDisplay.instance;
@override
double get devicePixelRatio => display.devicePixelRatio;
@visibleForTesting
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);
}
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 {
_EngineFlutterViewImpl(
EnginePlatformDispatcher platformDispatcher,
DomElement hostElement, {
JsViewConstraints? viewConstraints,
}
) : super._(_nextViewId++, platformDispatcher, hostElement, viewConstraints: viewConstraints);
}
/// The Web implementation of [ui.SingletonFlutterWindow].
final class EngineFlutterWindow extends EngineFlutterView implements ui.SingletonFlutterWindow {
EngineFlutterWindow._(
EnginePlatformDispatcher platformDispatcher,
DomElement? hostElement,
) : super._(kImplicitViewId, platformDispatcher, hostElement) {
if (ui_web.isCustomUrlStrategySet) {
_browserHistory = createHistoryForExistingState(ui_web.urlStrategy);
}
}
@override
void dispose() {
super.dispose();
_browserHistory?.dispose();
}
@override
ui.VoidCallback? get onMetricsChanged => platformDispatcher.onMetricsChanged;
@override
set onMetricsChanged(ui.VoidCallback? callback) {
platformDispatcher.onMetricsChanged = callback;
}
@override
ui.Locale get locale => platformDispatcher.locale;
@override
List<ui.Locale> get locales => platformDispatcher.locales;
@override
ui.Locale? computePlatformResolvedLocale(List<ui.Locale> supportedLocales) {
return platformDispatcher.computePlatformResolvedLocale(supportedLocales);
}
@override
ui.VoidCallback? get onLocaleChanged => platformDispatcher.onLocaleChanged;
@override
set onLocaleChanged(ui.VoidCallback? callback) {
platformDispatcher.onLocaleChanged = callback;
}
@override
String get initialLifecycleState => platformDispatcher.initialLifecycleState;
@override
double get textScaleFactor => platformDispatcher.textScaleFactor;
@override
bool get nativeSpellCheckServiceDefined => platformDispatcher.nativeSpellCheckServiceDefined;
@override
bool get brieflyShowPassword => platformDispatcher.brieflyShowPassword;
@override
bool get alwaysUse24HourFormat => platformDispatcher.alwaysUse24HourFormat;
@override
ui.VoidCallback? get onTextScaleFactorChanged => platformDispatcher.onTextScaleFactorChanged;
@override
set onTextScaleFactorChanged(ui.VoidCallback? callback) {
platformDispatcher.onTextScaleFactorChanged = callback;
}
@override
ui.Brightness get platformBrightness => platformDispatcher.platformBrightness;
@override
ui.VoidCallback? get onPlatformBrightnessChanged => platformDispatcher.onPlatformBrightnessChanged;
@override
set onPlatformBrightnessChanged(ui.VoidCallback? callback) {
platformDispatcher.onPlatformBrightnessChanged = callback;
}
@override
String? get systemFontFamily => platformDispatcher.systemFontFamily;
@override
ui.VoidCallback? get onSystemFontFamilyChanged => platformDispatcher.onSystemFontFamilyChanged;
@override
set onSystemFontFamilyChanged(ui.VoidCallback? callback) {
platformDispatcher.onSystemFontFamilyChanged = callback;
}
@override
ui.FrameCallback? get onBeginFrame => platformDispatcher.onBeginFrame;
@override
set onBeginFrame(ui.FrameCallback? callback) {
platformDispatcher.onBeginFrame = callback;
}
@override
ui.VoidCallback? get onDrawFrame => platformDispatcher.onDrawFrame;
@override
set onDrawFrame(ui.VoidCallback? callback) {
platformDispatcher.onDrawFrame = callback;
}
@override
ui.TimingsCallback? get onReportTimings => platformDispatcher.onReportTimings;
@override
set onReportTimings(ui.TimingsCallback? callback) {
platformDispatcher.onReportTimings = callback;
}
@override
ui.PointerDataPacketCallback? get onPointerDataPacket => platformDispatcher.onPointerDataPacket;
@override
set onPointerDataPacket(ui.PointerDataPacketCallback? callback) {
platformDispatcher.onPointerDataPacket = callback;
}
@override
ui.KeyDataCallback? get onKeyData => platformDispatcher.onKeyData;
@override
set onKeyData(ui.KeyDataCallback? callback) {
platformDispatcher.onKeyData = callback;
}
@override
String get defaultRouteName => platformDispatcher.defaultRouteName;
@override
void scheduleFrame() => platformDispatcher.scheduleFrame();
@override
bool get semanticsEnabled => platformDispatcher.semanticsEnabled;
@override
ui.VoidCallback? get onSemanticsEnabledChanged => platformDispatcher.onSemanticsEnabledChanged;
@override
set onSemanticsEnabledChanged(ui.VoidCallback? callback) {
platformDispatcher.onSemanticsEnabledChanged = callback;
}
@override
ui.FrameData get frameData => const ui.FrameData.webOnly();
@override
ui.VoidCallback? get onFrameDataChanged => null;
@override
set onFrameDataChanged(ui.VoidCallback? callback) {}
@override
ui.AccessibilityFeatures get accessibilityFeatures => platformDispatcher.accessibilityFeatures;
@override
ui.VoidCallback? get onAccessibilityFeaturesChanged =>
platformDispatcher.onAccessibilityFeaturesChanged;
@override
set onAccessibilityFeaturesChanged(ui.VoidCallback? callback) {
platformDispatcher.onAccessibilityFeaturesChanged = callback;
}
@override
void sendPlatformMessage(
String name,
ByteData? data,
ui.PlatformMessageResponseCallback? callback,
) {
platformDispatcher.sendPlatformMessage(name, data, callback);
}
@override
ui.PlatformMessageCallback? get onPlatformMessage => platformDispatcher.onPlatformMessage;
@override
set onPlatformMessage(ui.PlatformMessageCallback? callback) {
platformDispatcher.onPlatformMessage = callback;
}
@override
void setIsolateDebugName(String name) => ui.PlatformDispatcher.instance.setIsolateDebugName(name);
/// Handles the browser history integration to allow users to use the back
/// button, etc.
BrowserHistory get browserHistory {
return _browserHistory ??=
createHistoryForExistingState(_urlStrategyForInitialization);
}
ui_web.UrlStrategy? get _urlStrategyForInitialization {
// Prevent any further customization of URL strategy.
ui_web.preventCustomUrlStrategy();
return ui_web.urlStrategy;
}
BrowserHistory?
_browserHistory; // Must be either SingleEntryBrowserHistory or MultiEntriesBrowserHistory.
Future<void> _useSingleEntryBrowserHistory() async {
// Recreate the browser history mode that's appropriate for the existing
// history state.
//
// If it happens to be a single-entry one, then there's nothing further to do.
//
// But if it's a multi-entry one, it will be torn down below and replaced
// with a single-entry history.
//
// See: https://github.com/flutter/flutter/issues/79241
_browserHistory ??=
createHistoryForExistingState(_urlStrategyForInitialization);
if (_browserHistory is SingleEntryBrowserHistory) {
return;
}
// At this point, we know that `_browserHistory` is a non-null
// `MultiEntriesBrowserHistory` instance.
final ui_web.UrlStrategy? strategy = _browserHistory?.urlStrategy;
await _browserHistory?.tearDown();
_browserHistory = SingleEntryBrowserHistory(urlStrategy: strategy);
}
Future<void> _useMultiEntryBrowserHistory() async {
// Recreate the browser history mode that's appropriate for the existing
// history state.
//
// If it happens to be a multi-entry one, then there's nothing further to do.
//
// But if it's a single-entry one, it will be torn down below and replaced
// with a multi-entry history.
//
// See: https://github.com/flutter/flutter/issues/79241
_browserHistory ??=
createHistoryForExistingState(_urlStrategyForInitialization);
if (_browserHistory is MultiEntriesBrowserHistory) {
return;
}
// At this point, we know that `_browserHistory` is a non-null
// `SingleEntryBrowserHistory` instance.
final ui_web.UrlStrategy? strategy = _browserHistory?.urlStrategy;
await _browserHistory?.tearDown();
_browserHistory = MultiEntriesBrowserHistory(urlStrategy: strategy);
}
@visibleForTesting
Future<void> debugInitializeHistory(
ui_web.UrlStrategy? strategy, {
required bool useSingle,
}) async {
await _browserHistory?.tearDown();
ui_web.urlStrategy = strategy;
if (useSingle) {
_browserHistory = SingleEntryBrowserHistory(urlStrategy: strategy);
} else {
_browserHistory = MultiEntriesBrowserHistory(urlStrategy: strategy);
}
}
Future<void> resetHistory() async {
await _browserHistory?.tearDown();
_browserHistory = null;
ui_web.debugResetCustomUrlStrategy();
}
Future<void> _endOfTheLine = Future<void>.value();
Future<bool> _waitInTheLine(_HandleMessageCallBack callback) async {
final Future<void> currentPosition = _endOfTheLine;
final Completer<void> completer = Completer<void>();
_endOfTheLine = completer.future;
await currentPosition;
bool result = false;
try {
result = await callback();
} finally {
completer.complete();
}
return result;
}
Future<bool> handleNavigationMessage(ByteData? data) async {
return _waitInTheLine(() async {
final MethodCall decoded = const JSONMethodCodec().decodeMethodCall(data);
final Map<String, dynamic>? arguments = decoded.arguments as Map<String, dynamic>?;
switch (decoded.method) {
case 'selectMultiEntryHistory':
await _useMultiEntryBrowserHistory();
return true;
case 'selectSingleEntryHistory':
await _useSingleEntryBrowserHistory();
return true;
// the following cases assert that arguments are not null
case 'routeUpdated': // deprecated
assert(arguments != null);
await _useSingleEntryBrowserHistory();
browserHistory.setRouteName(arguments!.tryString('routeName'));
return true;
case 'routeInformationUpdated':
assert(arguments != null);
final String? uriString = arguments!.tryString('uri');
final String path;
if (uriString != null) {
final Uri uri = Uri.parse(uriString);
// Need to remove scheme and authority.
path = Uri.decodeComponent(
Uri(
path: uri.path.isEmpty ? '/' : uri.path,
queryParameters: uri.queryParametersAll.isEmpty ? null : uri.queryParametersAll,
fragment: uri.fragment.isEmpty ? null : uri.fragment,
).toString(),
);
} else {
path = arguments.tryString('location')!;
}
browserHistory.setRouteName(
path,
state: arguments['state'],
replace: arguments.tryBool('replace') ?? false,
);
return true;
}
return false;
});
}
// TODO(mdebbar): Deprecate this and remove it.
// https://github.com/flutter/flutter/issues/127395
void debugOverrideDevicePixelRatio(double? value) {
assert(() {
printWarning(
'The window.debugOverrideDevicePixelRatio API is deprecated and will '
'be removed in a future release. Please use '
'`debugOverrideDevicePixelRatio` from `dart:ui_web` instead.',
);
return true;
}());
display.debugOverrideDevicePixelRatio(value);
}
// TODO(mdebbar): Deprecate this and remove it.
// https://github.com/flutter/flutter/issues/127395
ui.Size? get webOnlyDebugPhysicalSizeOverride {
assert(() {
printWarning(
'The webOnlyDebugPhysicalSizeOverride API is deprecated and will be '
'removed in a future release. Please use '
'`SingletonFlutterWindow.debugPhysicalSizeOverride` from `dart:ui_web` '
'instead.',
);
return true;
}());
return debugPhysicalSizeOverride;
}
// TODO(mdebbar): Deprecate this and remove it.
// https://github.com/flutter/flutter/issues/127395
set webOnlyDebugPhysicalSizeOverride(ui.Size? value) {
assert(() {
printWarning(
'The webOnlyDebugPhysicalSizeOverride API is deprecated and will be '
'removed in a future release. Please use '
'`SingletonFlutterWindow.debugPhysicalSizeOverride` from `dart:ui_web` '
'instead.',
);
return true;
}());
debugPhysicalSizeOverride = value;
}
}
/// The window singleton.
///
/// `dart:ui` window delegates to this value. However, this value has a wider
/// API surface, providing Web-specific functionality that the standard
/// `dart:ui` version does not.
EngineFlutterWindow get window {
assert(
_window != null,
'Trying to access the implicit FlutterView, but it is not available.\n'
'Note: the implicit FlutterView is not available in multi-view mode.',
);
return _window!;
}
EngineFlutterWindow? _window;
/// Initializes the [window] (aka the implicit view), if it's not already
/// initialized.
EngineFlutterWindow ensureImplicitViewInitialized({
DomElement? hostElement,
}) {
if (_window == null) {
_window = EngineFlutterView.implicit(
EnginePlatformDispatcher.instance,
hostElement,
);
EnginePlatformDispatcher.instance.viewManager.registerView(_window!);
}
return _window!;
}
/// The Web implementation of [ui.ViewPadding].
class ViewPadding implements ui.ViewPadding {
const ViewPadding({
required this.left,
required this.top,
required this.right,
required this.bottom,
});
@override
final double left;
@override
final double top;
@override
final double right;
@override
final double bottom;
}
class ViewConstraints implements ui.ViewConstraints {
const ViewConstraints({
this.minWidth = 0.0,
this.maxWidth = double.infinity,
this.minHeight = 0.0,
this.maxHeight = double.infinity,
});
ViewConstraints.tight(ui.Size size)
: minWidth = size.width,
maxWidth = size.width,
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
final double maxWidth;
@override
final double minHeight;
@override
final double maxHeight;
@override
bool isSatisfiedBy(ui.Size size) {
return (minWidth <= size.width) && (size.width <= maxWidth) &&
(minHeight <= size.height) && (size.height <= maxHeight);
}
@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(
minWidth: minWidth / factor,
maxWidth: maxWidth / factor,
minHeight: minHeight / factor,
maxHeight: maxHeight / factor,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (other.runtimeType != runtimeType) {
return false;
}
return other is ViewConstraints
&& other.minWidth == minWidth
&& other.maxWidth == maxWidth
&& other.minHeight == minHeight
&& other.maxHeight == maxHeight;
}
@override
int get hashCode => Object.hash(minWidth, maxWidth, minHeight, maxHeight);
@override
String toString() {
if (minWidth == double.infinity && minHeight == double.infinity) {
return 'ViewConstraints(biggest)';
}
if (minWidth == 0 && maxWidth == double.infinity &&
minHeight == 0 && maxHeight == double.infinity) {
return 'ViewConstraints(unconstrained)';
}
String describe(double min, double max, String dim) {
if (min == max) {
return '$dim=${min.toStringAsFixed(1)}';
}
return '${min.toStringAsFixed(1)}<=$dim<=${max.toStringAsFixed(1)}';
}
final String width = describe(minWidth, maxWidth, 'w');
final String height = describe(minHeight, maxHeight, 'h');
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;
}