part of dart._engine; // 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. /// Requests that the browser schedule a frame. /// /// This may be overridden in tests, for example, to pump fake frames. ui.VoidCallback? scheduleFrameCallback; /// Signature of functions added as a listener to high contrast changes typedef HighContrastListener = void Function(bool enabled); typedef _KeyDataResponseCallback = void Function(bool handled); const StandardMethodCodec standardCodec = StandardMethodCodec(); const JSONMethodCodec jsonCodec = JSONMethodCodec(); /// Determines if high contrast is enabled using media query 'forced-colors: active' for Windows class HighContrastSupport { static HighContrastSupport instance = HighContrastSupport(); static const String _highContrastMediaQueryString = '(forced-colors: active)'; final List _listeners = []; /// Reference to css media query that indicates whether high contrast is on. final DomMediaQueryList _highContrastMediaQuery = domWindow.matchMedia( _highContrastMediaQueryString, ); late final DomEventListener _onHighContrastChangeListener = createDomEventListener( _onHighContrastChange, ); bool get isHighContrastEnabled => _highContrastMediaQuery.matches; /// Adds function to the list of listeners on high contrast changes void addListener(HighContrastListener listener) { if (_listeners.isEmpty) { _highContrastMediaQuery.addListener(_onHighContrastChangeListener); } _listeners.add(listener); } /// Removes function from the list of listeners on high contrast changes void removeListener(HighContrastListener listener) { _listeners.remove(listener); if (_listeners.isEmpty) { _highContrastMediaQuery.removeListener(_onHighContrastChangeListener); } } JSVoid _onHighContrastChange(DomEvent event) { final DomMediaQueryListEvent mqEvent = event as DomMediaQueryListEvent; final bool isHighContrastEnabled = mqEvent.matches!; for (final HighContrastListener listener in _listeners) { listener(isHighContrastEnabled); } } } /// 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(); HighContrastSupport.instance.addListener(_updateHighContrast); _addFontSizeObserver(); _addLocaleChangedListener(); registerHotRestartListener(dispose); _appLifecycleState.addListener(_setAppLifecycleState); _viewFocusBinding.init(); domDocument.body?.prepend(accessibilityPlaceholder); _onViewDisposedListener = viewManager.onViewDisposed.listen((_) { // Send a metrics changed event to the framework when a view is disposed. // View creation/resize is handled by the `_didResize` handler in the // EngineFlutterView itself. invokeOnMetricsChanged(); }); } late StreamSubscription _onViewDisposedListener; /// The [EnginePlatformDispatcher] singleton. static EnginePlatformDispatcher get instance => _instance; static final EnginePlatformDispatcher _instance = EnginePlatformDispatcher(); final DomElement accessibilityPlaceholder = EngineSemantics.instance.semanticsHelper.prepareAccessibilityPlaceholder(); PlatformConfiguration configuration = PlatformConfiguration( locales: parseBrowserLanguages(), textScaleFactor: findBrowserTextScaleFactor(), accessibilityFeatures: computeAccessibilityFeatures(), ); /// Compute accessibility features based on the current value of high contrast flag static EngineAccessibilityFeatures computeAccessibilityFeatures() { final EngineAccessibilityFeaturesBuilder builder = EngineAccessibilityFeaturesBuilder(0); if (HighContrastSupport.instance.isHighContrastEnabled) { builder.highContrast = true; } return builder.build(); } void dispose() { _removeBrightnessMediaQueryListener(); _disconnectFontSizeObserver(); _removeLocaleChangedListener(); HighContrastSupport.instance.removeListener(_updateHighContrast); _appLifecycleState.removeListener(_setAppLifecycleState); _viewFocusBinding.dispose(); accessibilityPlaceholder.remove(); _onViewDisposedListener.cancel(); viewManager.dispose(); } /// 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); } @override Iterable displays = [EngineFlutterDisplay.instance]; late final FlutterViewManager viewManager = FlutterViewManager(this); late final AppLifecycleState _appLifecycleState = AppLifecycleState.create(viewManager); /// The current list of windows. @override Iterable get views => viewManager.views; /// Returns the [EngineFlutterView] with the provided ID if one exists, or null /// otherwise. @override EngineFlutterView? view({required int id}) => viewManager[id]; /// The [FlutterView] provided by the engine if the platform is unable to /// create windows, or, for backwards compatibility. /// /// If the platform provides an implicit view, it can be used to bootstrap /// the framework. This is common for platforms designed for single-view /// applications like mobile devices with a single display. /// /// Applications and libraries must not rely on this property being set /// as it may be null depending on the engine's configuration. Instead, /// consider using [View.of] to lookup the [FlutterView] the current /// [BuildContext] is drawing into. /// /// While the properties on the referenced [FlutterView] may change, /// the reference itself is guaranteed to never change over the lifetime /// of the application: if this property is null at startup, it will remain /// so throughout the entire lifetime of the application. If it points to a /// specific [FlutterView], it will continue to point to the same view until /// the application is shut down (although the engine may replace or remove /// the underlying backing surface of the view at its discretion). /// /// See also: /// /// * [View.of], for accessing the current view. /// * [PlatformDisptacher.views] for a list of all [FlutterView]s provided /// by the platform. @override EngineFlutterWindow? get implicitView => viewManager[kImplicitViewId] as EngineFlutterWindow?; /// 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); } } late final ViewFocusBinding _viewFocusBinding = ViewFocusBinding( viewManager, invokeOnViewFocusChange, ); @override ui.ViewFocusChangeCallback? get onViewFocusChange => _onViewFocusChange; ui.ViewFocusChangeCallback? _onViewFocusChange; Zone? _onViewFocusChangeZone; @override set onViewFocusChange(ui.ViewFocusChangeCallback? callback) { _onViewFocusChange = callback; _onViewFocusChangeZone = Zone.current; } // Engine code should use this method instead of the callback directly. // Otherwise zones won't work properly. void invokeOnViewFocusChange(ui.ViewFocusEvent viewFocusEvent) { invoke1(_onViewFocusChange, _onViewFocusChangeZone, viewFocusEvent); } @override void requestViewFocusChange({ required int viewId, required ui.ViewFocusState state, required ui.ViewFocusDirection direction, }) { _viewFocusBinding.changeViewFocus(viewId, state); } /// A set of views which have rendered in the current `onBeginFrame` or /// `onDrawFrame` scope. Set? _viewsRenderedInCurrentFrame; /// 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) { _viewsRenderedInCurrentFrame = {}; invoke1(_onBeginFrame, _onBeginFrameZone, duration); _viewsRenderedInCurrentFrame = null; } /// 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() { _viewsRenderedInCurrentFrame = {}; invoke(_onDrawFrame, _onDrawFrameZone); _viewsRenderedInCurrentFrame = null; } /// 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(_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 timings) { invoke1>(_onReportTimings, _onReportTimingsZone, timings); } @override void sendPlatformMessage( String name, ByteData? data, ui.PlatformMessageResponseCallback? callback, ) { _sendPlatformMessage(name, data, _zonedPlatformMessageResponseCallback(callback)); } @override void sendPortPlatformMessage(String name, ByteData? data, int identifier, Object port) { throw Exception("Isolates aren't supported in web."); } @override void registerBackgroundIsolate(ui.RootIsolateToken token) { throw Exception("Isolates aren't supported in web."); } // 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( _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); }; } void _sendPlatformMessage( String name, ByteData? data, ui.PlatformMessageResponseCallback? callback, ) { // In widget tests we want to bypass processing of platform messages. bool returnImmediately = false; assert(() { if (ui_web.debugEmulateFlutterTesterEnvironment) { returnImmediately = true; } return true; }()); if (returnImmediately) { return; } if (debugPrintPlatformMessages) { print('Sent platform message on channel: "$name"'); } bool allowDebugEcho = false; assert(() { allowDebugEcho = true; return true; }()); if (allowDebugEcho && 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': final MethodCall decoded = jsonCodec.decodeMethodCall(data); switch (decoded.method) { case 'Skia.setResourceCacheMaxBytes': if (renderer is CanvasKitRenderer) { assert( decoded.arguments is int, 'Argument to Skia.setResourceCacheMaxBytes must be an int, but was ${(decoded.arguments as Object?).runtimeType}', ); final int cacheSizeInBytes = decoded.arguments as int; CanvasKitRenderer.instance.resourceCacheMaxBytes = cacheSizeInBytes; } // Also respond in HTML mode. Otherwise, apps would have to detect // CanvasKit vs HTML before invoking this method. replyToPlatformMessage(callback, jsonCodec.encodeSuccessEnvelope([true])); } return; case 'flutter/assets': final String url = utf8.decode(data!.buffer.asUint8List()); _handleFlutterAssetsMessage(url, callback); return; case 'flutter/platform': final MethodCall decoded = jsonCodec.decodeMethodCall(data); switch (decoded.method) { case 'SystemNavigator.pop': // TODO(a-wallen): As multi-window support expands, the pop call // will need to include the view ID. Right now only one view is // supported. // // TODO(mdebbar): What should we do in multi-view mode? // https://github.com/flutter/flutter/issues/139174 if (implicitView != null) { implicitView!.browserHistory.exit().then((_) { replyToPlatformMessage(callback, jsonCodec.encodeSuccessEnvelope(true)); }); } else { replyToPlatformMessage(callback, jsonCodec.encodeSuccessEnvelope(true)); } return; case 'HapticFeedback.vibrate': final String? type = decoded.arguments as String?; vibrate(_getHapticFeedbackDuration(type)); replyToPlatformMessage(callback, jsonCodec.encodeSuccessEnvelope(true)); return; case 'SystemChrome.setApplicationSwitcherDescription': final Map arguments = decoded.arguments as Map; final String label = arguments['label'] as String? ?? ''; // TODO(web): Stop setting the color from here, https://github.com/flutter/flutter/issues/123365 final int primaryColor = arguments['primaryColor'] as int? ?? 0xFF000000; domDocument.title = label; setThemeColor(ui.Color(primaryColor)); replyToPlatformMessage(callback, jsonCodec.encodeSuccessEnvelope(true)); return; case 'SystemChrome.setSystemUIOverlayStyle': final Map arguments = decoded.arguments as Map; final int? statusBarColor = arguments['statusBarColor'] as int?; setThemeColor(statusBarColor == null ? null : ui.Color(statusBarColor)); replyToPlatformMessage(callback, jsonCodec.encodeSuccessEnvelope(true)); return; case 'SystemChrome.setPreferredOrientations': final List arguments = decoded.arguments as List; ScreenOrientation.instance.setPreferredOrientation(arguments).then((bool success) { replyToPlatformMessage(callback, jsonCodec.encodeSuccessEnvelope(success)); }); return; case 'SystemSound.play': // There are no default system sounds on web. replyToPlatformMessage(callback, jsonCodec.encodeSuccessEnvelope(true)); return; case 'Clipboard.setData': ClipboardMessageHandler().setDataMethodCall(decoded, callback); return; case 'Clipboard.getData': ClipboardMessageHandler().getDataMethodCall(callback); return; case 'Clipboard.hasStrings': ClipboardMessageHandler().hasStringsMethodCall(callback); return; } // Dispatched by the bindings to delay service worker initialization. case 'flutter/service_worker': domWindow.dispatchEvent(createDomEvent('Event', 'flutter-first-frame')); return; case 'flutter/textinput': textEditing.channel.handleTextInput(data, callback); return; case 'flutter/contextmenu': final MethodCall decoded = jsonCodec.decodeMethodCall(data); switch (decoded.method) { case 'enableContextMenu': implicitView!.contextMenu.enable(); replyToPlatformMessage(callback, jsonCodec.encodeSuccessEnvelope(true)); return; case 'disableContextMenu': implicitView!.contextMenu.disable(); replyToPlatformMessage(callback, jsonCodec.encodeSuccessEnvelope(true)); return; } return; case 'flutter/mousecursor': final MethodCall decoded = standardCodec.decodeMethodCall(data); final Map arguments = decoded.arguments as Map; switch (decoded.method) { case 'activateSystemCursor': // TODO(mdebbar): Once the framework starts sending us a viewId, we // should use it to grab the correct view. // https://github.com/flutter/flutter/issues/140226 views.firstOrNull?.mouseCursor.activateSystemCursor(arguments.tryString('kind')); } return; case 'flutter/web_test_e2e': replyToPlatformMessage( callback, jsonCodec.encodeSuccessEnvelope(_handleWebTestEnd2EndMessage(jsonCodec, data)), ); return; case PlatformViewMessageHandler.channelName: // `arguments` can be a Map for `create`, // but an `int` for `dispose`, hence why `dynamic` everywhere. final MethodCall(:String method, :dynamic arguments) = standardCodec.decodeMethodCall(data); PlatformViewMessageHandler.instance.handlePlatformViewCall(method, arguments, callback!); return; case 'flutter/accessibility': // In widget tests we want to bypass processing of platform messages. const StandardMessageCodec codec = StandardMessageCodec(); final EngineSemantics semantics = EngineSemantics.instance; if (semantics.semanticsEnabled) { semantics.accessibilityAnnouncements.handleMessage(codec, data); } replyToPlatformMessage(callback, codec.encodeMessage(true)); return; case 'flutter/navigation': // TODO(a-wallen): As multi-window support expands, the navigation call // will need to include the view ID. Right now only one view is // supported. // // TODO(mdebbar): What should we do in multi-view mode? // https://github.com/flutter/flutter/issues/139174 if (implicitView != null) { implicitView!.handleNavigationMessage(data).then((bool handled) { if (handled) { replyToPlatformMessage(callback, jsonCodec.encodeSuccessEnvelope(true)); } else { callback?.call(null); } }); } 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); } Future _handleFlutterAssetsMessage( String url, ui.PlatformMessageResponseCallback? callback, ) async { try { final HttpFetchResponse response = await ui_web.assetManager.loadAsset(url) as HttpFetchResponse; final ByteBuffer assetData = await response.asByteBuffer(); replyToPlatformMessage(callback, assetData.asByteData()); } catch (error) { printWarning('Error while trying to load an asset: $error'); 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!(); } @override void scheduleWarmUpFrame({ required ui.VoidCallback beginFrame, required ui.VoidCallback drawFrame, }) { Timer.run(beginFrame); // We use timers here to ensure that microtasks flush in between. // // TODO(dkwingsmt): This logic was moved from the framework and is different // from how Web renders a regular frame, which doesn't flush microtasks // between the callbacks at all (see `initializeEngineServices`). We might // want to change this. See the to-do in `initializeEngineServices` and // https://github.com/flutter/engine/pull/50570#discussion_r1496671676 Timer.run(drawFrame); } /// 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. Future render(ui.Scene scene, [ui.FlutterView? view]) async { 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; } // 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(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, target); } } /// 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 @Deprecated(''' In a multi-view world, the platform dispatcher can no longer provide apis to update semantics since each view will host its own semantics tree. Semantics updates must be passed to an individual [FlutterView]. To update semantics, use PlatformDispatcher.instance.views to get a [FlutterView] and call `updateSemantics`. ''') void updateSemantics(ui.SemanticsUpdate update) { implicitView?.semantics.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 get locales => configuration.locales; // A subscription to the 'languagechange' event of 'window'. DomSubscription? _onLocaleChangedSubscription; /// Configures the [_onLocaleChangedSubscription]. void _addLocaleChangedListener() { if (_onLocaleChangedSubscription != null) { return; } updateLocales(); // First time, for good measure. _onLocaleChangedSubscription = DomSubscription(domWindow, 'languagechange', (DomEvent _) { // Update internal config, then propagate the changes. updateLocales(); invokeOnLocaleChanged(); }); } /// Removes the [_onLocaleChangedSubscription]. void _removeLocaleChangedListener() { _onLocaleChangedSubscription?.cancel(); _onLocaleChangedSubscription = null; } /// 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 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 []); } // Called by `_onLocaleChangedSubscription` when browser languages change. void updateLocales() { configuration = configuration.copyWith(locales: parseBrowserLanguages()); } static List parseBrowserLanguages() { // TODO(yjbanov): find a solution for IE final List? languages = domWindow.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 [_defaultLocale]; } final List locales = []; for (final String language in languages) { final List 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 element to /// recalculate [textScaleFactor]. /// /// Updates [textScaleFactor] with the new value. DomMutationObserver? _fontSizeObserver; /// Set the callback function for updating [textScaleFactor] based on /// font-size changes in the browser's element. void _addFontSizeObserver() { const String styleAttribute = 'style'; _fontSizeObserver = createDomMutationObserver(( JSArray mutations, DomMutationObserver _, ) { for (final JSAny? mutation in mutations.toDart) { final DomMutationRecord record = mutation! as DomMutationRecord; if (record.type == 'attributes' && record.attributeName == styleAttribute) { final double newTextScaleFactor = findBrowserTextScaleFactor(); _updateTextScaleFactor(newTextScaleFactor); } } }); _fontSizeObserver!.observe( domDocument.documentElement!, attributes: true, attributeFilter: [styleAttribute], ); } /// Remove the observer for font-size changes in the browser's element. void _disconnectFontSizeObserver() { _fontSizeObserver?.disconnect(); _fontSizeObserver = null; } void _setAppLifecycleState(ui.AppLifecycleState state) { invokeOnPlatformMessage( 'flutter/lifecycle', const StringCodec().encodeMessage(state.toString()), (_) {}, ); } /// 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; /// Updates [_highContrast] and invokes [onHighContrastModeChanged] /// callback if [_highContrast] changed. void _updateHighContrast(bool value) { if (configuration.accessibilityFeatures.highContrast != value) { final EngineAccessibilityFeatures original = configuration.accessibilityFeatures as EngineAccessibilityFeatures; configuration = configuration.copyWith( accessibilityFeatures: original.copyWith(highContrast: value), ); invokeOnPlatformConfigurationChanged(); } } /// Reference to css media query that indicates the user theme preference on the web. final DomMediaQueryList _brightnessMediaQuery = domWindow.matchMedia( '(prefers-color-scheme: dark)', ); /// A callback that is invoked whenever [_brightnessMediaQuery] changes value. /// /// Updates the [_platformBrightness] with the new user preference. DomEventListener? _brightnessMediaQueryListener; /// Set the callback function for listening changes in [_brightnessMediaQuery] value. void _addBrightnessMediaQueryListener() { _updatePlatformBrightness( _brightnessMediaQuery.matches ? ui.Brightness.dark : ui.Brightness.light, ); _brightnessMediaQueryListener = createDomEventListener((DomEvent event) { final DomMediaQueryListEvent mqEvent = event as DomMediaQueryListEvent; _updatePlatformBrightness(mqEvent.matches! ? ui.Brightness.dark : ui.Brightness.light); }); _brightnessMediaQuery.addListener(_brightnessMediaQueryListener); } /// 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 on a semantics node. /// /// This callback is used when the user expresses the action they wish to /// perform based on the semantics node supplied by updateSemantics. /// /// The framework invokes this callback in the same zone in which the /// callback was set. @override ui.SemanticsActionEventCallback? get onSemanticsActionEvent => _onSemanticsActionEvent; ui.SemanticsActionEventCallback? _onSemanticsActionEvent; Zone _onSemanticsActionEventZone = Zone.root; @override set onSemanticsActionEvent(ui.SemanticsActionEventCallback? callback) { _onSemanticsActionEvent = callback; _onSemanticsActionEventZone = Zone.current; } /// Engine code should use this method instead of the callback directly. /// Otherwise zones won't work properly. void invokeOnSemanticsAction(int viewId, int nodeId, ui.SemanticsAction action, ByteData? args) { invoke1( _onSemanticsActionEvent, _onSemanticsActionEventZone, ui.SemanticsActionEvent(type: action, nodeId: nodeId, viewId: viewId, arguments: args), ); } // TODO(dnfield): make this work on web. // https://github.com/flutter/flutter/issues/100277 ui.ErrorCallback? _onError; // ignore: unused_field late 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 /// [``](https://developer.android.com/guide/topics/manifest/intent-filter-element.html). /// /// ## iOS /// /// On iOS, calling /// [`FlutterViewController.setInitialRoute`](/ios-embedder/interface_flutter_view_controller.html#a7f269c2da73312f856d42611cc12a33f) /// 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 { // TODO(mdebbar): What should we do in multi-view mode? // https://github.com/flutter/flutter/issues/139174 return _defaultRouteName ??= implicitView?.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; /// 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.delayed(Duration.zero).then((_) { if (callback != null) { callback(data); } }); } @override ui.FrameData get frameData => const ui.FrameData.webOnly(); @override double scaleFontSize(double unscaledFontSize) => unscaledFontSize * textScaleFactor; } 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': EngineFlutterDisplay.instance.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(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(callback, arg); } } /// Invokes [callback] inside the given [zone] passing it [arg1] and [arg2]. void invoke2(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( 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 element. double findBrowserTextScaleFactor() { final num fontSize = parseFontSize(domDocument.documentElement!) ?? _defaultRootFontSize; return fontSize / _defaultRootFontSize; } class ViewConfiguration { const ViewConfiguration({ this.view, this.devicePixelRatio = 1.0, this.visible = false, this.viewInsets = ui.ViewPadding.zero as ViewPadding, this.viewPadding = ui.ViewPadding.zero as ViewPadding, this.systemGestureInsets = ui.ViewPadding.zero as ViewPadding, this.padding = ui.ViewPadding.zero as ViewPadding, this.gestureSettings = const ui.GestureSettings(), this.displayFeatures = const [], }); ViewConfiguration copyWith({ EngineFlutterView? view, double? devicePixelRatio, bool? visible, ViewPadding? viewInsets, ViewPadding? viewPadding, ViewPadding? systemGestureInsets, ViewPadding? padding, ui.GestureSettings? gestureSettings, List? displayFeatures, }) { return ViewConfiguration( view: view ?? this.view, devicePixelRatio: devicePixelRatio ?? this.devicePixelRatio, visible: visible ?? this.visible, viewInsets: viewInsets ?? this.viewInsets, viewPadding: viewPadding ?? this.viewPadding, systemGestureInsets: systemGestureInsets ?? this.systemGestureInsets, padding: padding ?? this.padding, gestureSettings: gestureSettings ?? this.gestureSettings, displayFeatures: displayFeatures ?? this.displayFeatures, ); } final EngineFlutterView? view; final double devicePixelRatio; final bool visible; final ViewPadding viewInsets; final ViewPadding viewPadding; final ViewPadding systemGestureInsets; final ViewPadding padding; final ui.GestureSettings gestureSettings; final List displayFeatures; @override String toString() { return '$runtimeType[view: $view]'; } } class PlatformConfiguration { const PlatformConfiguration({ this.accessibilityFeatures = const EngineAccessibilityFeatures(0), this.alwaysUse24HourFormat = false, this.semanticsEnabled = false, this.platformBrightness = ui.Brightness.light, this.textScaleFactor = 1.0, this.locales = const [], this.defaultRouteName = '/', this.systemFontFamily, }); PlatformConfiguration copyWith({ ui.AccessibilityFeatures? accessibilityFeatures, bool? alwaysUse24HourFormat, bool? semanticsEnabled, ui.Brightness? platformBrightness, double? textScaleFactor, List? locales, String? defaultRouteName, String? systemFontFamily, }) { return PlatformConfiguration( accessibilityFeatures: accessibilityFeatures ?? this.accessibilityFeatures, alwaysUse24HourFormat: alwaysUse24HourFormat ?? this.alwaysUse24HourFormat, semanticsEnabled: semanticsEnabled ?? this.semanticsEnabled, platformBrightness: platformBrightness ?? this.platformBrightness, textScaleFactor: textScaleFactor ?? this.textScaleFactor, locales: locales ?? this.locales, defaultRouteName: defaultRouteName ?? this.defaultRouteName, systemFontFamily: systemFontFamily ?? this.systemFontFamily, ); } final ui.AccessibilityFeatures accessibilityFeatures; final bool alwaysUse24HourFormat; final bool semanticsEnabled; final ui.Brightness platformBrightness; final double textScaleFactor; final List locales; final String defaultRouteName; final String? systemFontFamily; }