blob: 76a6da344387707661b96282e018199b629afbd8 [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:convert';
import 'dart:html' as html;
import 'dart:typed_data';
import 'package:meta/meta.dart';
import 'package:ui/ui.dart' as ui;
import '../engine.dart' show platformViewManager, registerHotRestartListener;
import 'canvaskit/initialization.dart';
import 'canvaskit/layer_scene_builder.dart';
import 'canvaskit/rasterizer.dart';
import 'clipboard.dart';
import 'embedder.dart';
import 'html/scene.dart';
import 'mouse_cursor.dart';
import 'platform_views/message_handler.dart';
import 'plugins.dart';
import 'profiler.dart';
import 'safe_browser_api.dart';
import 'semantics.dart';
import 'services.dart';
import 'text_editing/text_editing.dart';
import 'util.dart';
import 'window.dart';
/// Requests that the browser schedule a frame.
///
/// This may be overridden in tests, for example, to pump fake frames.
ui.VoidCallback? scheduleFrameCallback;
typedef _KeyDataResponseCallback = void Function(bool handled);
/// Platform event dispatcher.
///
/// This is the central entry point for platform messages and configuration
/// events from the platform.
class EnginePlatformDispatcher extends ui.PlatformDispatcher {
/// Private constructor, since only dart:ui is supposed to create one of
/// these.
EnginePlatformDispatcher._() {
_addBrightnessMediaQueryListener();
_addFontSizeObserver();
}
/// The [EnginePlatformDispatcher] singleton.
static EnginePlatformDispatcher get instance => _instance;
static final EnginePlatformDispatcher _instance =
EnginePlatformDispatcher._();
/// The current platform configuration.
@override
ui.PlatformConfiguration get configuration => _configuration;
ui.PlatformConfiguration _configuration = ui.PlatformConfiguration(
locales: parseBrowserLanguages(),
textScaleFactor: findBrowserTextScaleFactor(),
);
/// Receives all events related to platform configuration changes.
@override
ui.VoidCallback? get onPlatformConfigurationChanged =>
_onPlatformConfigurationChanged;
ui.VoidCallback? _onPlatformConfigurationChanged;
Zone? _onPlatformConfigurationChangedZone;
@override
set onPlatformConfigurationChanged(ui.VoidCallback? callback) {
_onPlatformConfigurationChanged = callback;
_onPlatformConfigurationChangedZone = Zone.current;
}
/// Engine code should use this method instead of the callback directly.
/// Otherwise zones won't work properly.
void invokeOnPlatformConfigurationChanged() {
invoke(
_onPlatformConfigurationChanged, _onPlatformConfigurationChangedZone);
}
/// The current list of windows,
@override
Iterable<ui.FlutterView> get views => _windows.values;
Map<Object, ui.FlutterWindow> get windows => _windows;
Map<Object, ui.FlutterWindow> _windows = <Object, ui.FlutterWindow>{};
/// A map of opaque platform window identifiers to window configurations.
///
/// This should be considered a protected member, only to be used by
/// [PlatformDispatcher] subclasses.
Map<Object, ui.ViewConfiguration> get windowConfigurations => _windowConfigurations;
Map<Object, ui.ViewConfiguration> _windowConfigurations =
<Object, ui.ViewConfiguration>{};
/// A callback that is invoked whenever the platform's [devicePixelRatio],
/// [physicalSize], [padding], [viewInsets], or [systemGestureInsets]
/// values change, for example when the device is rotated or when the
/// application is resized (e.g. when showing applications side-by-side
/// on Android).
///
/// The engine invokes this callback in the same zone in which the callback
/// was set.
///
/// The framework registers with this callback and updates the layout
/// appropriately.
///
/// See also:
///
/// * [WidgetsBindingObserver], for a mechanism at the widgets layer to
/// register for notifications when this is called.
/// * [MediaQuery.of], a simpler mechanism for the same.
@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 (_onMetricsChanged != null) {
invoke(_onMetricsChanged, _onMetricsChangedZone);
}
}
/// Returns device pixel ratio returned by browser.
static double get browserDevicePixelRatio {
final double? ratio = html.window.devicePixelRatio as double?;
// Guard against WebOS returning 0 and other browsers returning null.
return (ratio == null || ratio == 0.0) ? 1.0 : ratio;
}
/// A callback invoked when any window begins a frame.
///
/// A callback that is invoked to notify the application that it is an
/// appropriate time to provide a scene using the [SceneBuilder] API and the
/// [PlatformWindow.render] method.
/// When possible, this is driven by the hardware VSync signal of the attached
/// screen with the highest VSync rate. This is only called if
/// [PlatformWindow.scheduleFrame] has been called since the last time this
/// callback was invoked.
@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);
}
/// A callback that is invoked for each frame after [onBeginFrame] has
/// completed and after the microtask queue has been drained.
///
/// This can be used to implement a second phase of frame rendering that
/// happens after any deferred work queued by the [onBeginFrame] phase.
@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);
}
/// A callback that is invoked when pointer data is available.
///
/// The framework invokes this callback in the same zone in which the
/// callback was set.
///
/// See also:
///
/// * [GestureBinding], the Flutter framework class which manages pointer
/// events.
@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 dataPacket) {
invoke1<ui.PointerDataPacket>(
_onPointerDataPacket, _onPointerDataPacketZone, dataPacket);
}
/// A callback that is invoked when key data is available.
///
/// The framework invokes this callback in the same zone in which the
/// callback was set.
///
/// See also:
///
/// * [GestureBinding], the Flutter framework class which manages pointer
/// events.
@override
ui.KeyDataCallback? get onKeyData => _onKeyData;
ui.KeyDataCallback? _onKeyData;
Zone? _onKeyDataZone;
@override
set onKeyData(ui.KeyDataCallback? callback) {
_onKeyData = callback;
_onKeyDataZone = Zone.current;
}
/// Engine code should use this method instead of the callback directly.
/// Otherwise zones won't work properly.
void invokeOnKeyData(ui.KeyData data, _KeyDataResponseCallback callback) {
final ui.KeyDataCallback? onKeyData = _onKeyData;
if (onKeyData != null) {
invoke(
() => callback(onKeyData(data)),
_onKeyDataZone,
);
} else {
callback(false);
}
}
/// A callback that is invoked to report the [FrameTiming] of recently
/// rasterized frames.
///
/// It's preferred to use [SchedulerBinding.addTimingsCallback] than to use
/// [PlatformDispatcher.onReportTimings] directly because
/// [SchedulerBinding.addTimingsCallback] allows multiple callbacks.
///
/// This can be used to see if the application has missed frames (through
/// [FrameTiming.buildDuration] and [FrameTiming.rasterDuration]), or high
/// latencies (through [FrameTiming.totalSpan]).
///
/// Unlike [Timeline], the timing information here is available in the release
/// mode (additional to the profile and the debug mode). Hence this can be
/// used to monitor the application's performance in the wild.
///
/// {@macro dart.ui.TimingsCallback.list}
///
/// If this is null, no additional work will be done. If this is not null,
/// Flutter spends less than 0.1ms every 1 second to report the timings
/// (measured on iPhone6S). The 0.1ms is about 0.6% of 16ms (frame budget for
/// 60fps), or 0.01% CPU usage per second.
@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
void sendPlatformMessage(
String name,
ByteData? data,
ui.PlatformMessageResponseCallback? callback,
) {
_sendPlatformMessage(
name, data, _zonedPlatformMessageResponseCallback(callback));
}
// TODO(ianh): Deprecate onPlatformMessage once the framework is moved over
// to using channel buffers exclusively.
@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,
) {
if (name == ui.ChannelBuffers.kControlChannelName) {
// TODO(ianh): move this logic into ChannelBuffers once we remove onPlatformMessage
try {
ui.channelBuffers.handleMessage(data!);
} finally {
callback(null);
}
} else if (_onPlatformMessage != null) {
invoke3<String, ByteData?, ui.PlatformMessageResponseCallback>(
_onPlatformMessage,
_onPlatformMessageZone,
name,
data,
callback,
);
} else {
ui.channelBuffers.push(name, data, 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);
};
}
PlatformViewMessageHandler? _platformViewMessageHandler;
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 purposes.
replyToPlatformMessage(callback, data);
return;
}
switch (name) {
/// This should be in sync with shell/common/shell.cc
case 'flutter/skia':
const MethodCodec codec = JSONMethodCodec();
final MethodCall decoded = codec.decodeMethodCall(data);
switch (decoded.method) {
case 'Skia.setResourceCacheMaxBytes':
if (useCanvasKit) {
// If we're in CanvasKit mode, we must also have a rasterizer.
assert(rasterizer != null);
assert(
decoded.arguments is int,
'Argument to Skia.setResourceCacheMaxBytes must be an int, but was ${decoded.arguments.runtimeType}',
);
final int cacheSizeInBytes = decoded.arguments as int;
rasterizer!.setSkiaResourceCacheMaxBytes(cacheSizeInBytes);
}
// Also respond in HTML mode. Otherwise, apps would have to detect
// CanvasKit vs HTML before invoking this method.
replyToPlatformMessage(
callback, codec.encodeSuccessEnvelope(<bool>[true]));
break;
}
return;
case 'flutter/assets':
final String url = utf8.decode(data!.buffer.asUint8List());
ui.webOnlyAssetManager.load(url).then((ByteData assetData) {
replyToPlatformMessage(callback, assetData);
}, onError: (dynamic error) {
printWarning('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':
// TODO(gspencergoog): As multi-window support expands, the pop call
// will need to include the window ID. Right now only one window is
// supported.
(_windows[0]! as EngineFlutterWindow)
.browserHistory
.exit()
.then((_) {
replyToPlatformMessage(
callback, codec.encodeSuccessEnvelope(true));
});
return;
case 'HapticFeedback.vibrate':
final String? type = decoded.arguments as String?;
vibrate(_getHapticFeedbackDuration(type));
replyToPlatformMessage(callback, codec.encodeSuccessEnvelope(true));
return;
case 'SystemChrome.setApplicationSwitcherDescription':
final Map<String, dynamic> arguments = decoded.arguments as Map<String, dynamic>;
// TODO(ferhat): Find more appropriate defaults? Or noop when values are null?
final String label = arguments['label'] as String? ?? '';
final int primaryColor = arguments['primaryColor'] as int? ?? 0xFF000000;
html.document.title = label;
setThemeColor(ui.Color(primaryColor));
replyToPlatformMessage(callback, codec.encodeSuccessEnvelope(true));
return;
case 'SystemChrome.setPreferredOrientations':
final List<dynamic> arguments = decoded.arguments as List<dynamic>;
flutterViewEmbedder.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;
// Dispatched by the bindings to delay service worker initialization.
case 'flutter/service_worker':
html.window.dispatchEvent(html.Event('flutter-first-frame'));
return;
case 'flutter/textinput':
textEditing.channel.handleTextInput(data, callback);
return;
case 'flutter/mousecursor':
const MethodCodec codec = StandardMethodCodec();
final MethodCall decoded = codec.decodeMethodCall(data);
final Map<dynamic, dynamic> arguments = decoded.arguments as Map<dynamic, dynamic>;
switch (decoded.method) {
case 'activateSystemCursor':
MouseCursor.instance!.activateSystemCursor(arguments.tryString('kind'));
}
return;
case 'flutter/web_test_e2e':
const MethodCodec codec = JSONMethodCodec();
replyToPlatformMessage(
callback,
codec.encodeSuccessEnvelope(
_handleWebTestEnd2EndMessage(codec, data)));
return;
case 'flutter/platform_views':
_platformViewMessageHandler ??= PlatformViewMessageHandler(
contentManager: platformViewManager,
contentHandler: (html.Element content) {
flutterViewEmbedder.glassPaneElement!.append(content);
},
);
_platformViewMessageHandler!.handlePlatformViewCall(data, callback!);
return;
case 'flutter/accessibility':
// In widget tests we want to bypass processing of platform messages.
const StandardMessageCodec codec = StandardMessageCodec();
accessibilityAnnouncements.handleMessage(codec, data);
replyToPlatformMessage(callback, codec.encodeMessage(true));
return;
case 'flutter/navigation':
// TODO(gspencergoog): As multi-window support expands, the navigation call
// will need to include the window ID. Right now only one window is
// supported.
(_windows[0]! as EngineFlutterWindow)
.handleNavigationMessage(data)
.then((bool handled) {
if (handled) {
const MethodCodec codec = JSONMethodCodec();
replyToPlatformMessage(callback, codec.encodeSuccessEnvelope(true));
} else {
callback?.call(null);
}
});
// 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;
}
// Passing [null] to [callback] indicates that the platform message isn't
// implemented. Look at [MethodChannel.invokeMethod] to see how [null] is
// handled.
replyToPlatformMessage(callback, null);
}
int _getHapticFeedbackDuration(String? type) {
const int vibrateLongPress = 50;
const int vibrateLightImpact = 10;
const int vibrateMediumImpact = 20;
const int vibrateHeavyImpact = 30;
const int vibrateSelectionClick = 10;
switch (type) {
case 'HapticFeedbackType.lightImpact':
return vibrateLightImpact;
case 'HapticFeedbackType.mediumImpact':
return vibrateMediumImpact;
case 'HapticFeedbackType.heavyImpact':
return vibrateHeavyImpact;
case 'HapticFeedbackType.selectionClick':
return vibrateSelectionClick;
default:
return vibrateLongPress;
}
}
/// Requests that, at the next appropriate opportunity, the [onBeginFrame]
/// and [onDrawFrame] callbacks be invoked.
///
/// See also:
///
/// * [SchedulerBinding], the Flutter framework class which manages the
/// scheduling of frames.
@override
void scheduleFrame() {
if (scheduleFrameCallback == null) {
throw Exception('scheduleFrameCallback must be initialized first.');
}
scheduleFrameCallback!();
}
/// Updates the application's rendering on the GPU with the newly provided
/// [Scene]. This function must be called within the scope of the
/// [onBeginFrame] or [onDrawFrame] callbacks being invoked. If this function
/// is called a second time during a single [onBeginFrame]/[onDrawFrame]
/// callback sequence or called outside the scope of those callbacks, the call
/// will be ignored.
///
/// To record graphical operations, first create a [PictureRecorder], then
/// construct a [Canvas], passing that [PictureRecorder] to its constructor.
/// After issuing all the graphical operations, call the
/// [PictureRecorder.endRecording] function on the [PictureRecorder] to obtain
/// the final [Picture] that represents the issued graphical operations.
///
/// Next, create a [SceneBuilder], and add the [Picture] to it using
/// [SceneBuilder.addPicture]. With the [SceneBuilder.build] method you can
/// then obtain a [Scene] object, which you can display to the user via this
/// [render] function.
///
/// See also:
///
/// * [SchedulerBinding], the Flutter framework class which manages the
/// scheduling of frames.
/// * [RendererBinding], the Flutter framework class which manages layout and
/// painting.
@override
void render(ui.Scene scene, [ui.FlutterView? view]) {
if (useCanvasKit) {
// "Build finish" and "raster start" happen back-to-back because we
// render on the same thread, so there's no overhead from hopping to
// another thread.
//
// CanvasKit works differently from the HTML renderer in that in HTML
// we update the DOM in SceneBuilder.build, which is these function calls
// here are CanvasKit-only.
frameTimingsOnBuildFinish();
frameTimingsOnRasterStart();
final LayerScene layerScene = scene as LayerScene;
rasterizer!.draw(layerScene.layerTree);
} else {
final SurfaceScene surfaceScene = scene as SurfaceScene;
flutterViewEmbedder.addSceneToSceneHost(surfaceScene.webOnlyRootElement);
}
frameTimingsOnRasterFinish();
}
/// Additional accessibility features that may be enabled by the platform.
@override
ui.AccessibilityFeatures get accessibilityFeatures =>
configuration.accessibilityFeatures;
/// A callback that is invoked when the value of [accessibilityFeatures] changes.
///
/// The framework invokes this callback in the same zone in which the
/// callback was set.
@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);
}
/// Change the retained semantics data about this window.
///
/// If [semanticsEnabled] is true, the user has requested that this function
/// be called whenever the semantic content of this window changes.
///
/// In either case, this function disposes the given update, which means the
/// semantics update cannot be used further.
@override
void updateSemantics(ui.SemanticsUpdate update) {
EngineSemanticsOwner.instance.updateSemantics(update);
}
/// This is equivalent to `locales.first`, except that it will provide an
/// undefined (using the language tag "und") non-null locale if the [locales]
/// list has not been set or is empty.
///
/// We use the first locale in the [locales] list instead of the browser's
/// built-in `navigator.language` because browsers do not agree on the
/// implementation.
///
/// See also:
///
/// * https://developer.mozilla.org/en-US/docs/Web/API/NavigatorLanguage/languages,
/// which explains browser quirks in the implementation notes.
@override
ui.Locale get locale =>
locales.isEmpty ? const ui.Locale.fromSubtags() : locales.first;
/// The full system-reported supported locales of the device.
///
/// This establishes the language and formatting conventions that application
/// should, if possible, use to render their user interface.
///
/// The list is ordered in order of priority, with lower-indexed locales being
/// preferred over higher-indexed ones. The first element is the primary [locale].
///
/// The [onLocaleChanged] callback is called whenever this value changes.
///
/// See also:
///
/// * [WidgetsBindingObserver], for a mechanism at the widgets layer to
/// observe when this value changes.
@override
List<ui.Locale> get locales => configuration.locales;
/// Performs the platform-native locale resolution.
///
/// Each platform may return different results.
///
/// If the platform fails to resolve a locale, then this will return null.
///
/// This method returns synchronously and is a direct call to
/// platform specific APIs without invoking method channels.
@override
ui.Locale? computePlatformResolvedLocale(List<ui.Locale> supportedLocales) {
// TODO(garyq): Implement on web.
return null;
}
/// A callback that is invoked whenever [locale] changes value.
///
/// The framework invokes this callback in the same zone in which the
/// callback was set.
///
/// See also:
///
/// * [WidgetsBindingObserver], for a mechanism at the widgets layer to
/// observe when this callback is invoked.
@override
ui.VoidCallback? get onLocaleChanged => _onLocaleChanged;
ui.VoidCallback? _onLocaleChanged;
Zone? _onLocaleChangedZone;
@override
set onLocaleChanged(ui.VoidCallback? callback) {
_onLocaleChanged = callback;
_onLocaleChangedZone = Zone.current;
}
/// The locale used when we fail to get the list from the browser.
static const ui.Locale _defaultLocale = ui.Locale('en', 'US');
/// Sets locales to an empty list.
///
/// The empty list is not a valid value for locales. This is only used for
/// testing locale update logic.
void debugResetLocales() {
_configuration = _configuration.copyWith(locales: const <ui.Locale>[]);
}
// Called by FlutterViewEmbedder when browser languages change.
void updateLocales() {
_configuration = _configuration.copyWith(locales: parseBrowserLanguages());
}
static List<ui.Locale> parseBrowserLanguages() {
// TODO(yjbanov): find a solution for IE
final List<String>? languages = html.window.navigator.languages;
if (languages == null || languages.isEmpty) {
// To make it easier for the app code, let's not leave the locales list
// empty. This way there's fewer corner cases for apps to handle.
return const <ui.Locale>[_defaultLocale];
}
final List<ui.Locale> locales = <ui.Locale>[];
for (final String language in languages) {
final List<String> parts = language.split('-');
if (parts.length > 1) {
locales.add(ui.Locale(parts.first, parts.last));
} else {
locales.add(ui.Locale(language));
}
}
assert(locales.isNotEmpty);
return locales;
}
/// Engine code should use this method instead of the callback directly.
/// Otherwise zones won't work properly.
void invokeOnLocaleChanged() {
invoke(_onLocaleChanged, _onLocaleChangedZone);
}
/// The system-reported text scale.
///
/// This establishes the text scaling factor to use when rendering text,
/// according to the user's platform preferences.
///
/// The [onTextScaleFactorChanged] callback is called whenever this value
/// changes.
///
/// See also:
///
/// * [WidgetsBindingObserver], for a mechanism at the widgets layer to
/// observe when this value changes.
@override
double get textScaleFactor => configuration.textScaleFactor;
/// The setting indicating whether time should always be shown in the 24-hour
/// format.
///
/// This option is used by [showTimePicker].
@override
bool get alwaysUse24HourFormat => configuration.alwaysUse24HourFormat;
/// Updates [textScaleFactor] and invokes [onTextScaleFactorChanged] and
/// [onPlatformConfigurationChanged] callbacks if [textScaleFactor] changed.
void _updateTextScaleFactor(double value) {
if (configuration.textScaleFactor != value) {
_configuration = configuration.copyWith(textScaleFactor: value);
invokeOnPlatformConfigurationChanged();
invokeOnTextScaleFactorChanged();
}
}
/// Watches for font-size changes in the browser's <html> element to
/// recalculate [textScaleFactor].
///
/// Updates [textScaleFactor] with the new value.
html.MutationObserver? _fontSizeObserver;
/// Set the callback function for updating [textScaleFactor] based on
/// font-size changes in the browser's <html> element.
void _addFontSizeObserver() {
const String styleAttribute = 'style';
_fontSizeObserver = html.MutationObserver(
(List<dynamic> mutations, html.MutationObserver _) {
for (final dynamic mutation in mutations) {
final html.MutationRecord record = mutation as html.MutationRecord;
if (record.type == 'attributes' &&
record.attributeName == styleAttribute) {
final double newTextScaleFactor = findBrowserTextScaleFactor();
_updateTextScaleFactor(newTextScaleFactor);
}
}
});
_fontSizeObserver!.observe(
html.document.documentElement!,
attributes: true,
attributeFilter: <String>[styleAttribute],
);
registerHotRestartListener(() {
_disconnectFontSizeObserver();
});
}
/// Remove the observer for font-size changes in the browser's <html> element.
void _disconnectFontSizeObserver() {
_fontSizeObserver?.disconnect();
_fontSizeObserver = null;
}
/// A callback that is invoked whenever [textScaleFactor] changes value.
///
/// The framework invokes this callback in the same zone in which the
/// callback was set.
///
/// See also:
///
/// * [WidgetsBindingObserver], for a mechanism at the widgets layer to
/// observe when this callback is invoked.
@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);
}
void updateSemanticsEnabled(bool semanticsEnabled) {
if (semanticsEnabled != this.semanticsEnabled) {
_configuration = _configuration.copyWith(semanticsEnabled: semanticsEnabled);
if (_onSemanticsEnabledChanged != null) {
invokeOnSemanticsEnabledChanged();
}
}
}
/// The setting indicating the current brightness mode of the host platform.
/// If the platform has no preference, [platformBrightness] defaults to [Brightness.light].
@override
ui.Brightness get platformBrightness => configuration.platformBrightness;
/// Updates [_platformBrightness] and invokes [onPlatformBrightnessChanged]
/// callback if [_platformBrightness] changed.
void _updatePlatformBrightness(ui.Brightness value) {
if (configuration.platformBrightness != value) {
_configuration = configuration.copyWith(platformBrightness: value);
invokeOnPlatformConfigurationChanged();
invokeOnPlatformBrightnessChanged();
}
}
/// The setting indicating the current system font of the host platform.
@override
String? get systemFontFamily => configuration.systemFontFamily;
/// 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 as html.MediaQueryListEvent;
_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;
}
/// A callback that is invoked whenever [platformBrightness] changes value.
///
/// The framework invokes this callback in the same zone in which the
/// callback was set.
///
/// See also:
///
/// * [WidgetsBindingObserver], for a mechanism at the widgets layer to
/// observe when this callback is invoked.
@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);
}
/// A callback that is invoked whenever [systemFontFamily] changes value.
///
/// The framework invokes this callback in the same zone in which the
/// callback was set.
///
/// See also:
///
/// * [WidgetsBindingObserver], for a mechanism at the widgets layer to
/// observe when this callback is invoked.
@override
ui.VoidCallback? get onSystemFontFamilyChanged =>
_onSystemFontFamilyChanged;
ui.VoidCallback? _onSystemFontFamilyChanged;
Zone? _onSystemFontFamilyChangedZone;
@override
set onSystemFontFamilyChanged(ui.VoidCallback? callback) {
_onSystemFontFamilyChanged = callback;
_onSystemFontFamilyChangedZone = Zone.current;
}
/// Engine code should use this method instead of the callback directly.
/// Otherwise zones won't work properly.
void invokeOnSystemFontFamilyChanged() {
invoke(_onSystemFontFamilyChanged, _onSystemFontFamilyChangedZone);
}
/// Whether the user has requested that [updateSemantics] be called when
/// the semantic contents of window changes.
///
/// The [onSemanticsEnabledChanged] callback is called whenever this value
/// changes.
@override
bool get semanticsEnabled => configuration.semanticsEnabled;
/// A callback that is invoked when the value of [semanticsEnabled] changes.
///
/// The framework invokes this callback in the same zone in which the
/// callback was set.
@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);
}
/// A callback that is invoked whenever the user requests an action to be
/// performed.
///
/// This callback is used when the user expresses the action they wish to
/// perform based on the semantics supplied by [updateSemantics].
///
/// The framework invokes this callback in the same zone in which the
/// callback was set.
@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);
}
// TODO(dnfield): make this work on web.
// https://github.com/flutter/flutter/issues/100277
ui.ErrorCallback? _onError;
// ignore: unused_field
Zone? _onErrorZone;
@override
ui.ErrorCallback? get onError => _onError;
@override
set onError(ui.ErrorCallback? callback) {
_onError = callback;
_onErrorZone = Zone.current;
}
/// The route or path that the embedder requested when the application was
/// launched.
///
/// This will be the string "`/`" if no particular route was requested.
///
/// ## Android
///
/// On Android, calling
/// [`FlutterView.setInitialRoute`](/javadoc/io/flutter/view/FlutterView.html#setInitialRoute-java.lang.String-)
/// will set this value. The value must be set sufficiently early, i.e. before
/// the [runApp] call is executed in Dart, for this to have any effect on the
/// framework. The `createFlutterView` method in your `FlutterActivity`
/// subclass is a suitable time to set the value. The application's
/// `AndroidManifest.xml` file must also be updated to have a suitable
/// [`<intent-filter>`](https://developer.android.com/guide/topics/manifest/intent-filter-element.html).
///
/// ## iOS
///
/// On iOS, calling
/// [`FlutterViewController.setInitialRoute`](/objcdoc/Classes/FlutterViewController.html#/c:objc%28cs%29FlutterViewController%28im%29setInitialRoute:)
/// will set this value. The value must be set sufficiently early, i.e. before
/// the [runApp] call is executed in Dart, for this to have any effect on the
/// framework. The `application:didFinishLaunchingWithOptions:` method is a
/// suitable time to set this value.
///
/// See also:
///
/// * [Navigator], a widget that handles routing.
/// * [SystemChannels.navigation], which handles subsequent navigation
/// requests from the embedder.
@override
String get defaultRouteName {
return _defaultRouteName ??=
(_windows[0]! as EngineFlutterWindow).browserHistory.currentPath;
}
/// 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/initialization.dart`.
String? _defaultRouteName;
@visibleForTesting
late Rasterizer? rasterizer = useCanvasKit ? Rasterizer() : null;
/// 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.FrameData get frameData => const ui.FrameData.webOnly();
}
bool _handleWebTestEnd2EndMessage(MethodCodec codec, ByteData? data) {
final MethodCall decoded = codec.decodeMethodCall(data);
final double ratio = double.parse(decoded.arguments as String);
switch (decoded.method) {
case 'setDevicePixelRatio':
window.debugOverrideDevicePixelRatio(ratio);
EnginePlatformDispatcher.instance.onMetricsChanged!();
return true;
}
return false;
}
/// Invokes [callback] inside the given [zone].
void invoke(void Function()? 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 Function(A a)? callback, 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] and [arg2].
void invoke2<A1, A2>(
void Function(A1 a1, A2 a2)? callback, Zone? zone, A1 arg1, A2 arg2) {
if (callback == null) {
return;
}
assert(zone != null);
if (identical(zone, Zone.current)) {
callback(arg1, arg2);
} else {
zone!.runGuarded(() {
callback(arg1, arg2);
});
}
}
/// Invokes [callback] inside the given [zone] passing it [arg1], [arg2], and [arg3].
void invoke3<A1, A2, A3>(void Function(A1 a1, A2 a2, A3 a3)? callback,
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);
});
}
}
const double _defaultRootFontSize = 16.0;
/// Finds the text scale factor of the browser by looking at the computed style
/// of the browser's <html> element.
double findBrowserTextScaleFactor() {
final num fontSize = parseFontSize(html.document.documentElement!) ?? _defaultRootFontSize;
return fontSize / _defaultRootFontSize;
}