blob: 1d29c31d2e421f7a658d923fc4f2aa306a901c73 [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.
// @dart = 2.6
part of engine;
/// When set to true, all platform messages will be printed to the console.
const bool _debugPrintPlatformMessages = false;
/// The Web implementation of [ui.Window].
class EngineWindow extends ui.Window {
EngineWindow() {
_addBrightnessMediaQueryListener();
}
@override
double get devicePixelRatio => _debugDevicePixelRatio != null
? _debugDevicePixelRatio
: browserDevicePixelRatio;
/// Returns device pixel ratio returned by browser.
static double get browserDevicePixelRatio {
double ratio = html.window.devicePixelRatio;
// Guard against WebOS returning 0.
return (ratio == null || ratio == 0.0) ? 1.0 : ratio;
}
/// Overrides the default device pixel ratio.
///
/// This is useful in tests to emulate screens of different dimensions.
void debugOverrideDevicePixelRatio(double value) {
_debugDevicePixelRatio = value;
}
double _debugDevicePixelRatio;
@override
ui.Size get physicalSize {
if (_physicalSize == null) {
_computePhysicalSize();
}
assert(_physicalSize != null);
return _physicalSize;
}
/// Computes the physical size of the screen from [html.window].
///
/// This function is expensive. It triggers browser layout if there are
/// pending DOM writes.
void _computePhysicalSize() {
bool override = false;
assert(() {
if (webOnlyDebugPhysicalSizeOverride != null) {
_physicalSize = webOnlyDebugPhysicalSizeOverride;
override = true;
}
return true;
}());
if (!override) {
double windowInnerWidth;
double windowInnerHeight;
final html.VisualViewport viewport = html.window.visualViewport;
if (viewport != null) {
windowInnerWidth = viewport.width * devicePixelRatio;
windowInnerHeight = viewport.height * devicePixelRatio;
} else {
windowInnerWidth = html.window.innerWidth * devicePixelRatio;
windowInnerHeight = html.window.innerHeight * devicePixelRatio;
}
_physicalSize = ui.Size(
windowInnerWidth,
windowInnerHeight,
);
}
}
void computeOnScreenKeyboardInsets() {
double windowInnerHeight;
final html.VisualViewport viewport = html.window.visualViewport;
if (viewport != null) {
windowInnerHeight = viewport.height * devicePixelRatio;
} else {
windowInnerHeight = html.window.innerHeight * devicePixelRatio;
}
final double bottomPadding = _physicalSize.height - windowInnerHeight;
_viewInsets =
WindowPadding(bottom: bottomPadding, left: 0, right: 0, top: 0);
}
/// 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() {
double height = 0;
double width = 0;
if (html.window.visualViewport != null) {
height = html.window.visualViewport.height * devicePixelRatio;
width = html.window.visualViewport.width * devicePixelRatio;
} else {
height = html.window.innerHeight * devicePixelRatio;
width = html.window.innerWidth * devicePixelRatio;
}
// First confirm both heught and width is effected.
if (_physicalSize.height != height && _physicalSize.width != 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 && height < width) ||
(_physicalSize.width > _physicalSize.height && width < height)) {
// Rotation detected
return true;
}
}
return false;
}
@override
WindowPadding get viewInsets => _viewInsets;
WindowPadding _viewInsets = ui.WindowPadding.zero;
/// Lazily populated and cleared at the end of the frame.
ui.Size _physicalSize;
/// Overrides the value of [physicalSize] in tests.
ui.Size webOnlyDebugPhysicalSizeOverride;
@override
double get physicalDepth => double.maxFinite;
/// Handles the browser history integration to allow users to use the back
/// button, etc.
final BrowserHistory _browserHistory = BrowserHistory();
/// Simulates clicking the browser's back button.
Future<void> webOnlyBack() => _browserHistory.back();
/// Lazily initialized when the `defaultRouteName` getter is invoked.
///
/// The reason for the lazy initialization is to give enough time for the app to set [locationStrategy]
/// in `lib/src/ui/initialization.dart`.
String _defaultRouteName;
@override
String get defaultRouteName => _defaultRouteName ??= _browserHistory.currentPath;
/// Change the strategy to use for handling browser history location.
/// Setting this member will automatically update [_browserHistory].
///
/// By setting this to null, the browser history will be disabled.
set locationStrategy(LocationStrategy strategy) {
_browserHistory.locationStrategy = strategy;
}
@override
ui.VoidCallback get onTextScaleFactorChanged => _onTextScaleFactorChanged;
ui.VoidCallback _onTextScaleFactorChanged;
Zone _onTextScaleFactorChangedZone;
@override
set onTextScaleFactorChanged(ui.VoidCallback callback) {
_onTextScaleFactorChanged = callback;
_onTextScaleFactorChangedZone = Zone.current;
}
/// Engine code should use this method instead of the callback directly.
/// Otherwise zones won't work properly.
void invokeOnTextScaleFactorChanged() {
_invoke(_onTextScaleFactorChanged, _onTextScaleFactorChangedZone);
}
@override
ui.VoidCallback get onPlatformBrightnessChanged => _onPlatformBrightnessChanged;
ui.VoidCallback _onPlatformBrightnessChanged;
Zone _onPlatformBrightnessChangedZone;
@override
set onPlatformBrightnessChanged(ui.VoidCallback callback) {
_onPlatformBrightnessChanged = callback;
_onPlatformBrightnessChangedZone = Zone.current;
}
/// Engine code should use this method instead of the callback directly.
/// Otherwise zones won't work properly.
void invokeOnPlatformBrightnessChanged() {
_invoke(_onPlatformBrightnessChanged, _onPlatformBrightnessChangedZone);
}
@override
ui.VoidCallback get onMetricsChanged => _onMetricsChanged;
ui.VoidCallback _onMetricsChanged;
Zone _onMetricsChangedZone;
@override
set onMetricsChanged(ui.VoidCallback callback) {
_onMetricsChanged = callback;
_onMetricsChangedZone = Zone.current;
}
/// Engine code should use this method instead of the callback directly.
/// Otherwise zones won't work properly.
void invokeOnMetricsChanged() {
if (window._onMetricsChanged != null) {
_invoke(_onMetricsChanged, _onMetricsChangedZone);
}
}
@override
ui.VoidCallback get onLocaleChanged => _onLocaleChanged;
ui.VoidCallback _onLocaleChanged;
Zone _onLocaleChangedZone;
@override
set onLocaleChanged(ui.VoidCallback callback) {
_onLocaleChanged = callback;
_onLocaleChangedZone = Zone.current;
}
/// Engine code should use this method instead of the callback directly.
/// Otherwise zones won't work properly.
void invokeOnLocaleChanged() {
_invoke(_onLocaleChanged, _onLocaleChangedZone);
}
@override
ui.FrameCallback get onBeginFrame => _onBeginFrame;
ui.FrameCallback _onBeginFrame;
Zone _onBeginFrameZone;
@override
set onBeginFrame(ui.FrameCallback callback) {
_onBeginFrame = callback;
_onBeginFrameZone = Zone.current;
}
/// Engine code should use this method instead of the callback directly.
/// Otherwise zones won't work properly.
void invokeOnBeginFrame(Duration duration) {
_invoke1<Duration>(_onBeginFrame, _onBeginFrameZone, duration);
}
@override
ui.TimingsCallback get onReportTimings => _onReportTimings;
ui.TimingsCallback _onReportTimings;
Zone _onReportTimingsZone;
@override
set onReportTimings(ui.TimingsCallback callback) {
_onReportTimings = callback;
_onReportTimingsZone = Zone.current;
}
/// Engine code should use this method instead of the callback directly.
/// Otherwise zones won't work properly.
void invokeOnReportTimings(List<ui.FrameTiming> timings) {
_invoke1<List<ui.FrameTiming>>(_onReportTimings, _onReportTimingsZone, timings);
}
@override
ui.VoidCallback get onDrawFrame => _onDrawFrame;
ui.VoidCallback _onDrawFrame;
Zone _onDrawFrameZone;
@override
set onDrawFrame(ui.VoidCallback callback) {
_onDrawFrame = callback;
_onDrawFrameZone = Zone.current;
}
/// Engine code should use this method instead of the callback directly.
/// Otherwise zones won't work properly.
void invokeOnDrawFrame() {
_invoke(_onDrawFrame, _onDrawFrameZone);
}
@override
ui.PointerDataPacketCallback get onPointerDataPacket => _onPointerDataPacket;
ui.PointerDataPacketCallback _onPointerDataPacket;
Zone _onPointerDataPacketZone;
@override
set onPointerDataPacket(ui.PointerDataPacketCallback callback) {
_onPointerDataPacket = callback;
_onPointerDataPacketZone = Zone.current;
}
/// Engine code should use this method instead of the callback directly.
/// Otherwise zones won't work properly.
void invokeOnPointerDataPacket(ui.PointerDataPacket packet) {
_invoke1<ui.PointerDataPacket>(_onPointerDataPacket, _onPointerDataPacketZone, packet);
}
@override
ui.VoidCallback get onSemanticsEnabledChanged => _onSemanticsEnabledChanged;
ui.VoidCallback _onSemanticsEnabledChanged;
Zone _onSemanticsEnabledChangedZone;
@override
set onSemanticsEnabledChanged(ui.VoidCallback callback) {
_onSemanticsEnabledChanged = callback;
_onSemanticsEnabledChangedZone = Zone.current;
}
/// Engine code should use this method instead of the callback directly.
/// Otherwise zones won't work properly.
void invokeOnSemanticsEnabledChanged() {
_invoke(_onSemanticsEnabledChanged, _onSemanticsEnabledChangedZone);
}
@override
ui.SemanticsActionCallback get onSemanticsAction => _onSemanticsAction;
ui.SemanticsActionCallback _onSemanticsAction;
Zone _onSemanticsActionZone;
@override
set onSemanticsAction(ui.SemanticsActionCallback callback) {
_onSemanticsAction = callback;
_onSemanticsActionZone = Zone.current;
}
/// Engine code should use this method instead of the callback directly.
/// Otherwise zones won't work properly.
void invokeOnSemanticsAction(int id, ui.SemanticsAction action, ByteData args) {
_invoke3<int, ui.SemanticsAction, ByteData>(_onSemanticsAction,
_onSemanticsActionZone, id, action, args);
}
@override
ui.VoidCallback get onAccessibilityFeaturesChanged => _onAccessibilityFeaturesChanged;
ui.VoidCallback _onAccessibilityFeaturesChanged;
Zone _onAccessibilityFeaturesChangedZone;
@override
set onAccessibilityFeaturesChanged(ui.VoidCallback callback) {
_onAccessibilityFeaturesChanged = callback;
_onAccessibilityFeaturesChangedZone = Zone.current;
}
/// Engine code should use this method instead of the callback directly.
/// Otherwise zones won't work properly.
void invokeOnAccessibilityFeaturesChanged() {
_invoke(_onAccessibilityFeaturesChanged, _onAccessibilityFeaturesChangedZone);
}
@override
ui.PlatformMessageCallback get onPlatformMessage => _onPlatformMessage;
ui.PlatformMessageCallback _onPlatformMessage;
Zone _onPlatformMessageZone;
@override
set onPlatformMessage(ui.PlatformMessageCallback callback) {
_onPlatformMessage = callback;
_onPlatformMessageZone = Zone.current;
}
/// Engine code should use this method instead of the callback directly.
/// Otherwise zones won't work properly.
void invokeOnPlatformMessage(String name, ByteData data, ui.PlatformMessageResponseCallback callback) {
_invoke3<String, ByteData, ui.PlatformMessageResponseCallback>(
_onPlatformMessage,
_onPlatformMessageZone,
name,
data,
callback,
);
}
@override
void sendPlatformMessage(
String name,
ByteData data,
ui.PlatformMessageResponseCallback callback,
) {
_sendPlatformMessage(name, data, _zonedPlatformMessageResponseCallback(callback));
}
/// Wraps the given [callback] in another callback that ensures that the
/// original callback is called in the zone it was registered in.
static ui.PlatformMessageResponseCallback _zonedPlatformMessageResponseCallback(ui.PlatformMessageResponseCallback callback) {
if (callback == null)
return null;
// Store the zone in which the callback is being registered.
final Zone registrationZone = Zone.current;
return (ByteData data) {
registrationZone.runUnaryGuarded(callback, data);
};
}
void _sendPlatformMessage(
String name,
ByteData data,
ui.PlatformMessageResponseCallback callback,
) {
// In widget tests we want to bypass processing of platform messages.
if (assertionsEnabled && ui.debugEmulateFlutterTesterEnvironment) {
return;
}
if (_debugPrintPlatformMessages) {
print('Sent platform message on channel: "$name"');
}
if (assertionsEnabled && name == 'flutter/debug-echo') {
// Echoes back the data unchanged. Used for testing purpopses.
_replyToPlatformMessage(callback, data);
return;
}
switch (name) {
case 'flutter/assets':
assert(ui.webOnlyAssetManager != null);
final String url = utf8.decode(data.buffer.asUint8List());
ui.webOnlyAssetManager.load(url).then((ByteData assetData) {
_replyToPlatformMessage(callback, assetData);
}, onError: (dynamic error) {
html.window.console
.warn('Error while trying to load an asset: $error');
_replyToPlatformMessage(callback, null);
});
return;
case 'flutter/platform':
const MethodCodec codec = JSONMethodCodec();
final MethodCall decoded = codec.decodeMethodCall(data);
switch (decoded.method) {
case 'SystemNavigator.pop':
_browserHistory.exit().then((_) {
_replyToPlatformMessage(
callback, codec.encodeSuccessEnvelope(true));
});
return;
case 'HapticFeedback.vibrate':
final String type = decoded.arguments;
domRenderer.vibrate(_getHapticFeedbackDuration(type));
_replyToPlatformMessage(callback, codec.encodeSuccessEnvelope(true));
return;
case 'SystemChrome.setApplicationSwitcherDescription':
final Map<String, dynamic> arguments = decoded.arguments;
domRenderer.setTitle(arguments['label']);
domRenderer.setThemeColor(ui.Color(arguments['primaryColor']));
_replyToPlatformMessage(callback, codec.encodeSuccessEnvelope(true));
return;
case 'SystemChrome.setPreferredOrientations':
final List<dynamic> arguments = decoded.arguments;
domRenderer.setPreferredOrientation(arguments).then((bool success) {
_replyToPlatformMessage(callback,
codec.encodeSuccessEnvelope(success));
});
return;
case 'SystemSound.play':
// There are no default system sounds on web.
_replyToPlatformMessage(callback, codec.encodeSuccessEnvelope(true));
return;
case 'Clipboard.setData':
ClipboardMessageHandler().setDataMethodCall(decoded, callback);
return;
case 'Clipboard.getData':
ClipboardMessageHandler().getDataMethodCall(callback);
return;
}
break;
case 'flutter/textinput':
textEditing.channel.handleTextInput(data, callback);
return;
case 'flutter/web_test_e2e':
const MethodCodec codec = JSONMethodCodec();
_replyToPlatformMessage(callback, codec.encodeSuccessEnvelope(
_handleWebTestEnd2EndMessage(codec, data)
));
return;
case 'flutter/platform_views':
if (experimentalUseSkia) {
rasterizer.viewEmbedder.handlePlatformViewCall(data, callback);
} else {
handlePlatformViewCall(data, callback);
}
return;
case 'flutter/accessibility':
// In widget tests we want to bypass processing of platform messages.
final StandardMessageCodec codec = StandardMessageCodec();
accessibilityAnnouncements.handleMessage(codec, data);
_replyToPlatformMessage(callback, codec.encodeMessage(true));
return;
case 'flutter/navigation':
const MethodCodec codec = JSONMethodCodec();
final MethodCall decoded = codec.decodeMethodCall(data);
final Map<String, dynamic> message = decoded.arguments;
switch (decoded.method) {
case 'routeUpdated':
case 'routePushed':
case 'routeReplaced':
_browserHistory.setRouteName(message['routeName']);
_replyToPlatformMessage(callback, codec.encodeSuccessEnvelope(true));
break;
case 'routePopped':
_browserHistory.setRouteName(message['previousRouteName']);
_replyToPlatformMessage(callback, codec.encodeSuccessEnvelope(true));
break;
}
// As soon as Flutter starts taking control of the app navigation, we
// should reset [_defaultRouteName] to "/" so it doesn't have any
// further effect after this point.
_defaultRouteName = '/';
return;
}
if (pluginMessageCallHandler != null) {
pluginMessageCallHandler(name, data, callback);
return;
}
// TODO(flutter_web): Some Flutter widgets send platform messages that we
// don't handle on web. So for now, let's just ignore them. In the future,
// we should consider uncommenting the following "callback(null)" line.
// Passing [null] to [callback] indicates that the platform message isn't
// implemented. Look at [MethodChannel.invokeMethod] to see how [null] is
// handled.
// callback(null);
}
int _getHapticFeedbackDuration(String type) {
switch (type) {
case 'HapticFeedbackType.lightImpact':
return DomRenderer.vibrateLightImpact;
case 'HapticFeedbackType.mediumImpact':
return DomRenderer.vibrateMediumImpact;
case 'HapticFeedbackType.heavyImpact':
return DomRenderer.vibrateHeavyImpact;
case 'HapticFeedbackType.selectionClick':
return DomRenderer.vibrateSelectionClick;
default:
return DomRenderer.vibrateLongPress;
}
}
/// In Flutter, platform messages are exchanged between threads so the
/// messages and responses have to be exchanged asynchronously. We simulate
/// that by adding a zero-length delay to the reply.
void _replyToPlatformMessage(
ui.PlatformMessageResponseCallback callback,
ByteData data,
) {
Future<void>.delayed(Duration.zero).then((_) {
if (callback != null) {
callback(data);
}
});
}
@override
ui.Brightness get platformBrightness => _platformBrightness;
ui.Brightness _platformBrightness = ui.Brightness.light;
/// Updates [_platformBrightness] and invokes [onPlatformBrightnessChanged]
/// callback if [_platformBrightness] changed.
void _updatePlatformBrightness(ui.Brightness newPlatformBrightness) {
ui.Brightness previousPlatformBrightness = _platformBrightness;
_platformBrightness = newPlatformBrightness;
if (previousPlatformBrightness != _platformBrightness &&
onPlatformBrightnessChanged != null) {
invokeOnPlatformBrightnessChanged();
}
}
/// Reference to css media query that indicates the user theme preference on the web.
final html.MediaQueryList _brightnessMediaQuery =
html.window.matchMedia('(prefers-color-scheme: dark)');
/// A callback that is invoked whenever [_brightnessMediaQuery] changes value.
///
/// Updates the [_platformBrightness] with the new user preference.
html.EventListener _brightnessMediaQueryListener;
/// Set the callback function for listening changes in [_brightnessMediaQuery] value.
void _addBrightnessMediaQueryListener() {
_updatePlatformBrightness(_brightnessMediaQuery.matches
? ui.Brightness.dark
: ui.Brightness.light);
_brightnessMediaQueryListener = (html.Event event) {
final html.MediaQueryListEvent mqEvent = event;
_updatePlatformBrightness(
mqEvent.matches ? ui.Brightness.dark : ui.Brightness.light);
};
_brightnessMediaQuery.addListener(_brightnessMediaQueryListener);
registerHotRestartListener(() {
_removeBrightnessMediaQueryListener();
});
}
/// Remove the callback function for listening changes in [_brightnessMediaQuery] value.
void _removeBrightnessMediaQueryListener() {
_brightnessMediaQuery.removeListener(_brightnessMediaQueryListener);
_brightnessMediaQueryListener = null;
}
@override
void render(ui.Scene scene) {
if (experimentalUseSkia) {
final LayerScene layerScene = scene;
rasterizer.draw(layerScene.layerTree);
} else {
final SurfaceScene surfaceScene = scene;
domRenderer.renderScene(surfaceScene.webOnlyRootElement);
}
}
@visibleForTesting
Rasterizer rasterizer = experimentalUseSkia ? Rasterizer(Surface()) : null;
}
bool _handleWebTestEnd2EndMessage(MethodCodec codec, ByteData data) {
final MethodCall decoded = codec.decodeMethodCall(data);
double ratio = double.parse(decoded.arguments);
switch(decoded.method) {
case 'setDevicePixelRatio':
window.debugOverrideDevicePixelRatio(ratio);
window.onMetricsChanged();
return true;
}
return false;
}
/// Invokes [callback] inside the given [zone].
void _invoke(void callback(), Zone zone) {
if (callback == null)
return;
assert(zone != null);
if (identical(zone, Zone.current)) {
callback();
} else {
zone.runGuarded(callback);
}
}
/// Invokes [callback] inside the given [zone] passing it [arg].
void _invoke1<A>(void callback(A a), Zone zone, A arg) {
if (callback == null)
return;
assert(zone != null);
if (identical(zone, Zone.current)) {
callback(arg);
} else {
zone.runUnaryGuarded<A>(callback, arg);
}
}
/// Invokes [callback] inside the given [zone] passing it [arg1], [arg2], and [arg3].
void _invoke3<A1, A2, A3>(void callback(A1 a1, A2 a2, A3 a3), Zone zone, A1 arg1, A2 arg2, A3 arg3) {
if (callback == null)
return;
assert(zone != null);
if (identical(zone, Zone.current)) {
callback(arg1, arg2, arg3);
} else {
zone.runGuarded(() {
callback(arg1, arg2, arg3);
});
}
}
/// 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.
final EngineWindow window = EngineWindow();
/// The Web implementation of [ui.WindowPadding].
class WindowPadding implements ui.WindowPadding {
const WindowPadding({
this.left,
this.top,
this.right,
this.bottom,
});
final double left;
final double top;
final double right;
final double bottom;
}