[web] Render in custom target (flutter/engine#37738)

* Introduce FullScreenApplicationDom, and wire it to meta viewport, event handlers and hot restart.

* Move internal stylesheet to HostNode from ViewEmbedder.

* Add setHostStyles and Attribute to ApplicationDom. Use it in the embedder.

* Move HotRestartCacheHandler to its own file.

* Remove Safari hack for visualViewport.

* No need to keep a ref to the viewport meta in full-screen.

* Add applicationDom.attachGlassPane and use it in the Embedder.

* Remove empty method bodies.

* Add attachResourcesHost and use it from the embedder.

* Removed some unused code.

* Some more cleanup.

* Add ResizeObserver JS interop API.

* Add the CustomElementApplicationDom and wire it to the ViewEmbedder.

* Add the DimensionsProvider classes.

* Reimplement engine.window using the DimensionsProvider.

* Delegate window metrics to engine window in html scene object.

* Wire DimensionsProvider into engine.window.

* Moved ApplicationDom into its own subdir.

* Make DimensionsProvider also an Observer. Expose onResize Stream.

* Delegate onResize and dpr from window to DimensionsObserver object.

* Remove or make most ApplicationDom methods private. Expose single initializeHost.

* Hook the new API.

* dart format

* ApplicationDom -> EmbeddingStrategy.

* Attach pointer move events to glassPaneElement

* Use offset positions for mouse events (relative to host element) rather than client (relative to viewport)

* Update TouchAdapter to understand scrolling (simulate offsetX/Y)

* Remove locale change handling from the embedding strategy.

Also, remove DomSubscription handling from the
hot_restart_cache_handler, now that it is not needed.

* Move locale handling from the embedder to the platform dispatcher

* Move some styles from host to glassPane so we are more friendly with external CSS.

* Make analyzer fixes

* Ensure DimensionsProvider is available in tests.

* Initialize the view DimensionsProvider next to where the EmbeddingStrategy is decided (more logical)

* Bring back the logic to support Firefox 83.

* Fix pointer_binding test for new anchor point in the DOM.

* Fix pointer_binding_test in Firefox.

* Add an iterable way of accessing 'rules'

From a CSSStyleSheet object.

Also add the cssText getter for a CSSRule so we can parse it later.

* Merge latest changes to host_node stylesheet.

* Add an id to the StyleSheet element that we add, so it can be selected
  later (in tests).
* Use the methods coming from browser_detection.dart to determine the
  browser runtime, instead of re-implementing them within the method.
* Merge the Edge stylesheet into the general one.
* Update tests so they can look at the CSS Rules that were added.

* Format test

* Try to use insertRule for -ms-reveal, and fallback in tests.

* Test hot_restart_cache_handler

Simplify API a little bit, make clear method private.

* Test dimensions_provider.

* Test full_page_dimensions_provider

* Test custom_element_dimensions_provider

* Test embedding_strategy. Make getDomCache util public.

* Fixes and tests for *_embedding_strategy.

* Move default text colors to our innermost style inside host_node (apply only to flt-scene-host). Remove code from the embedding strategies, and adjust tests.

* Safari expands shorthand properties in CSSOM.

Check individually for both font-family and font-size in Safari, rather
than font in the host_node_test.

* Add computeEventOffsetToTarget function, and use it.

* Address PR comments.

* Update licenses_flutter.

* Remove DomCSSRuleList class and instead use Iterable of DomCSSRule

* Make the embeddingStrategy final instead of late

* Attach mouse/pointermove events to domWindow.

* Rename DimensionsProvider.onHotRestart to .close, and slightly improve docs.

* Fix compute physicalX/Y for TalkBack events.

Extracted compute function to a helper file.

* Clarify what does (and does not) support 3D transforms in the event_position_helper file.

* Update licenses file
This commit is contained in:
David Iglesias
2022-12-21 17:15:06 -08:00
committed by GitHub
parent 36e1b7fbb9
commit 0b4da98cf6
26 changed files with 1950 additions and 490 deletions

View File

@@ -1962,6 +1962,7 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/platform_views/message_handle
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/platform_views/slots.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/plugins.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/pointer_binding.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/pointer_binding/event_position_helper.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/pointer_converter.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/profiler.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/raw_keyboard.dart + ../../../flutter/LICENSE
@@ -2017,6 +2018,13 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/ulps.dart + ../../../flutter/
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/util.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/validators.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/vector_math.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/dimensions_provider.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/full_page_dimensions_provider.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/embedding_strategy/custom_element_embedding_strategy.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/embedding_strategy/embedding_strategy.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/embedding_strategy/full_page_embedding_strategy.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/hot_restart_cache_handler.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/window.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/text.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/tile_mode.dart + ../../../flutter/LICENSE
@@ -4413,6 +4421,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/platform_views/message_handler.
FILE: ../../../flutter/lib/web_ui/lib/src/engine/platform_views/slots.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/plugins.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/pointer_binding.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/pointer_binding/event_position_helper.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/pointer_converter.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/profiler.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/raw_keyboard.dart
@@ -4468,6 +4477,13 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/ulps.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/util.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/validators.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/vector_math.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/dimensions_provider.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/full_page_dimensions_provider.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/embedding_strategy/custom_element_embedding_strategy.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/embedding_strategy/embedding_strategy.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/embedding_strategy/full_page_embedding_strategy.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/view_embedder/hot_restart_cache_handler.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/window.dart
FILE: ../../../flutter/lib/web_ui/lib/text.dart
FILE: ../../../flutter/lib/web_ui/lib/tile_mode.dart

View File

@@ -122,6 +122,7 @@ export 'engine/platform_views/message_handler.dart';
export 'engine/platform_views/slots.dart';
export 'engine/plugins.dart';
export 'engine/pointer_binding.dart';
export 'engine/pointer_binding/event_position_helper.dart';
export 'engine/pointer_converter.dart';
export 'engine/profiler.dart';
export 'engine/raw_keyboard.dart';
@@ -170,4 +171,11 @@ export 'engine/text_editing/text_editing.dart';
export 'engine/util.dart';
export 'engine/validators.dart';
export 'engine/vector_math.dart';
export 'engine/view_embedder/dimensions_provider/custom_element_dimensions_provider.dart';
export 'engine/view_embedder/dimensions_provider/dimensions_provider.dart';
export 'engine/view_embedder/dimensions_provider/full_page_dimensions_provider.dart';
export 'engine/view_embedder/embedding_strategy/custom_element_embedding_strategy.dart';
export 'engine/view_embedder/embedding_strategy/embedding_strategy.dart';
export 'engine/view_embedder/embedding_strategy/full_page_embedding_strategy.dart';
export 'engine/view_embedder/hot_restart_cache_handler.dart';
export 'engine/window.dart';

View File

@@ -174,6 +174,7 @@ class DomEvent {}
extension DomEventExtension on DomEvent {
external DomEventTarget? get target;
external DomEventTarget? get currentTarget;
external double? get timeStamp;
external String get type;
external void preventDefault();
@@ -461,6 +462,9 @@ class DomHTMLElement extends DomElement {}
extension DomHTMLElementExtension on DomHTMLElement {
external double get offsetWidth;
external double get offsetLeft;
external double get offsetTop;
external DomHTMLElement? get offsetParent;
}
@JS()
@@ -1089,6 +1093,8 @@ extension DomMouseEventExtension on DomMouseEvent {
external double get clientY;
external double get offsetX;
external double get offsetY;
external double get pageX;
external double get pageY;
DomPoint get client => DomPoint(clientX, clientY);
DomPoint get offset => DomPoint(offsetX, offsetY);
external double get button;
@@ -1312,7 +1318,10 @@ class DomStyleSheet {}
class DomCSSStyleSheet extends DomStyleSheet {}
extension DomCSSStyleSheetExtension on DomCSSStyleSheet {
external DomCSSRuleList get cssRules;
Iterable<DomCSSRule> get cssRules =>
createDomListWrapper<DomCSSRule>(js_util
.getProperty<_DomList>(this, 'cssRules'));
double insertRule(String rule, [int? index]) => js_util
.callMethod<double>(
this, 'insertRule',
@@ -1323,6 +1332,12 @@ extension DomCSSStyleSheetExtension on DomCSSStyleSheet {
@staticInterop
class DomCSSRule {}
@JS()
@staticInterop
extension DomCSSRuleExtension on DomCSSRule {
external String get cssText;
}
@JS()
@staticInterop
class DomScreen {}
@@ -1420,12 +1435,75 @@ extension DomMessageChannelExtension on DomMessageChannel {
external DomMessagePort get port2;
}
/// ResizeObserver JS binding.
///
/// See: https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver
@JS()
@staticInterop
class DomCSSRuleList {}
abstract class DomResizeObserver {}
extension DomCSSRuleListExtension on DomCSSRuleList {
external double get length;
/// Creates a DomResizeObserver with a callback.
///
/// Internally converts the `List<dynamic>` of entries into the expected
/// `List<DomResizeObserverEntry>`
DomResizeObserver? createDomResizeObserver(DomResizeObserverCallbackFn fn) {
return domCallConstructorString('ResizeObserver', <Object?>[
allowInterop(
(List<dynamic> entries, DomResizeObserver observer) {
fn(entries.cast<DomResizeObserverEntry>(), observer);
}
),
]) as DomResizeObserver?;
}
/// ResizeObserver instance methods.
///
/// See: https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver#instance_methods
extension DomResizeObserverExtension on DomResizeObserver {
external void disconnect();
external void observe(DomElement target, [DomResizeObserverObserveOptions options]);
external void unobserve(DomElement target);
}
/// Options object passed to the `observe` method of a [DomResizeObserver].
///
/// See: https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver/observe#parameters
@JS()
@staticInterop
@anonymous
abstract class DomResizeObserverObserveOptions {
external factory DomResizeObserverObserveOptions({
String box,
});
}
/// Type of the function used to create a Resize Observer.
typedef DomResizeObserverCallbackFn = void Function(List<DomResizeObserverEntry> entries, DomResizeObserver observer);
/// The object passed to the [DomResizeObserverCallbackFn], which allows access to the new dimensions of the observed element.
///
/// See: https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry
@JS()
@staticInterop
abstract class DomResizeObserverEntry {}
/// ResizeObserverEntry instance properties.
///
/// See: https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry#instance_properties
extension DomResizeObserverEntryExtension on DomResizeObserverEntry {
/// A DOMRectReadOnly object containing the new size of the observed element when the callback is run.
///
/// Note that this is better supported than the above two properties, but it
/// is left over from an earlier implementation of the Resize Observer API, is
/// still included in the spec for web compat reasons, and may be deprecated
/// in future versions.
external DomRectReadOnly get contentRect;
external DomElement get target;
// Some more future getters:
//
// borderBoxSize
// contentBoxSize
// devicePixelContentBoxSize
}
/// A factory to create `TrustedTypePolicy` objects.

View File

@@ -6,7 +6,7 @@ import 'dart:async';
import 'package:ui/ui.dart' as ui;
import '../engine.dart' show buildMode, registerHotRestartListener, renderer;
import '../engine.dart' show buildMode, renderer, window;
import 'browser_detection.dart';
import 'configuration.dart';
import 'dom.dart';
@@ -14,11 +14,10 @@ import 'host_node.dart';
import 'keyboard_binding.dart';
import 'platform_dispatcher.dart';
import 'pointer_binding.dart';
import 'safe_browser_api.dart';
import 'semantics.dart';
import 'text_editing/text_editing.dart';
import 'util.dart';
import 'window.dart';
import 'view_embedder/dimensions_provider/dimensions_provider.dart';
import 'view_embedder/embedding_strategy/embedding_strategy.dart';
/// Controls the placement and lifecycle of a Flutter view on the web page.
///
@@ -34,34 +33,37 @@ import 'window.dart';
/// - [sceneHostElement], the anchor that provides a stable location in the DOM
/// tree for the [sceneElement].
/// - [semanticsHostElement], hosts the ARIA-annotated semantics tree.
///
/// This class is currently a singleton, but it'll possibly need to morph to have
/// multiple instances in a multi-view scenario. (One ViewEmbedder per FlutterView).
class FlutterViewEmbedder {
FlutterViewEmbedder() {
assert(() {
_setupHotRestart();
return true;
}());
/// Creates a FlutterViewEmbedder.
///
/// The incoming [hostElement] parameter specifies the root element in the DOM
/// into which Flutter will be rendered.
///
/// The hostElement is abstracted by an [EmbeddingStrategy] instance, which has
/// different behavior depending on the `hostElement` value:
///
/// - A `null` `hostElement` will cause Flutter to take over the whole page.
/// - A non-`null` `hostElement` will render flutter inside that element.
FlutterViewEmbedder({DomElement? hostElement})
: _embeddingStrategy =
EmbeddingStrategy.create(hostElement: hostElement) {
// Configure the EngineWindow so it knows how to measure itself.
// TODO(dit): Refactor ownership according to new design, https://github.com/flutter/flutter/issues/117098
window.configureDimensionsProvider(DimensionsProvider.create(
hostElement: hostElement,
));
reset();
assert(() {
_registerHotRestartCleanUp();
return true;
}());
}
/// Abstracts all the DOM manipulations required to embed a Flutter app in an user-supplied `hostElement`.
final EmbeddingStrategy _embeddingStrategy;
// The tag name for the root view of the flutter app (glass-pane)
static const String _glassPaneTagName = 'flt-glass-pane';
/// Listens to window resize events
DomSubscription? _resizeSubscription;
/// Listens to window locale events.
DomSubscription? _localeSubscription;
/// Contains Flutter-specific CSS rules, such as default margins and
/// paddings.
DomHTMLStyleElement? _styleElement;
/// Configures the screen, such as scaling.
DomHTMLMetaElement? _viewportMeta;
static const String glassPaneTagName = 'flt-glass-pane';
/// The element that contains the [sceneElement].
///
@@ -97,50 +99,6 @@ class FlutterViewEmbedder {
DomElement? get sceneElement => _sceneElement;
DomElement? _sceneElement;
/// This is state persistent across hot restarts that indicates what
/// to clear. Delay removal of old visible state to make the
/// transition appear smooth.
static const String _staleHotRestartStore = '__flutter_state';
List<DomElement?>? _staleHotRestartState;
/// Creates a container for DOM elements that need to be cleaned up between
/// hot restarts.
///
/// If a contains already exists, reuses the existing one.
void _setupHotRestart() {
// This persists across hot restarts to clear stale DOM.
_staleHotRestartState = getJsProperty<List<DomElement?>?>(domWindow, _staleHotRestartStore);
if (_staleHotRestartState == null) {
_staleHotRestartState = <DomElement?>[];
setJsProperty(
domWindow, _staleHotRestartStore, _staleHotRestartState);
}
}
/// Registers DOM elements that need to be cleaned up before hot restarting.
///
/// [_setupHotRestart] must have been called prior to calling this method.
void _registerHotRestartCleanUp() {
registerHotRestartListener(() {
_resizeSubscription?.cancel();
_localeSubscription?.cancel();
_staleHotRestartState!.addAll(<DomElement?>[
_glassPaneElement,
_styleElement,
_viewportMeta,
]);
});
}
void _clearOnHotRestart() {
if (_staleHotRestartState!.isNotEmpty) {
for (final DomElement? element in _staleHotRestartState!) {
element?.remove();
}
_staleHotRestartState!.clear();
}
}
/// Don't unnecessarily move DOM nodes around. If a DOM node is
/// already in the right place, skip DOM mutation. This is both faster and
/// more correct, because moving DOM nodes loses internal state, such as
@@ -151,10 +109,6 @@ class FlutterViewEmbedder {
_sceneElement = sceneElement;
_sceneHostElement!.append(sceneElement!);
}
assert(() {
_clearOnHotRestart();
return true;
}());
}
/// The element that captures input events, such as pointer events.
@@ -170,8 +124,6 @@ class FlutterViewEmbedder {
HostNode? get glassPaneShadow => _glassPaneShadow;
HostNode? _glassPaneShadow;
final DomElement rootElement = domDocument.body!;
static const String defaultFontStyle = 'normal';
static const String defaultFontWeight = 'normal';
static const double defaultFontSize = 14;
@@ -180,106 +132,42 @@ class FlutterViewEmbedder {
'$defaultFontStyle $defaultFontWeight ${defaultFontSize}px $defaultFontFamily';
void reset() {
final bool isWebKit = browserEngine == BrowserEngine.webkit;
// How was the current renderer selected?
const String rendererSelection = FlutterConfiguration.flutterWebAutoDetect
? 'auto-selected'
: 'requested explicitly';
_styleElement?.remove();
_styleElement = createDomHTMLStyleElement();
_resourcesHost?.remove();
_resourcesHost = null;
domDocument.head!.append(_styleElement!);
final DomCSSStyleSheet sheet = _styleElement!.sheet! as DomCSSStyleSheet;
applyGlobalCssRulesToSheet(
sheet,
browserEngine: browserEngine,
hasAutofillOverlay: browserHasAutofillOverlay(),
// Initializes the embeddingStrategy so it can host a single-view Flutter app.
_embeddingStrategy.initialize(
hostElementAttributes: <String, String>{
'flt-renderer': '${renderer.rendererTag} ($rendererSelection)',
'flt-build-mode': buildMode,
// TODO(mdebbar): Disable spellcheck until changes in the framework and
// engine are complete.
'spellcheck': 'false',
},
);
final DomHTMLBodyElement bodyElement = domDocument.body!;
bodyElement.setAttribute(
'flt-renderer',
'${renderer.rendererTag} (${FlutterConfiguration.flutterWebAutoDetect ? 'auto-selected' : 'requested explicitly'})',
);
bodyElement.setAttribute('flt-build-mode', buildMode);
setElementStyle(bodyElement, 'position', 'fixed');
setElementStyle(bodyElement, 'top', '0');
setElementStyle(bodyElement, 'right', '0');
setElementStyle(bodyElement, 'bottom', '0');
setElementStyle(bodyElement, 'left', '0');
setElementStyle(bodyElement, 'overflow', 'hidden');
setElementStyle(bodyElement, 'padding', '0');
setElementStyle(bodyElement, 'margin', '0');
// TODO(yjbanov): fix this when KVM I/O support is added. Currently scroll
// using drag, and text selection interferes.
setElementStyle(bodyElement, 'user-select', 'none');
setElementStyle(bodyElement, '-webkit-user-select', 'none');
setElementStyle(bodyElement, '-ms-user-select', 'none');
setElementStyle(bodyElement, '-moz-user-select', 'none');
// This is required to prevent the browser from doing any native touch
// handling. If this is not done, the browser doesn't report 'pointermove'
// events properly.
setElementStyle(bodyElement, 'touch-action', 'none');
// These are intentionally outrageous font parameters to make sure that the
// apps fully specify their text styles.
setElementStyle(bodyElement, 'font', defaultCssFont);
setElementStyle(bodyElement, 'color', 'red');
// TODO(mdebbar): Disable spellcheck until changes in the framework and
// engine are complete.
bodyElement.spellcheck = false;
for (final DomElement viewportMeta
in domDocument.head!.querySelectorAll('meta[name="viewport"]')) {
if (assertionsEnabled) {
// Filter out the meta tag that the engine placed on the page. This is
// to avoid UI flicker during hot restart. Hot restart will clean up the
// old meta tag synchronously with the first post-restart frame.
if (!viewportMeta.hasAttribute('flt-viewport')) {
print(
'WARNING: found an existing <meta name="viewport"> tag. Flutter '
'Web uses its own viewport configuration for better compatibility '
'with Flutter. This tag will be replaced.',
);
}
}
viewportMeta.remove();
}
// This removes a previously created meta tag. Note, however, that this does
// not remove the meta tag during hot restart. Hot restart resets all static
// variables, so this will be null upon hot restart. Instead, this tag is
// removed by _clearOnHotRestart.
_viewportMeta?.remove();
_viewportMeta = createDomHTMLMetaElement()
..setAttribute('flt-viewport', '')
..name = 'viewport'
..content = 'width=device-width, initial-scale=1.0, '
'maximum-scale=1.0, user-scalable=no';
domDocument.head!.append(_viewportMeta!);
// IMPORTANT: the glass pane element must come after the scene element in the DOM node list so
// it can intercept input events.
_glassPaneElement?.remove();
final DomElement glassPaneElement = domDocument.createElement(_glassPaneTagName);
// Create and inject the [_glassPaneElement].
final DomElement glassPaneElement =
domDocument.createElement(glassPaneTagName);
_glassPaneElement = glassPaneElement;
glassPaneElement.style
..position = 'absolute'
..top = '0'
..right = '0'
..bottom = '0'
..left = '0';
// This must be appended to the body, so the engine can create a host node
// properly.
bodyElement.append(glassPaneElement);
// This must be attached to the DOM now, so the engine can create a host
// node (ShadowDOM or a fallback) next.
//
// The embeddingStrategy will take care of cleaning up the glassPane on hot
// restart.
_embeddingStrategy.attachGlassPane(glassPaneElement);
// Create a [HostNode] under the glass pane element, and attach everything
// there, instead of directly underneath the glass panel.
final HostNode glassPaneElementHostNode = _createHostNode(glassPaneElement);
//
// TODO(dit): clean HostNode, https://github.com/flutter/flutter/issues/116204
final HostNode glassPaneElementHostNode = HostNode.create(
glassPaneElement,
defaultCssFont,
);
_glassPaneShadow = glassPaneElementHostNode;
// Don't allow the scene to receive pointer events.
@@ -324,67 +212,20 @@ class FlutterViewEmbedder {
}
KeyboardBinding.initInstance();
PointerBinding.initInstance(glassPaneElement, KeyboardBinding.instance!.converter);
PointerBinding.initInstance(
glassPaneElement,
KeyboardBinding.instance!.converter,
);
if (domWindow.visualViewport == null && isWebKit) {
// Older Safari versions sometimes give us bogus innerWidth/innerHeight
// values when the page loads. When it changes the values to correct ones
// it does not notify of the change via `onResize`. As a workaround, we
// set up a temporary periodic timer that polls innerWidth and triggers
// the resizeListener so that the framework can react to the change.
//
// Safari 13 has implemented visualViewport API so it doesn't need this
// timer.
//
// VisualViewport API is not enabled in Firefox as well. On the other hand
// Firefox returns correct values for innerHeight, innerWidth.
// Firefox also triggers domWindow.onResize therefore this timer does
// not need to be set up for Firefox.
final int initialInnerWidth = domWindow.innerWidth!.toInt();
// Counts how many times screen size was checked. It is checked up to 5
// times.
int checkCount = 0;
Timer.periodic(const Duration(milliseconds: 100), (Timer t) {
checkCount += 1;
if (initialInnerWidth != domWindow.innerWidth) {
// Window size changed. Notify.
t.cancel();
_metricsDidChange(null);
} else if (checkCount > 5) {
// Checked enough times. Stop.
t.cancel();
}
});
}
if (domWindow.visualViewport != null) {
_resizeSubscription = DomSubscription(domWindow.visualViewport!, 'resize',
allowInterop(_metricsDidChange));
} else {
_resizeSubscription = DomSubscription(domWindow, 'resize',
allowInterop(_metricsDidChange));
}
_localeSubscription = DomSubscription(domWindow, 'languagechange',
allowInterop(_languageDidChange));
EnginePlatformDispatcher.instance.updateLocales();
}
// Creates a [HostNode] into a `root` [DomElement].
HostNode _createHostNode(DomElement root) {
if (getJsProperty<Object?>(root, 'attachShadow') != null) {
return ShadowDomHostNode(root);
} else {
// attachShadow not available, fall back to ElementHostNode.
return ElementHostNode(root);
}
window.onResize.listen(_metricsDidChange);
}
/// The framework specifies semantics in physical pixels, but CSS uses
/// logical pixels. To compensate, an inverse scale is injected at the root
/// level.
void updateSemanticsScreenProperties() {
_semanticsHostElement!.style.setProperty('transform',
'scale(${1 / domWindow.devicePixelRatio})');
_semanticsHostElement!.style
.setProperty('transform', 'scale(${1 / window.devicePixelRatio})');
}
/// Called immediately after browser window metrics change.
@@ -396,8 +237,9 @@ class FlutterViewEmbedder {
///
/// Note: always check for rotations for a mobile device. Update the physical
/// size if the change is caused by a rotation.
void _metricsDidChange(DomEvent? event) {
void _metricsDidChange(ui.Size? newSize) {
updateSemanticsScreenProperties();
// TODO(dit): Do not computePhysicalSize twice, https://github.com/flutter/flutter/issues/117036
if (isMobile && !window.isRotation() && textEditing.isEditing) {
window.computeOnScreenKeyboardInsets(true);
EnginePlatformDispatcher.instance.invokeOnMetricsChanged();
@@ -409,12 +251,6 @@ class FlutterViewEmbedder {
}
}
/// Called immediately after browser window language change.
void _languageDidChange(DomEvent event) {
EnginePlatformDispatcher.instance.updateLocales();
ui.window.onLocaleChanged?.call();
}
static const String orientationLockTypeAny = 'any';
static const String orientationLockTypeNatural = 'natural';
static const String orientationLockTypeLandscape = 'landscape';
@@ -487,17 +323,6 @@ class FlutterViewEmbedder {
}
}
/// The element corresponding to the only child of the root surface.
DomElement? get _rootApplicationElement {
final DomElement lastElement = rootElement.children.last;
for (final DomElement child in lastElement.children) {
if (child.tagName == 'FLT-SCENE') {
return child;
}
}
return null;
}
/// Add an element as a global resource to be referenced by CSS.
///
/// This call create a global resource host element on demand and either
@@ -507,15 +332,18 @@ class FlutterViewEmbedder {
void addResource(DomElement element) {
final bool isWebKit = browserEngine == BrowserEngine.webkit;
if (_resourcesHost == null) {
_resourcesHost = createDomHTMLDivElement()
final DomElement resourcesHost = domDocument
.createElement('flt-svg-filters')
..style.visibility = 'hidden';
if (isWebKit) {
final DomNode bodyNode = domDocument.body!;
bodyNode.insertBefore(_resourcesHost!, bodyNode.firstChild);
// The resourcesHost *must* be a sibling of the glassPaneElement.
_embeddingStrategy.attachResourcesHost(resourcesHost,
nextTo: glassPaneElement);
} else {
_glassPaneShadow!.node.insertBefore(
_resourcesHost!, _glassPaneShadow!.node.firstChild);
glassPaneShadow!.node
.insertBefore(resourcesHost, glassPaneShadow!.node.firstChild);
}
_resourcesHost = resourcesHost;
}
_resourcesHost!.append(element);
}
@@ -528,127 +356,6 @@ class FlutterViewEmbedder {
assert(element.parentNode == _resourcesHost);
element.remove();
}
String get currentHtml => _rootApplicationElement?.outerHTML ?? '';
}
// Applies the required global CSS to an incoming [DomCSSStyleSheet] `sheet`.
void applyGlobalCssRulesToSheet(
DomCSSStyleSheet sheet, {
required BrowserEngine browserEngine,
required bool hasAutofillOverlay,
String glassPaneTagName = FlutterViewEmbedder._glassPaneTagName,
}) {
final bool isWebKit = browserEngine == BrowserEngine.webkit;
final bool isFirefox = browserEngine == BrowserEngine.firefox;
// TODO(web): use more efficient CSS selectors; descendant selectors are slow.
// More info: https://csswizardry.com/2011/09/writing-efficient-css-selectors
if (isFirefox) {
// For firefox set line-height, otherwise textx at same font-size will
// measure differently in ruler.
//
// - See: https://github.com/flutter/flutter/issues/44803
sheet.insertRule(
'flt-paragraph, flt-span {line-height: 100%;}',
sheet.cssRules.length.toInt(),
);
}
// This undoes browser's default painting and layout attributes of range
// input, which is used in semantics.
sheet.insertRule(
'''
flt-semantics input[type=range] {
appearance: none;
-webkit-appearance: none;
width: 100%;
position: absolute;
border: none;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
''',
sheet.cssRules.length.toInt(),
);
if (isWebKit) {
sheet.insertRule(
'flt-semantics input[type=range]::-webkit-slider-thumb {'
' -webkit-appearance: none;'
'}',
sheet.cssRules.length.toInt());
}
if (isFirefox) {
sheet.insertRule(
'input::-moz-selection {'
' background-color: transparent;'
'}',
sheet.cssRules.length.toInt());
sheet.insertRule(
'textarea::-moz-selection {'
' background-color: transparent;'
'}',
sheet.cssRules.length.toInt());
} else {
// On iOS, the invisible semantic text field has a visible cursor and
// selection highlight. The following 2 CSS rules force everything to be
// transparent.
sheet.insertRule(
'input::selection {'
' background-color: transparent;'
'}',
sheet.cssRules.length.toInt());
sheet.insertRule(
'textarea::selection {'
' background-color: transparent;'
'}',
sheet.cssRules.length.toInt());
}
sheet.insertRule('''
flt-semantics input,
flt-semantics textarea,
flt-semantics [contentEditable="true"] {
caret-color: transparent;
}
''', sheet.cssRules.length.toInt());
// By default on iOS, Safari would highlight the element that's being tapped
// on using gray background. This CSS rule disables that.
if (isWebKit) {
sheet.insertRule('''
$glassPaneTagName * {
-webkit-tap-highlight-color: transparent;
}
''', sheet.cssRules.length.toInt());
}
// Hide placeholder text
sheet.insertRule(
'''
.flt-text-editing::placeholder {
opacity: 0;
}
''',
sheet.cssRules.length.toInt(),
);
// This css prevents an autofill overlay brought by the browser during
// text field autofill by delaying the transition effect.
// See: https://github.com/flutter/flutter/issues/61132.
if (browserHasAutofillOverlay()) {
sheet.insertRule('''
.transparentTextEditing:-webkit-autofill,
.transparentTextEditing:-webkit-autofill:hover,
.transparentTextEditing:-webkit-autofill:focus,
.transparentTextEditing:-webkit-autofill:active {
-webkit-transition-delay: 99999s;
}
''', sheet.cssRules.length.toInt());
}
}
/// The embedder singleton.
@@ -660,15 +367,17 @@ FlutterViewEmbedder get flutterViewEmbedder {
assert(() {
if (embedder == null) {
throw StateError(
'FlutterViewEmbedder not initialized. Call `ensureFlutterViewEmbedderInitialized()` '
'prior to calling the `flutterViewEmbedder` getter.'
);
'FlutterViewEmbedder not initialized. Call `ensureFlutterViewEmbedderInitialized()` '
'prior to calling the `flutterViewEmbedder` getter.');
}
return true;
}());
return embedder!;
}
FlutterViewEmbedder? _flutterViewEmbedder;
/// Initializes the [FlutterViewEmbedder], if it's not already initialized.
FlutterViewEmbedder ensureFlutterViewEmbedderInitialized() => _flutterViewEmbedder ??= FlutterViewEmbedder();
FlutterViewEmbedder ensureFlutterViewEmbedderInitialized() =>
_flutterViewEmbedder ??=
FlutterViewEmbedder(hostElement: configuration.hostElement);

View File

@@ -5,6 +5,7 @@
import 'browser_detection.dart';
import 'dom.dart';
import 'embedder.dart';
import 'safe_browser_api.dart';
import 'text_editing/text_editing.dart';
/// The interface required to host a flutter app in the DOM, and its tests.
@@ -13,7 +14,25 @@ import 'text_editing/text_editing.dart';
/// (preferred Flutter rendering method) and [DomDocument] (fallback).
///
/// Not to be confused with [DomDocumentOrShadowRoot].
///
/// This also handles the stylesheet that is applied to the different types of
/// HostNodes; for ShadowDOM there's not much to do, but for ElementNodes, the
/// stylesheet is "namespaced" by the `flt-glass-pane` prefix, so it "only"
/// affects things that Flutter web owns.
abstract class HostNode {
/// Returns an appropriate HostNode for the given [root].
///
/// If `attachShadow` is supported, this returns a [ShadowDomHostNode], else
/// this will fall-back to an [ElementHostNode].
factory HostNode.create(DomElement root, String defaultFont) {
if (getJsProperty<Object?>(root, 'attachShadow') != null) {
return ShadowDomHostNode(root, defaultFont);
} else {
// attachShadow not available, fall back to ElementHostNode.
return ElementHostNode(root, defaultFont);
}
}
/// Retrieves the [DomElement] that currently has focus.
///
/// See:
@@ -88,11 +107,12 @@ abstract class HostNode {
class ShadowDomHostNode implements HostNode {
/// Build a HostNode by attaching a [DomShadowRoot] to the `root` element.
///
/// This also calls [applyGlobalCssRulesToSheet], defined in dom_renderer.
ShadowDomHostNode(DomElement root) :
assert(
/// This also calls [applyGlobalCssRulesToSheet], with the [defaultFont]
/// to be used as the default font definition.
ShadowDomHostNode(DomElement root, String defaultFont)
: assert(
root.isConnected ?? true,
'The `root` of a ShadowDomHostNode must be connected to the Document object or a ShadowRoot.',
'The `root` of a ShadowDomHostNode must be connected to the Document object or a ShadowRoot.'
) {
_shadow = root.attachShadow(<String, dynamic>{
'mode': 'open',
@@ -101,29 +121,16 @@ class ShadowDomHostNode implements HostNode {
'delegatesFocus': false,
});
final DomHTMLStyleElement shadowRootStyleElement = createDomHTMLStyleElement();
final DomHTMLStyleElement shadowRootStyleElement =
createDomHTMLStyleElement();
shadowRootStyleElement.id = 'flt-internals-stylesheet';
// The shadowRootStyleElement must be appended to the DOM, or its `sheet` will be null later.
_shadow.appendChild(shadowRootStyleElement);
// TODO(dit): Apply only rules for the shadow root
applyGlobalCssRulesToSheet(
shadowRootStyleElement.sheet! as DomCSSStyleSheet,
browserEngine: browserEngine,
hasAutofillOverlay: browserHasAutofillOverlay(),
defaultCssFont: defaultFont,
);
// Removes password reveal icon for text inputs in Edge browsers.
// Style tag needs to be injected into DOM because non-Edge
// browsers will crash trying to parse -ms-reveal CSS selectors if added via
// sheet.insertRule().
// See: https://github.com/flutter/flutter/issues/83695
if (isEdge) {
final DomHTMLStyleElement edgeStyleElement = createDomHTMLStyleElement();
edgeStyleElement.id = 'ms-reveal';
edgeStyleElement.innerText = 'input::-ms-reveal {display: none;}';
_shadow.appendChild(edgeStyleElement);
}
}
late DomShadowRoot _shadow;
@@ -164,7 +171,20 @@ class ShadowDomHostNode implements HostNode {
/// being constructed.
class ElementHostNode implements HostNode {
/// Build a HostNode by attaching a child [DomElement] to the `root` element.
ElementHostNode(DomElement root) {
ElementHostNode(DomElement root, String defaultFont) {
// Append the stylesheet here, so this class is completely symmetric to the
// ShadowDOM version.
final DomHTMLStyleElement styleElement = createDomHTMLStyleElement();
styleElement.id = 'flt-internals-stylesheet';
// The styleElement must be appended to the DOM, or its `sheet` will be null later.
root.appendChild(styleElement);
applyGlobalCssRulesToSheet(
styleElement.sheet! as DomCSSStyleSheet,
hasAutofillOverlay: browserHasAutofillOverlay(),
cssSelectorPrefix: FlutterViewEmbedder.glassPaneTagName,
defaultCssFont: defaultFont,
);
_element = domDocument.createElement('flt-element-host-node');
root.appendChild(_element);
}
@@ -200,3 +220,144 @@ class ElementHostNode implements HostNode {
@override
void appendAll(Iterable<DomNode> nodes) => nodes.forEach(append);
}
// Applies the required global CSS to an incoming [DomCSSStyleSheet] `sheet`.
void applyGlobalCssRulesToSheet(
DomCSSStyleSheet sheet, {
required bool hasAutofillOverlay,
String cssSelectorPrefix = '',
required String defaultCssFont,
}) {
// TODO(web): use more efficient CSS selectors; descendant selectors are slow.
// More info: https://csswizardry.com/2011/09/writing-efficient-css-selectors
// These are intentionally outrageous font parameters to make sure that the
// apps fully specify their text styles.
//
// Fixes #115216 by ensuring that our parameters only affect the flt-scene-host children.
sheet.insertRule('''
$cssSelectorPrefix flt-scene-host {
color: red;
font: $defaultCssFont;
}
''', sheet.cssRules.length);
// By default on iOS, Safari would highlight the element that's being tapped
// on using gray background. This CSS rule disables that.
if (isSafari) {
sheet.insertRule('''
$cssSelectorPrefix * {
-webkit-tap-highlight-color: transparent;
}
''', sheet.cssRules.length);
}
if (isFirefox) {
// For firefox set line-height, otherwise text at same font-size will
// measure differently in ruler.
//
// - See: https://github.com/flutter/flutter/issues/44803
sheet.insertRule('''
$cssSelectorPrefix flt-paragraph,
$cssSelectorPrefix flt-span {
line-height: 100%;
}
''', sheet.cssRules.length);
}
// This undoes browser's default painting and layout attributes of range
// input, which is used in semantics.
sheet.insertRule('''
$cssSelectorPrefix flt-semantics input[type=range] {
appearance: none;
-webkit-appearance: none;
width: 100%;
position: absolute;
border: none;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
''', sheet.cssRules.length);
if (isSafari) {
sheet.insertRule('''
$cssSelectorPrefix flt-semantics input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none;
}
''', sheet.cssRules.length);
}
// The invisible semantic text field may have a visible cursor and selection
// highlight. The following 2 CSS rules force everything to be transparent.
sheet.insertRule('''
$cssSelectorPrefix input::selection {
background-color: transparent;
}
''', sheet.cssRules.length);
sheet.insertRule('''
$cssSelectorPrefix textarea::selection {
background-color: transparent;
}
''', sheet.cssRules.length);
sheet.insertRule('''
$cssSelectorPrefix flt-semantics input,
$cssSelectorPrefix flt-semantics textarea,
$cssSelectorPrefix flt-semantics [contentEditable="true"] {
caret-color: transparent;
}
''', sheet.cssRules.length);
// Hide placeholder text
sheet.insertRule('''
$cssSelectorPrefix .flt-text-editing::placeholder {
opacity: 0;
}
''', sheet.cssRules.length);
// This css prevents an autofill overlay brought by the browser during
// text field autofill by delaying the transition effect.
// See: https://github.com/flutter/flutter/issues/61132.
if (browserHasAutofillOverlay()) {
sheet.insertRule('''
$cssSelectorPrefix .transparentTextEditing:-webkit-autofill,
$cssSelectorPrefix .transparentTextEditing:-webkit-autofill:hover,
$cssSelectorPrefix .transparentTextEditing:-webkit-autofill:focus,
$cssSelectorPrefix .transparentTextEditing:-webkit-autofill:active {
-webkit-transition-delay: 99999s;
}
''', sheet.cssRules.length);
}
// Removes password reveal icon for text inputs in Edge browsers.
// Non-Edge browsers will crash trying to parse -ms-reveal CSS selector,
// so we guard it behind an isEdge check.
// Fixes: https://github.com/flutter/flutter/issues/83695
if (isEdge) {
// We try-catch this, because in testing, we fake Edge via the UserAgent,
// so the below will throw an exception (because only real Edge understands
// the ::-ms-reveal pseudo-selector).
try {
sheet.insertRule('''
$cssSelectorPrefix input::-ms-reveal {
display: none;
}
''', sheet.cssRules.length);
} on DomException catch (e) {
// Browsers that don't understand ::-ms-reveal throw a DOMException
// of type SyntaxError.
domWindow.console.warn(e);
// Add a fake rule if our code failed because we're under testing
assert(() {
sheet.insertRule('''
$cssSelectorPrefix input.fallback-for-fakey-browser-in-ci {
display: none;
}
''', sheet.cssRules.length);
return true;
}());
}
}
}

View File

@@ -6,6 +6,7 @@ import 'package:ui/ui.dart' as ui;
import '../dom.dart';
import '../vector_math.dart';
import '../window.dart';
import 'surface.dart';
class SurfaceScene implements ui.Scene {
@@ -45,12 +46,10 @@ class PersistedScene extends PersistedContainerSurface {
@override
void recomputeTransformAndClip() {
// The scene clip is the size of the entire window.
// TODO(yjbanov): in the add2app scenario where we might be hosted inside
// a custom element, this will be different. We will need to
// update this code when we add add2app support.
final double screenWidth = domWindow.innerWidth!;
final double screenHeight = domWindow.innerHeight!;
localClipBounds = ui.Rect.fromLTRB(0, 0, screenWidth, screenHeight);
final ui.Size screen = window.physicalSize / window.devicePixelRatio;
// Question: why is the above a logical size, rather than a physical size
// like everywhere else in the metrics?
localClipBounds = ui.Rect.fromLTRB(0, 0, screen.width, screen.height);
projectedClip = null;
}

View File

@@ -84,6 +84,7 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
_addBrightnessMediaQueryListener();
HighContrastSupport.instance.addListener(_updateHighContrast);
_addFontSizeObserver();
_addLocaleChangedListener();
registerHotRestartListener(dispose);
}
@@ -112,6 +113,7 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
void dispose() {
_removeBrightnessMediaQueryListener();
_disconnectFontSizeObserver();
_removeLocaleChangedListener();
HighContrastSupport.instance.removeListener(_updateHighContrast);
}
@@ -743,6 +745,29 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
@override
List<ui.Locale> 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', allowInterop((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.

View File

@@ -12,13 +12,17 @@ import '../engine.dart' show registerHotRestartListener;
import 'browser_detection.dart';
import 'dom.dart';
import 'platform_dispatcher.dart';
import 'pointer_binding/event_position_helper.dart';
import 'pointer_converter.dart';
import 'safe_browser_api.dart';
import 'semantics.dart';
/// Set this flag to true to see all the fired events in the console.
/// Set this flag to true to log all the browser events.
const bool _debugLogPointerEvents = false;
/// Set this to true to log all the events sent to the Flutter framework.
const bool _debugLogFlutterEvents = false;
/// The signature of a callback that handles pointer events.
typedef _PointerDataCallback = void Function(Iterable<ui.PointerData>);
@@ -147,13 +151,16 @@ class PointerBinding {
_pointerDataConverter.clearPointerState();
}
// TODO(dit): remove old API fallbacks, https://github.com/flutter/flutter/issues/116141
_BaseAdapter _createAdapter() {
if (_detector.hasPointerEvents) {
return _PointerAdapter(_onPointerData, glassPaneElement, _pointerDataConverter, _keyboardConverter);
}
// Fallback for Safari Mobile < 13. To be removed.
if (_detector.hasTouchEvents) {
return _TouchAdapter(_onPointerData, glassPaneElement, _pointerDataConverter, _keyboardConverter);
}
// Fallback for Safari Desktop < 13. To be removed.
if (_detector.hasMouseEvents) {
return _MouseAdapter(_onPointerData, glassPaneElement, _pointerDataConverter, _keyboardConverter);
}
@@ -162,6 +169,11 @@ class PointerBinding {
void _onPointerData(Iterable<ui.PointerData> data) {
final ui.PointerDataPacket packet = ui.PointerDataPacket(data: data.toList());
if (_debugLogFlutterEvents) {
for(final ui.PointerData datum in data) {
print('fw:${datum.change} ${datum.physicalX},${datum.physicalY}');
}
}
EnginePlatformDispatcher.instance.invokeOnPointerDataPacket(packet);
}
}
@@ -300,9 +312,10 @@ abstract class _BaseAdapter {
if (_debugLogPointerEvents) {
if (domInstanceOfString(event, 'PointerEvent')) {
final DomPointerEvent pointerEvent = event as DomPointerEvent;
final ui.Offset offset = computeEventOffsetToTarget(event, glassPaneElement);
print('${pointerEvent.type} '
'${pointerEvent.clientX.toStringAsFixed(1)},'
'${pointerEvent.clientY.toStringAsFixed(1)}');
'${offset.dx.toStringAsFixed(1)},'
'${offset.dy.toStringAsFixed(1)}');
} else {
print(event.type);
}
@@ -439,6 +452,7 @@ mixin _WheelEventListenerMixin on _BaseAdapter {
}
final List<ui.PointerData> data = <ui.PointerData>[];
final ui.Offset offset = computeEventOffsetToTarget(event, glassPaneElement);
_pointerDataConverter.convert(
data,
change: ui.PointerChange.hover,
@@ -446,8 +460,8 @@ mixin _WheelEventListenerMixin on _BaseAdapter {
kind: kind,
signalKind: ui.PointerSignalKind.scroll,
device: _mouseDeviceId,
physicalX: event.clientX * ui.window.devicePixelRatio,
physicalY: event.clientY * ui.window.devicePixelRatio,
physicalX: offset.dx * ui.window.devicePixelRatio,
physicalY: offset.dy * ui.window.devicePixelRatio,
buttons: event.buttons!.toInt(),
pressure: 1.0,
pressureMax: 1.0,
@@ -734,6 +748,7 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin {
_callback(pointerData);
});
// Why `domWindow` you ask? See this fiddle: https://jsfiddle.net/ditman/7towxaqp
_addPointerEventListener(domWindow, 'pointermove', (DomPointerEvent event) {
final int device = _getPointerId(event);
final _ButtonSanitizer sanitizer = _ensureSanitizer(device);
@@ -761,6 +776,7 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin {
}
}, useCapture: false, checkModifiers: false);
// TODO(dit): This must happen in the glassPane, https://github.com/flutter/flutter/issues/116561
_addPointerEventListener(domWindow, 'pointerup', (DomPointerEvent event) {
final int device = _getPointerId(event);
if (_hasSanitizer(device)) {
@@ -774,6 +790,8 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin {
}
});
// TODO(dit): Synthesize a "cancel" event when 'pointerup' happens outside of the glassPane, https://github.com/flutter/flutter/issues/116561
// A browser fires cancel event if it concludes the pointer will no longer
// be able to generate events (example: device is deactivated)
_addPointerEventListener(glassPaneElement, 'pointercancel', (DomPointerEvent event) {
@@ -806,6 +824,7 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin {
final double tilt = _computeHighestTilt(event);
final Duration timeStamp = _BaseAdapter._eventTimeStampToDuration(event.timeStamp!);
final num? pressure = event.pressure;
final ui.Offset offset = computeEventOffsetToTarget(event, glassPaneElement);
_pointerDataConverter.convert(
data,
change: details.change,
@@ -813,8 +832,8 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin {
kind: kind,
signalKind: ui.PointerSignalKind.none,
device: _getPointerId(event),
physicalX: event.clientX * ui.window.devicePixelRatio,
physicalY: event.clientY * ui.window.devicePixelRatio,
physicalX: offset.dx * ui.window.devicePixelRatio,
physicalY: offset.dy * ui.window.devicePixelRatio,
buttons: details.buttons,
pressure: pressure == null ? 0.0 : pressure.toDouble(),
pressureMax: 1.0,
@@ -834,6 +853,10 @@ class _PointerAdapter extends _BaseAdapter with _WheelEventListenerMixin {
return coalescedEvents;
}
}
// Important: coalesced events lack the `eventTarget` property (because they're
// being handled in a deferred way).
//
// See the "Note" here: https://developer.mozilla.org/en-US/docs/Web/API/Event/currentTarget
return <DomPointerEvent>[event];
}
@@ -997,6 +1020,7 @@ class _TouchAdapter extends _BaseAdapter {
timeStamp: timeStamp,
signalKind: ui.PointerSignalKind.none,
device: touch.identifier!.toInt(),
// Account for zoom/scroll in the TouchEvent
physicalX: touch.clientX * ui.window.devicePixelRatio,
physicalY: touch.clientY * ui.window.devicePixelRatio,
buttons: pressed ? _kPrimaryMouseButton : 0,
@@ -1080,6 +1104,7 @@ class _MouseAdapter extends _BaseAdapter with _WheelEventListenerMixin {
_callback(pointerData);
});
// Why `domWindow` you ask? See this fiddle: https://jsfiddle.net/ditman/7towxaqp
_addMouseEventListener(domWindow, 'mousemove', (DomMouseEvent event) {
final List<ui.PointerData> pointerData = <ui.PointerData>[];
final _SanitizedDetails? up = _sanitizer.sanitizeMissingRightClickUp(buttons: event.buttons!.toInt());
@@ -1100,6 +1125,7 @@ class _MouseAdapter extends _BaseAdapter with _WheelEventListenerMixin {
}
}, useCapture: false);
// TODO(dit): This must happen in the glassPane, https://github.com/flutter/flutter/issues/116561
_addMouseEventListener(domWindow, 'mouseup', (DomMouseEvent event) {
final List<ui.PointerData> pointerData = <ui.PointerData>[];
final _SanitizedDetails? sanitizedDetails = _sanitizer.sanitizeUpEvent(buttons: event.buttons?.toInt());
@@ -1124,6 +1150,7 @@ class _MouseAdapter extends _BaseAdapter with _WheelEventListenerMixin {
assert(data != null);
assert(event != null);
assert(details != null);
final ui.Offset offset = computeEventOffsetToTarget(event, glassPaneElement);
_pointerDataConverter.convert(
data,
change: details.change,
@@ -1131,8 +1158,8 @@ class _MouseAdapter extends _BaseAdapter with _WheelEventListenerMixin {
kind: ui.PointerDeviceKind.mouse,
signalKind: ui.PointerSignalKind.none,
device: _mouseDeviceId,
physicalX: event.clientX * ui.window.devicePixelRatio,
physicalY: event.clientY * ui.window.devicePixelRatio,
physicalX: offset.dx * ui.window.devicePixelRatio,
physicalY: offset.dy * ui.window.devicePixelRatio,
buttons: details.buttons,
pressure: 1.0,
pressureMax: 1.0,

View File

@@ -0,0 +1,113 @@
// 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 'package:ui/ui.dart' as ui show Offset;
import '../dom.dart';
import '../semantics.dart' show EngineSemanticsOwner;
/// Returns an [ui.Offset] of the position of [event], relative to the position of [actualTarget].
///
/// The offset is *not* multiplied by DPR or anything else, it's the closest
/// to what the DOM would return if we had currentTarget readily available.
///
/// This needs an `actualTarget`, because the `event.currentTarget` (which is what
/// this would really need to use) gets lost when the `event` comes from a "coalesced"
/// event.
///
/// It also takes into account semantics being enabled to fix the case where
/// offsetX, offsetY == 0 (TalkBack events).
ui.Offset computeEventOffsetToTarget(DomMouseEvent event, DomElement actualTarget) {
// On top of a platform view
if (event.target != actualTarget) {
return _computeOffsetOnPlatformView(event, actualTarget);
}
// On a TalkBack event
if (EngineSemanticsOwner.instance.semanticsEnabled && event.offsetX == 0 && event.offsetY == 0) {
return _computeOffsetForTalkbackEvent(event, actualTarget);
}
// Return the offsetX/Y in the normal case.
// (This works with 3D translations of the parent element.)
return ui.Offset(event.offsetX, event.offsetY);
}
/// Computes the event offset when hovering over a platformView.
///
/// This still uses offsetX/Y, but adds the offset from the top/left corner of the
/// platform view to the glass pane (`actualTarget`).
///
/// ×--FlutterView(actualTarget)--------------+
/// |\ |
/// | x1,y1 |
/// | |
/// | |
/// | ×-PlatformView(target)---------+ |
/// | |\ | |
/// | | x2,y2 | |
/// | | | |
/// | | × (event) | |
/// | | \ | |
/// | | offsetX, offsetY | |
/// | | (Relative to PlatformView) | |
/// | +------------------------------+ |
/// +-----------------------------------------+
///
/// Offset between PlatformView and FlutterView (xP, yP) = (x2 - x1, y2 - y1)
///
/// Event offset relative to FlutterView = (offsetX + xP, offsetY + yP)
// TODO(dit): Make this understand 3D transforms, https://github.com/flutter/flutter/issues/117091
ui.Offset _computeOffsetOnPlatformView(DomMouseEvent event, DomElement actualTarget) {
final DomElement target = event.target! as DomElement;
final DomRect targetRect = target.getBoundingClientRect();
final DomRect actualTargetRect = actualTarget.getBoundingClientRect();
final double offsetTop = targetRect.y - actualTargetRect.y;
final double offsetLeft = targetRect.x - actualTargetRect.x;
return ui.Offset(event.offsetX + offsetLeft, event.offsetY + offsetTop);
}
/// Computes the event offset when TalkBack is firing the event.
///
/// In this case, we need to use the clientX/Y position of the event (which are
/// relative to the absolute top-left corner of the page, including scroll), then
/// deduct the offsetLeft/Top from every offsetParent of the `actualTarget`.
///
/// ×-Page----║-------------------------------+
/// | ║ |
/// | ×-------║--------offsetParent(s)-----+ |
/// | |\ | |
/// | | offsetLeft, offsetTop | |
/// | | | |
/// | | | |
/// | | ×-----║-------------actualTarget-+ | |
/// | | | | | |
/// ═════ × ─ (scrollLeft, scrollTop)═ ═ ═
/// | | | | | |
/// | | | × | | |
/// | | | \ | | |
/// | | | clientX, clientY | | |
/// | | | (Relative to Page + Scroll) | | |
/// | | +-----║--------------------------+ | |
/// | +-------║----------------------------+ |
/// +---------║-------------------------------+
///
/// Computing the offset of the event relative to the actualTarget requires to
/// compute the clientX, clientY of the actualTarget. To do that, we iterate
/// up the offsetParent elements of actualTarget adding their offset and scroll
/// positions. Finally, we deduct that from clientX, clientY of the event.
// TODO(dit): Make this understand 3D transforms, https://github.com/flutter/flutter/issues/117091
ui.Offset _computeOffsetForTalkbackEvent(DomMouseEvent event, DomElement actualTarget) {
assert(EngineSemanticsOwner.instance.semanticsEnabled);
// Use clientX/clientY as the position of the event (this is relative to
// the top left of the page, including scroll)
double offsetX = event.clientX;
double offsetY = event.clientY;
// Compute the scroll offset of actualTarget
DomHTMLElement parent = actualTarget as DomHTMLElement;
while(parent.offsetParent != null){
offsetX -= parent.offsetLeft - parent.scrollLeft;
offsetY -= parent.offsetTop - parent.scrollTop;
parent = parent.offsetParent!;
}
return ui.Offset(offsetX, offsetY);
}

View File

@@ -0,0 +1,88 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:ui/src/engine/dom.dart';
import 'package:ui/src/engine/window.dart';
import 'package:ui/ui.dart' as ui show Size;
import 'dimensions_provider.dart';
/// This class provides observable, real-time dimensions of a host element.
///
/// All the measurements returned from this class are potentially *expensive*,
/// and should be cached as needed. Every call to every method on this class
/// WILL perform actual DOM measurements.
class CustomElementDimensionsProvider extends DimensionsProvider {
/// Creates a [CustomElementDimensionsProvider] from a [_hostElement].
CustomElementDimensionsProvider(this._hostElement) {
// Hook up a resize observer on the hostElement (if supported!).
_hostElementResizeObserver = createDomResizeObserver((
List<DomResizeObserverEntry> entries,
DomResizeObserver _,
) {
entries
.map((DomResizeObserverEntry entry) =>
ui.Size(entry.contentRect.width, entry.contentRect.height))
.forEach(_broadcastSize);
});
assert(() {
if (_hostElementResizeObserver == null) {
domWindow.console.warn('ResizeObserver API not supported. '
'Flutter will not resize with its hostElement.');
}
return true;
}());
_hostElementResizeObserver?.observe(_hostElement);
}
// The host element that will be used to retrieve (and observe) app size measurements.
final DomElement _hostElement;
// Handle resize events
late DomResizeObserver? _hostElementResizeObserver;
final StreamController<ui.Size> _onResizeStreamController =
StreamController<ui.Size>.broadcast();
// Broadcasts the last seen `Size`.
void _broadcastSize(ui.Size size) {
_onResizeStreamController.add(size);
}
@override
void close() {
_hostElementResizeObserver?.disconnect();
// ignore:unawaited_futures
_onResizeStreamController.close();
}
@override
Stream<ui.Size> get onResize => _onResizeStreamController.stream;
@override
ui.Size computePhysicalSize() {
final double devicePixelRatio = getDevicePixelRatio();
return ui.Size(
_hostElement.clientWidth * devicePixelRatio,
_hostElement.clientHeight * devicePixelRatio,
);
}
@override
WindowPadding computeKeyboardInsets(
double physicalHeight,
bool isEditingOnMobile,
) {
return const WindowPadding(
top: 0,
right: 0,
bottom: 0,
left: 0,
);
}
}

View File

@@ -0,0 +1,65 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:ui/src/engine/window.dart';
import 'package:ui/ui.dart' as ui show Size;
import '../../dom.dart';
import 'custom_element_dimensions_provider.dart';
import 'full_page_dimensions_provider.dart';
/// This class provides the dimensions of the "viewport" in which the app is rendered.
///
/// Similarly to the `EmbeddingStrategy`, this class is specialized to handle
/// different sources of information:
///
/// * [FullPageDimensionsProvider] - The default behavior, uses the VisualViewport
/// API to measure, and react to, the dimensions of the full browser window.
/// * [CustomElementDimensionsProvider] - Uses a custom html Element as the source
/// of dimensions, and the ResizeObserver to notify the app of changes.
///
/// All the measurements returned from this class are potentially *expensive*,
/// and should be cached as needed. Every call to every method on this class
/// WILL perform actual DOM measurements.
abstract class DimensionsProvider {
DimensionsProvider();
/// Creates the appropriate DimensionsProvider depending on the incoming [hostElement].
factory DimensionsProvider.create({DomElement? hostElement}) {
if (hostElement != null) {
return CustomElementDimensionsProvider(hostElement);
} else {
return FullPageDimensionsProvider();
}
}
/// Returns the DPI reported by the browser.
double getDevicePixelRatio() {
// This is overridable in tests.
return window.devicePixelRatio;
}
/// Returns the [ui.Size] of the "viewport".
///
/// This function is expensive. It triggers browser layout if there are
/// pending DOM writes.
ui.Size computePhysicalSize();
/// Returns the [WindowPadding] of the keyboard insets (if present).
WindowPadding computeKeyboardInsets(
double physicalHeight,
bool isEditingOnMobile,
);
/// Returns a Stream with the changes to [ui.Size] (when cheap to get).
Stream<ui.Size?> get onResize;
/// Clears any resources grabbed by the DimensionsProvider instance.
///
/// All internal event handlers will be disconnected, and the [onResize] Stream
/// will be closed.
void close();
}

View File

@@ -0,0 +1,123 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:js/js.dart';
import 'package:ui/src/engine/browser_detection.dart';
import 'package:ui/src/engine/dom.dart';
import 'package:ui/src/engine/window.dart';
import 'package:ui/ui.dart' as ui show Size;
import 'dimensions_provider.dart';
/// This class provides the real-time dimensions of a "full page" viewport.
///
/// All the measurements returned from this class are potentially *expensive*,
/// and should be cached as needed. Every call to every method on this class
/// WILL perform actual DOM measurements.
class FullPageDimensionsProvider extends DimensionsProvider {
/// Constructs a global [FullPageDimensionsProvider].
///
/// Doesn't need any parameters, because all the measurements come from the
/// globally available [DomVisualViewport].
FullPageDimensionsProvider() {
// Determine what 'resize' event we'll be listening to.
// This is needed for older browsers (Firefox < 91, Safari < 13)
// TODO(dit): Clean this up, https://github.com/flutter/flutter/issues/117105
final DomEventTarget resizeEventTarget =
domWindow.visualViewport ?? domWindow;
// Subscribe to the 'resize' event, and convert it to a ui.Size stream.
_domResizeSubscription = DomSubscription(
resizeEventTarget,
'resize',
allowInterop(_onVisualViewportResize),
);
}
late DomSubscription _domResizeSubscription;
final StreamController<ui.Size?> _onResizeStreamController =
StreamController<ui.Size?>.broadcast();
void _onVisualViewportResize(DomEvent event) {
// `event` doesn't contain any size information (as opposed to the custom
// element resize observer). If it did, we could broadcast the physical
// dimensions here and never have to re-measure the app, until the next
// resize event triggers.
// Would it be too costly to broadcast the computed physical size from here,
// and then never re-measure the app?
// Related: https://github.com/flutter/flutter/issues/117036
_onResizeStreamController.add(null);
}
@override
void close() {
_domResizeSubscription.cancel();
// ignore:unawaited_futures
_onResizeStreamController.close();
}
@override
Stream<ui.Size?> get onResize => _onResizeStreamController.stream;
@override
ui.Size computePhysicalSize() {
late double windowInnerWidth;
late double windowInnerHeight;
final DomVisualViewport? viewport = domWindow.visualViewport;
final double devicePixelRatio = getDevicePixelRatio();
if (viewport != null) {
if (operatingSystem == OperatingSystem.iOs) {
/// Chrome on iOS reports incorrect viewport.height when app
/// starts in portrait orientation and the phone is rotated to
/// landscape.
///
/// We instead use documentElement clientWidth/Height to read
/// accurate physical size. VisualViewport api is only used during
/// text editing to make sure inset is correctly reported to
/// framework.
final double docWidth = domDocument.documentElement!.clientWidth;
final double docHeight = domDocument.documentElement!.clientHeight;
windowInnerWidth = docWidth * devicePixelRatio;
windowInnerHeight = docHeight * devicePixelRatio;
} else {
windowInnerWidth = viewport.width! * devicePixelRatio;
windowInnerHeight = viewport.height! * devicePixelRatio;
}
} else {
windowInnerWidth = domWindow.innerWidth! * devicePixelRatio;
windowInnerHeight = domWindow.innerHeight! * devicePixelRatio;
}
return ui.Size(
windowInnerWidth,
windowInnerHeight,
);
}
@override
WindowPadding computeKeyboardInsets(
double physicalHeight,
bool isEditingOnMobile,
) {
final double devicePixelRatio = getDevicePixelRatio();
final DomVisualViewport? viewport = domWindow.visualViewport;
late double windowInnerHeight;
if (viewport != null) {
if (operatingSystem == OperatingSystem.iOs && !isEditingOnMobile) {
windowInnerHeight =
domDocument.documentElement!.clientHeight * devicePixelRatio;
} else {
windowInnerHeight = viewport.height! * devicePixelRatio;
}
} else {
windowInnerHeight = domWindow.innerHeight! * devicePixelRatio;
}
final double bottomPadding = physicalHeight - windowInnerHeight;
return WindowPadding(bottom: bottomPadding, left: 0, right: 0, top: 0);
}
}

View File

@@ -0,0 +1,57 @@
// 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 'package:ui/src/engine/dom.dart';
import 'embedding_strategy.dart';
/// An [EmbeddingStrategy] that renders flutter inside a target host element.
///
/// This strategy attempts to minimize DOM modifications outside of the host
/// element, so it plays "nice" with other web frameworks.
class CustomElementEmbeddingStrategy extends EmbeddingStrategy {
/// Creates a [CustomElementEmbeddingStrategy] to embed a Flutter view into [_hostElement].
CustomElementEmbeddingStrategy(this._hostElement) {
_hostElement.clearChildren();
}
/// The target element in which this strategy will embedd Flutter.
final DomElement _hostElement;
@override
void initialize({
Map<String, String>? hostElementAttributes,
}) {
// ignore:avoid_function_literals_in_foreach_calls
hostElementAttributes?.entries.forEach((MapEntry<String, String> entry) {
_setHostAttribute(entry.key, entry.value);
});
_setHostAttribute('flt-embedding', 'custom-element');
}
@override
void attachGlassPane(DomElement glassPaneElement) {
glassPaneElement
..style.width = '100%'
..style.height = '100%'
..style.display = 'block'
..style.overflow = 'hidden'
..style.position = 'relative';
_hostElement.appendChild(glassPaneElement);
registerElementForCleanup(glassPaneElement);
}
@override
void attachResourcesHost(DomElement resourceHost, {DomElement? nextTo}) {
_hostElement.insertBefore(resourceHost, nextTo);
registerElementForCleanup(resourceHost);
}
void _setHostAttribute(String name, String value) {
_hostElement.setAttribute(name, value);
}
}

View File

@@ -0,0 +1,58 @@
// 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 'package:meta/meta.dart';
import 'package:ui/src/engine/dom.dart';
import 'package:ui/src/engine/view_embedder/hot_restart_cache_handler.dart';
import 'custom_element_embedding_strategy.dart';
import 'full_page_embedding_strategy.dart';
/// Controls how a Flutter app is placed, sized and measured on the page.
///
/// The base class handles general behavior (like hot-restart cleanup), and then
/// each specialization enables different types of DOM embeddings:
///
/// * [FullPageEmbeddingStrategy] - The default behavior, where flutter takes
/// control of the whole page.
/// * [CustomElementEmbeddingStrategy] - Flutter is rendered inside a custom host
/// element, provided by the web app programmer through the engine
/// initialization.
abstract class EmbeddingStrategy {
EmbeddingStrategy() {
// Initialize code to handle hot-restart (debug only).
assert(() {
_hotRestartCache = HotRestartCacheHandler();
return true;
}());
}
factory EmbeddingStrategy.create({DomElement? hostElement}) {
if (hostElement != null) {
return CustomElementEmbeddingStrategy(hostElement);
} else {
return FullPageEmbeddingStrategy();
}
}
/// Keeps a list of elements to be cleaned up at hot-restart.
HotRestartCacheHandler? _hotRestartCache;
void initialize({
Map<String, String>? hostElementAttributes,
});
/// Attaches the glassPane element into the hostElement.
void attachGlassPane(DomElement glassPaneElement);
/// Attaches the resourceHost element into the hostElement.
void attachResourcesHost(DomElement resourceHost, {DomElement? nextTo});
/// Registers a [DomElement] to be cleaned up after hot restart.
@mustCallSuper
void registerElementForCleanup(DomElement element) {
_hotRestartCache?.registerElement(element);
}
}

View File

@@ -0,0 +1,108 @@
// 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 'package:ui/src/engine/dom.dart';
import 'package:ui/src/engine/util.dart' show assertionsEnabled, setElementStyle;
import 'embedding_strategy.dart';
/// An [EmbeddingStrategy] that takes over the whole web page.
///
/// This strategy takes over the <body> element, modifies the viewport meta-tag,
/// and ensures that the root Flutter view covers the whole screen.
class FullPageEmbeddingStrategy extends EmbeddingStrategy {
@override
void initialize({
Map<String, String>? hostElementAttributes,
}) {
// ignore:avoid_function_literals_in_foreach_calls
hostElementAttributes?.entries.forEach((MapEntry<String, String> entry) {
_setHostAttribute(entry.key, entry.value);
});
_setHostAttribute('flt-embedding', 'full-page');
_applyViewportMeta();
_setHostStyles();
}
@override
void attachGlassPane(DomElement glassPaneElement) {
/// Tweaks style so the glassPane works well with the hostElement.
glassPaneElement.style
..position = 'absolute'
..top = '0'
..right = '0'
..bottom = '0'
..left = '0';
domDocument.body!.append(glassPaneElement);
registerElementForCleanup(glassPaneElement);
}
@override
void attachResourcesHost(DomElement resourceHost, {DomElement? nextTo}) {
domDocument.body!.insertBefore(resourceHost, nextTo);
registerElementForCleanup(resourceHost);
}
void _setHostAttribute(String name, String value) {
domDocument.body!.setAttribute(name, value);
}
// Sets the global styles for a flutter app.
void _setHostStyles() {
final DomHTMLBodyElement bodyElement = domDocument.body!;
setElementStyle(bodyElement, 'position', 'fixed');
setElementStyle(bodyElement, 'top', '0');
setElementStyle(bodyElement, 'right', '0');
setElementStyle(bodyElement, 'bottom', '0');
setElementStyle(bodyElement, 'left', '0');
setElementStyle(bodyElement, 'overflow', 'hidden');
setElementStyle(bodyElement, 'padding', '0');
setElementStyle(bodyElement, 'margin', '0');
setElementStyle(bodyElement, 'user-select', 'none');
setElementStyle(bodyElement, '-webkit-user-select', 'none');
// This is required to prevent the browser from doing any native touch
// handling. If this is not done, the browser doesn't report 'pointermove'
// events properly.
setElementStyle(bodyElement, 'touch-action', 'none');
}
// Sets a meta viewport tag appropriate for Flutter Web in full screen.
void _applyViewportMeta() {
for (final DomElement viewportMeta
in domDocument.head!.querySelectorAll('meta[name="viewport"]')) {
if (assertionsEnabled) {
// Filter out the meta tag that the engine placed on the page. This is
// to avoid UI flicker during hot restart. Hot restart will clean up the
// old meta tag synchronously with the first post-restart frame.
if (!viewportMeta.hasAttribute('flt-viewport')) {
print(
'WARNING: found an existing <meta name="viewport"> tag. Flutter '
'Web uses its own viewport configuration for better compatibility '
'with Flutter. This tag will be replaced.',
);
}
}
viewportMeta.remove();
}
// The meta viewport is always removed by the for method above, so we don't
// need to do anything else here, other than create it again.
final DomHTMLMetaElement viewportMeta = createDomHTMLMetaElement()
..setAttribute('flt-viewport', '')
..name = 'viewport'
..content = 'width=device-width, initial-scale=1.0, '
'maximum-scale=1.0, user-scalable=no';
domDocument.head!.append(viewportMeta);
registerElementForCleanup(viewportMeta);
}
}

View File

@@ -0,0 +1,57 @@
// 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 'package:meta/meta.dart';
import '../dom.dart';
import '../safe_browser_api.dart';
/// Handles [DomElement]s that need to be removed after a hot-restart.
///
/// Elements are stored in an [_elements] list, backed by a global JS variable,
/// named [defaultCacheName].
///
/// When the app hot-restarts (and a new instance of this class is created),
/// everything in [_elements] is removed from the DOM.
class HotRestartCacheHandler {
HotRestartCacheHandler() {
if (_elements.isNotEmpty) {
// We are in a post hot-restart world, clear the elements now.
_clearAllElements();
}
}
/// The name for the JS global variable backing this cache.
@visibleForTesting
static const String defaultCacheName = '__flutter_state';
/// The js-interop layer backing [_elements].
///
/// Elements are stored in a JS global array named [defaultCacheName].
late List<DomElement?>? _jsElements;
/// The elements that need to be cleaned up after hot-restart.
List<DomElement?> get _elements {
_jsElements =
getJsProperty<List<DomElement?>?>(domWindow, defaultCacheName);
if (_jsElements == null) {
_jsElements = <DomElement?>[];
setJsProperty(domWindow, defaultCacheName, _jsElements);
}
return _jsElements!;
}
/// Removes every element from [_elements] and empties the list.
void _clearAllElements() {
for (final DomElement? element in _elements) {
element?.remove();
}
_elements.clear();
}
/// Registers a [DomElement] to be removed after hot-restart.
void registerElement(DomElement element) {
_elements.add(element);
}
}

View File

@@ -12,8 +12,7 @@ import 'package:js/js.dart';
import 'package:meta/meta.dart';
import 'package:ui/ui.dart' as ui;
import '../engine.dart' show registerHotRestartListener, renderer;
import 'browser_detection.dart';
import '../engine.dart' show DimensionsProvider, registerHotRestartListener, renderer;
import 'dom.dart';
import 'navigation/history.dart';
import 'navigation/js_url_strategy.dart';
@@ -55,6 +54,7 @@ class EngineFlutterWindow extends ui.SingletonFlutterWindow {
registerHotRestartListener(() {
_browserHistory?.dispose();
renderer.clearFragmentProgramCache();
_dimensionsProvider.close();
});
}
@@ -207,6 +207,16 @@ class EngineFlutterWindow extends ui.SingletonFlutterWindow {
const ui.ViewConfiguration();
}
late DimensionsProvider _dimensionsProvider;
void configureDimensionsProvider(DimensionsProvider dimensionsProvider) {
_dimensionsProvider = dimensionsProvider;
}
@override
double get devicePixelRatio => _dimensionsProvider.getDevicePixelRatio();
Stream<ui.Size?> get onResize => _dimensionsProvider.onResize;
@override
ui.Size get physicalSize {
if (_physicalSize == null) {
@@ -232,38 +242,7 @@ class EngineFlutterWindow extends ui.SingletonFlutterWindow {
}());
if (!override) {
double windowInnerWidth;
double windowInnerHeight;
final DomVisualViewport? viewport = domWindow.visualViewport;
if (viewport != null) {
if (operatingSystem == OperatingSystem.iOs) {
/// Chrome on iOS reports incorrect viewport.height when app
/// starts in portrait orientation and the phone is rotated to
/// landscape.
///
/// We instead use documentElement clientWidth/Height to read
/// accurate physical size. VisualViewport api is only used during
/// text editing to make sure inset is correctly reported to
/// framework.
final double docWidth =
domDocument.documentElement!.clientWidth;
final double docHeight =
domDocument.documentElement!.clientHeight;
windowInnerWidth = docWidth * devicePixelRatio;
windowInnerHeight = docHeight * devicePixelRatio;
} else {
windowInnerWidth = viewport.width! * devicePixelRatio;
windowInnerHeight = viewport.height! * devicePixelRatio;
}
} else {
windowInnerWidth = domWindow.innerWidth! * devicePixelRatio;
windowInnerHeight = domWindow.innerHeight! * devicePixelRatio;
}
_physicalSize = ui.Size(
windowInnerWidth,
windowInnerHeight,
);
_physicalSize = _dimensionsProvider.computePhysicalSize();
}
}
@@ -273,21 +252,10 @@ class EngineFlutterWindow extends ui.SingletonFlutterWindow {
}
void computeOnScreenKeyboardInsets(bool isEditingOnMobile) {
double windowInnerHeight;
final DomVisualViewport? viewport = domWindow.visualViewport;
if (viewport != null) {
if (operatingSystem == OperatingSystem.iOs && !isEditingOnMobile) {
windowInnerHeight =
domDocument.documentElement!.clientHeight * devicePixelRatio;
} else {
windowInnerHeight = viewport.height! * devicePixelRatio;
}
} else {
windowInnerHeight = domWindow.innerHeight! * devicePixelRatio;
}
final double bottomPadding = _physicalSize!.height - windowInnerHeight;
_viewInsets =
WindowPadding(bottom: bottomPadding, left: 0, right: 0, top: 0);
_viewInsets = _dimensionsProvider.computeKeyboardInsets(
_physicalSize!.height,
isEditingOnMobile,
);
}
/// Uses the previous physical size and current innerHeight/innerWidth
@@ -305,26 +273,16 @@ class EngineFlutterWindow extends ui.SingletonFlutterWindow {
/// height: 658 width: 393
/// height: 368 width: 393
bool isRotation() {
double height = 0;
double width = 0;
if (domWindow.visualViewport != null) {
height =
domWindow.visualViewport!.height! * devicePixelRatio;
width = domWindow.visualViewport!.width! * devicePixelRatio;
} else {
height = domWindow.innerHeight! * devicePixelRatio;
width = domWindow.innerWidth! * devicePixelRatio;
}
// This method compares the new dimensions with the previous ones.
// Return false if the previous dimensions are not set.
if (_physicalSize != null) {
final ui.Size current = _dimensionsProvider.computePhysicalSize();
// First confirm both height and width are effected.
if (_physicalSize!.height != height && _physicalSize!.width != width) {
if (_physicalSize!.height != current.height && _physicalSize!.width != current.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)) {
if ((_physicalSize!.height > _physicalSize!.width && current.height < current.width) ||
(_physicalSize!.width > _physicalSize!.height && current.width < current.height)) {
// Rotation detected
return true;
}

View File

@@ -15,7 +15,7 @@ void testMain() {
domDocument.body!.append(rootNode);
group('ShadowDomHostNode', () {
final HostNode hostNode = ShadowDomHostNode(rootNode);
final HostNode hostNode = ShadowDomHostNode(rootNode, '14px monospace');
test('Initializes and attaches a shadow root', () {
expect(domInstanceOfString(hostNode.node, 'ShadowRoot'), isTrue);
@@ -33,30 +33,90 @@ void testMain() {
});
test('Attaches a stylesheet to the shadow root', () {
final DomElement firstChild =
(hostNode.node as DomShadowRoot).childNodes.toList()[0] as DomElement;
final DomElement? style =
hostNode.querySelector('#flt-internals-stylesheet');
expect(firstChild.tagName, equalsIgnoringCase('style'));
expect(style, isNotNull);
expect(style!.tagName, equalsIgnoringCase('style'));
});
test('(Self-test) hasCssRule can extract rules', () {
final DomElement? style =
hostNode.querySelector('#flt-internals-stylesheet');
final bool hasRule = hasCssRule(style,
selector: '.flt-text-editing::placeholder',
declaration: 'opacity: 0');
final bool hasFakeRule = hasCssRule(style,
selector: 'input::selection', declaration: 'color: #fabada;');
expect(hasRule, isTrue);
expect(hasFakeRule, isFalse);
});
test('Attaches outrageous text styles to flt-scene-host', () {
final DomElement? style =
hostNode.querySelector('#flt-internals-stylesheet');
final bool hasColorRed = hasCssRule(style,
selector: 'flt-scene-host', declaration: 'color: red');
bool hasFont = false;
if (isSafari) {
// Safari expands the shorthand rules, so we check for all we've set (separately).
hasFont = hasCssRule(style,
selector: 'flt-scene-host',
declaration: 'font-family: monospace') &&
hasCssRule(style,
selector: 'flt-scene-host', declaration: 'font-size: 14px');
} else {
hasFont = hasCssRule(style,
selector: 'flt-scene-host', declaration: 'font: 14px monospace');
}
expect(hasColorRed, isTrue,
reason: 'Should make foreground color red within scene host.');
expect(hasFont, isTrue, reason: 'Should pass default css font.');
});
test('Attaches styling to remove password reveal icons on Edge', () {
final DomElement? edgeStyleElement = hostNode.querySelector('#ms-reveal');
final DomElement? style =
hostNode.querySelector('#flt-internals-stylesheet');
expect(edgeStyleElement, isNotNull);
expect(edgeStyleElement!.innerText, 'input::-ms-reveal {display: none;}');
// Check that style.sheet! contains input::-ms-reveal rule
final bool hidesRevealIcons = hasCssRule(style,
selector: 'input::-ms-reveal', declaration: 'display: none');
final bool codeRanInFakeyBrowser = hasCssRule(style,
selector: 'input.fallback-for-fakey-browser-in-ci',
declaration: 'display: none');
if (codeRanInFakeyBrowser) {
print('Please, fix https://github.com/flutter/flutter/issues/116302');
}
expect(hidesRevealIcons || codeRanInFakeyBrowser, isTrue,
reason: 'In Edge, stylesheet must contain "input::-ms-reveal" rule.');
}, skip: !isEdge);
test('Does not attach the Edge-specific style tag on non-Edge browsers',
() {
final DomElement? edgeStyleElement = hostNode.querySelector('#ms-reveal');
expect(edgeStyleElement, isNull);
final DomElement? style =
hostNode.querySelector('#flt-internals-stylesheet');
// Check that style.sheet! contains input::-ms-reveal rule
final bool hidesRevealIcons = hasCssRule(style,
selector: 'input::-ms-reveal', declaration: 'display: none');
expect(hidesRevealIcons, isFalse);
}, skip: isEdge);
_runDomTests(hostNode);
});
group('ElementHostNode', () {
final HostNode hostNode = ElementHostNode(rootNode);
final HostNode hostNode = ElementHostNode(rootNode, '');
test('Initializes and attaches a child element', () {
expect(domInstanceOfString(hostNode.node, 'Element'), isTrue);
@@ -112,3 +172,25 @@ void _runDomTests(HostNode hostNode) {
});
});
}
/// Finds out whether a given CSS Rule ([selector] { [declaration]; }) exists in a [styleSheet].
bool hasCssRule(
DomElement? styleSheet, {
required String selector,
required String declaration,
}) {
assert(styleSheet != null);
assert((styleSheet! as DomHTMLStyleElement).sheet != null);
// regexr.com/740ff
final RegExp ruleLike =
RegExp('[^{]*(?:$selector)[^{]*{[^}]*(?:$declaration)[^}]*}');
final DomCSSStyleSheet sheet =
(styleSheet! as DomHTMLStyleElement).sheet! as DomCSSStyleSheet;
// Check that the cssText of any rule matches the ruleLike RegExp.
return sheet.cssRules
.map((DomCSSRule rule) => rule.cssText)
.any((String rule) => ruleLike.hasMatch(rule));
}

View File

@@ -23,13 +23,15 @@ typedef _ContextTestBody<T> = void Function(T);
void _testEach<T extends _BasicEventContext>(
Iterable<T> contexts,
String description,
_ContextTestBody<T> body,
_ContextTestBody<T> body, {
Object? skip,
}
) {
for (final T context in contexts) {
if (context.isSupported) {
test('${context.name} $description', () {
body(context);
});
}, skip: skip);
}
}
}
@@ -388,6 +390,8 @@ void testMain() {
expect(event.buttons, equals(1));
expect(event.client.x, equals(100));
expect(event.client.y, equals(101));
expect(event.offset.x, equals(100));
expect(event.offset.y, equals(101));
event = expectCorrectType(
context.mouseDown(clientX: 110, clientY: 111, button: 2, buttons: 2));
@@ -849,7 +853,7 @@ void testMain() {
packets.clear();
// Release the pointer on the semantics placeholder.
domWindow.dispatchEvent(context.primaryUp(
glassPane.dispatchEvent(context.primaryUp(
clientX: 100.0,
clientY: 200.0,
));
@@ -865,6 +869,7 @@ void testMain() {
semanticsPlaceholder.remove();
},
skip: isFirefox, // https://bugzilla.mozilla.org/show_bug.cgi?id=1804190
);
// BUTTONED ADAPTERS
@@ -2472,7 +2477,7 @@ void testMain() {
packets.clear();
// Move outside the glasspane.
domWindow.dispatchEvent(context.primaryMove(
glassPane.dispatchEvent(context.primaryMove(
clientX: 900.0,
clientY: 1900.0,
));
@@ -2484,7 +2489,7 @@ void testMain() {
packets.clear();
// Release outside the glasspane.
domWindow.dispatchEvent(context.primaryUp(
glassPane.dispatchEvent(context.primaryUp(
clientX: 1000.0,
clientY: 2000.0,
));
@@ -3351,6 +3356,7 @@ class _MouseEventContext extends _BasicEventContext
final List<dynamic> eventArgs = <dynamic>[
type,
<String, dynamic>{
'bubbles': true,
'buttons': buttons,
'button': button,
'clientX': clientX,
@@ -3569,6 +3575,7 @@ class _PointerEventContext extends _BasicEventContext
String? pointerType,
}) {
return createDomPointerEvent('pointerup', <String, dynamic>{
'bubbles': true,
'pointerId': pointer,
'button': button,
'buttons': buttons,

View File

@@ -0,0 +1,170 @@
// 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.
@TestOn('browser')
import 'dart:async';
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine/dom.dart';
import 'package:ui/src/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider.dart';
import 'package:ui/src/engine/window.dart';
import 'package:ui/ui.dart' as ui show Size;
void main() {
internalBootstrapBrowserTest(() => doTests);
}
void doTests() {
final DomElement sizeSource = createDomElement('div')
..style.display = 'block';
group('computePhysicalSize', () {
late CustomElementDimensionsProvider provider;
setUp(() {
sizeSource
..style.width = '10px'
..style.height = '10px';
domDocument.body!.append(sizeSource);
provider = CustomElementDimensionsProvider(sizeSource);
});
tearDown(() {
provider.close(); // cleanup
sizeSource.remove();
});
test('returns physical size of element (width * dpr)', () {
const double dpr = 2.5;
const double logicalWidth = 50;
const double logicalHeight = 75;
window.debugOverrideDevicePixelRatio(dpr);
sizeSource
..style.width = '${logicalWidth}px'
..style.height = '${logicalHeight}px';
const ui.Size expected = ui.Size(logicalWidth * dpr, logicalHeight * dpr);
final ui.Size computed = provider.computePhysicalSize();
expect(computed, expected);
});
});
group('computeKeyboardInsets', () {
late CustomElementDimensionsProvider provider;
setUp(() {
sizeSource
..style.width = '10px'
..style.height = '10px';
domDocument.body!.append(sizeSource);
provider = CustomElementDimensionsProvider(sizeSource);
});
tearDown(() {
provider.close(); // cleanup
sizeSource.remove();
});
test('from viewport physical size (simulated keyboard) - always zero', () {
// Simulate a 100px tall keyboard showing...
const double dpr = 2.5;
window.debugOverrideDevicePixelRatio(dpr);
const double keyboardGap = 100;
final double physicalHeight =
(domWindow.visualViewport!.height! + keyboardGap) * dpr;
final WindowPadding computed =
provider.computeKeyboardInsets(physicalHeight, false);
expect(computed.top, 0);
expect(computed.right, 0);
expect(computed.bottom, 0);
expect(computed.left, 0);
});
});
group('onResize Stream', () {
late CustomElementDimensionsProvider provider;
setUp(() async {
sizeSource
..style.width = '10px'
..style.height = '10px';
domDocument.body!.append(sizeSource);
provider = CustomElementDimensionsProvider(sizeSource);
// Let the DOM settle before starting the test, so we don't get the first
// 10,10 Size in the test. Otherwise, the ResizeObserver may trigger
// unexpectedly after the test has started, and break our "first" result.
await Future<void>.delayed(const Duration(milliseconds: 250));
});
tearDown(() {
provider.close(); // cleanup
sizeSource.remove();
});
test('funnels resize events on sizeSource', () async {
final Future<Object?> event = provider.onResize.first;
final Future<List<Object?>> events = provider.onResize.take(3).toList();
// The resize observer fires asynchronously, so we wait a little between
// resizes, so the observer has time to fire events separately.
await Future<void>.delayed(const Duration(milliseconds: 100), () {
sizeSource
..style.width = '100px'
..style.height = '100px';
});
await Future<void>.delayed(const Duration(milliseconds: 100), () {
sizeSource
..style.width = '200px'
..style.height = '200px';
});
await Future<void>.delayed(const Duration(milliseconds: 100), () {
sizeSource
..style.width = '300px'
..style.height = '300px';
});
// Let the DOM settle so the observer reports the last 300x300 mutation...
await Future<void>.delayed(const Duration(milliseconds: 100));
expect(event, completion(const ui.Size(100, 100)));
expect(events, completes);
expect(
events,
completion(const <ui.Size>[
ui.Size(100, 100),
ui.Size(200, 200),
ui.Size(300, 300),
]));
});
test('closed by onHotRestart', () async {
// Register an onDone listener for the stream
final Completer<bool> completer = Completer<bool>();
provider.onResize.listen(null, onDone: () {
completer.complete(true);
});
// Should close the stream
provider.close();
sizeSource
..style.width = '100px'
..style.height = '100px';
// Give time to the mutationObserver to fire (if needed, it won't)
await Future<void>.delayed(const Duration(milliseconds: 100));
expect(provider.onResize.isEmpty, completion(isTrue));
expect(completer.future, completion(isTrue));
});
});
}

View File

@@ -0,0 +1,48 @@
// 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.
@TestOn('browser')
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine/dom.dart';
import 'package:ui/src/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider.dart';
import 'package:ui/src/engine/view_embedder/dimensions_provider/dimensions_provider.dart';
import 'package:ui/src/engine/view_embedder/dimensions_provider/full_page_dimensions_provider.dart';
import 'package:ui/src/engine/window.dart';
void main() {
internalBootstrapBrowserTest(() => doTests);
}
void doTests() {
group('Factory', () {
test('Creates a FullPage instance when hostElement is null', () async {
final DimensionsProvider provider = DimensionsProvider.create();
expect(provider, isA<FullPageDimensionsProvider>());
});
test('Creates a CustomElement instance when hostElement is not null',
() async {
final DomElement element = createDomElement('some-random-element');
final DimensionsProvider provider = DimensionsProvider.create(
hostElement: element,
);
expect(provider, isA<CustomElementDimensionsProvider>());
});
});
group('getDevicePixelRatio', () {
test('Returns the correct pixelRatio', () async {
// Override the DPI to something known, but weird...
window.debugOverrideDevicePixelRatio(33930);
final DimensionsProvider provider = DimensionsProvider.create();
expect(provider.getDevicePixelRatio(), 33930);
});
});
}

View File

@@ -0,0 +1,107 @@
// 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.
@TestOn('browser')
import 'dart:async';
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine/dom.dart';
import 'package:ui/src/engine/view_embedder/dimensions_provider/full_page_dimensions_provider.dart';
import 'package:ui/src/engine/window.dart';
import 'package:ui/ui.dart' as ui show Size;
void main() {
internalBootstrapBrowserTest(() => doTests);
}
void doTests() {
group('computePhysicalSize', () {
late FullPageDimensionsProvider provider;
setUp(() {
provider = FullPageDimensionsProvider();
});
test('returns visualViewport physical size (width * dpr)', () {
const double dpr = 2.5;
window.debugOverrideDevicePixelRatio(dpr);
final ui.Size expected = ui.Size(domWindow.visualViewport!.width! * dpr,
domWindow.visualViewport!.height! * dpr);
final ui.Size computed = provider.computePhysicalSize();
expect(computed, expected);
});
});
group('computeKeyboardInsets', () {
late FullPageDimensionsProvider provider;
setUp(() {
provider = FullPageDimensionsProvider();
});
test('from viewport physical size (simulated keyboard)', () {
// Simulate a 100px tall keyboard showing...
const double dpr = 2.5;
window.debugOverrideDevicePixelRatio(dpr);
const double keyboardGap = 100;
final double physicalHeight =
(domWindow.visualViewport!.height! + keyboardGap) * dpr;
const double expectedBottom = keyboardGap * dpr;
final WindowPadding computed =
provider.computeKeyboardInsets(physicalHeight, false);
expect(computed.top, 0);
expect(computed.right, 0);
expect(computed.bottom, expectedBottom);
expect(computed.left, 0);
});
});
group('onResize Stream', () {
// Needed to synthesize "resize" events
final DomEventTarget resizeEventTarget =
domWindow.visualViewport ?? domWindow;
late FullPageDimensionsProvider provider;
setUp(() {
provider = FullPageDimensionsProvider();
});
test('funnels resize events on resizeEventTarget', () {
final Future<Object?> event = provider.onResize.first;
final Future<List<Object?>> events = provider.onResize.take(3).toList();
resizeEventTarget.dispatchEvent(createDomEvent('Event', 'resize'));
resizeEventTarget.dispatchEvent(createDomEvent('Event', 'resize'));
resizeEventTarget.dispatchEvent(createDomEvent('Event', 'resize'));
expect(event, completes);
expect(events, completes);
expect(events, completion(hasLength(3)));
});
test('closed by onHotRestart', () {
// Register an onDone listener for the stream
final Completer<bool> completer = Completer<bool>();
provider.onResize.listen(null, onDone: () {
completer.complete(true);
});
// Should close the stream
provider.close();
resizeEventTarget.dispatchEvent(createDomEvent('Event', 'resize'));
expect(provider.onResize.isEmpty, completion(isTrue));
expect(completer.future, completion(isTrue));
});
});
}

View File

@@ -0,0 +1,124 @@
// 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.
@TestOn('browser')
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine/dom.dart';
import 'package:ui/src/engine/view_embedder/embedding_strategy/custom_element_embedding_strategy.dart';
void main() {
internalBootstrapBrowserTest(() => doTests);
}
void doTests() {
late CustomElementEmbeddingStrategy strategy;
late DomElement target;
group('initialize', () {
setUp(() {
target = createDomElement('this-is-the-target');
domDocument.body!.append(target);
strategy = CustomElementEmbeddingStrategy(target);
});
tearDown(() {
target.remove();
});
test('Prepares target environment', () {
strategy.initialize(
hostElementAttributes: <String, String>{
'key-for-testing': 'value-for-testing',
},
);
expect(target.getAttribute('key-for-testing'), 'value-for-testing',
reason:
'Should add attributes as key=value into target element.');
expect(target.getAttribute('flt-embedding'), 'custom-element',
reason:
'Should identify itself as a specific key=value into the target element.');
});
});
group('attachGlassPane', () {
setUp(() {
target = createDomElement('this-is-the-target');
domDocument.body!.append(target);
strategy = CustomElementEmbeddingStrategy(target);
strategy.initialize();
});
tearDown(() {
target.remove();
});
test('Should attach glasspane into embedder target (body)', () async {
final DomElement glassPane = createDomElement('some-tag-for-tests');
final DomCSSStyleDeclaration style = glassPane.style;
expect(glassPane.isConnected, isFalse);
expect(style.position, '',
reason: 'Should not have any specific position.');
expect(style.width, '', reason: 'Should not have any size set.');
strategy.attachGlassPane(glassPane);
// Assert injection into <body>
expect(glassPane.isConnected, isTrue,
reason: 'Should inject glassPane into the document.');
expect(glassPane.parent, target,
reason: 'Should inject glassPane into the target element');
final DomCSSStyleDeclaration styleAfter = glassPane.style;
// Assert required styling to cover the viewport
expect(styleAfter.position, 'relative',
reason: 'Should be relatively positioned.');
expect(styleAfter.display, 'block', reason: 'Should be display:block.');
expect(styleAfter.width, '100%',
reason: 'Should take 100% of the available width');
expect(styleAfter.height, '100%',
reason: 'Should take 100% of the available height');
expect(styleAfter.overflow, 'hidden',
reason: 'Should hide the occasional oversized canvas elements.');
});
});
group('attachResourcesHost', () {
late DomElement glassPane;
setUp(() {
target = createDomElement('this-is-the-target');
glassPane = createDomElement('woah-a-glasspane');
domDocument.body!.append(target);
strategy = CustomElementEmbeddingStrategy(target);
strategy.initialize();
strategy.attachGlassPane(glassPane);
});
tearDown(() {
target.remove();
});
test(
'Should attach resources host into target (body), `nextTo` other element',
() async {
final DomElement resources = createDomElement('resources-host-element');
expect(resources.isConnected, isFalse);
strategy.attachResourcesHost(resources, nextTo: glassPane);
expect(resources.isConnected, isTrue,
reason: 'Should inject resources host somewhere in the document.');
expect(resources.parent, target,
reason: 'Should inject the resources into the target element');
expect(resources.nextSibling, glassPane,
reason: 'Should be injected `nextTo` the passed element.');
});
});
}

View File

@@ -0,0 +1,58 @@
// 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.
@TestOn('browser')
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine/dom.dart';
import 'package:ui/src/engine/view_embedder/embedding_strategy/custom_element_embedding_strategy.dart';
import 'package:ui/src/engine/view_embedder/embedding_strategy/embedding_strategy.dart';
import 'package:ui/src/engine/view_embedder/embedding_strategy/full_page_embedding_strategy.dart';
import '../hot_restart_cache_handler_test.dart' show getDomCache;
void main() {
internalBootstrapBrowserTest(() => doTests);
}
void doTests() {
group('Factory', () {
test('Creates a FullPage instance when hostElement is null', () async {
final EmbeddingStrategy strategy = EmbeddingStrategy.create();
expect(strategy, isA<FullPageEmbeddingStrategy>());
});
test('Creates a CustomElement instance when hostElement is not null',
() async {
final DomElement element = createDomElement('some-random-element');
final EmbeddingStrategy strategy = EmbeddingStrategy.create(
hostElement: element,
);
expect(strategy, isA<CustomElementEmbeddingStrategy>());
});
});
group('registerElementForCleanup', () {
test('stores elements in a global domCache', () async {
final EmbeddingStrategy strategy = EmbeddingStrategy.create();
final DomElement toBeCached = createDomElement('some-element-to-cache');
final DomElement other = createDomElement('other-element-to-cache');
final DomElement another = createDomElement('another-element-to-cache');
strategy.registerElementForCleanup(toBeCached);
strategy.registerElementForCleanup(other);
strategy.registerElementForCleanup(another);
final List<DomElement?> cache = getDomCache()!;
expect(cache, hasLength(3));
expect(cache.first, toBeCached);
expect(cache.last, another);
});
});
}

View File

@@ -0,0 +1,132 @@
// 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.
@TestOn('browser')
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine/dom.dart';
import 'package:ui/src/engine/view_embedder/embedding_strategy/full_page_embedding_strategy.dart';
void main() {
internalBootstrapBrowserTest(() => doTests);
}
void doTests() {
late FullPageEmbeddingStrategy strategy;
late DomElement target;
group('initialize', () {
setUp(() {
strategy = FullPageEmbeddingStrategy();
target = domDocument.body!;
final DomHTMLMetaElement meta = createDomHTMLMetaElement();
meta
..id = 'my_viewport_meta_for_testing'
..name = 'viewport'
..content = 'width=device-width, initial-scale=1.0, '
'maximum-scale=1.0, user-scalable=no';
domDocument.head!.append(meta);
});
test('Prepares target environment', () {
DomElement? userMeta =
domDocument.querySelector('#my_viewport_meta_for_testing');
expect(userMeta, isNotNull);
strategy.initialize(
hostElementAttributes: <String, String>{
'key-for-testing': 'value-for-testing',
},
);
expect(target.getAttribute('key-for-testing'), 'value-for-testing',
reason:
'Should add attributes as key=value into target element.');
expect(target.getAttribute('flt-embedding'), 'full-page',
reason:
'Should identify itself as a specific key=value into the target element.');
// Locate the viewport metas again...
userMeta = domDocument.querySelector('#my_viewport_meta_for_testing');
final DomElement? flutterMeta =
domDocument.querySelector('meta[name="viewport"]');
expect(userMeta, isNull,
reason: 'Should delete previously existing viewport meta tags.');
expect(flutterMeta, isNotNull);
expect(flutterMeta!.hasAttribute('flt-viewport'), isTrue,
reason: 'Should install flutter viewport meta tag.');
});
});
group('attachGlassPane', () {
setUp(() {
strategy = FullPageEmbeddingStrategy();
strategy.initialize();
});
test('Should attach glasspane into embedder target (body)', () async {
final DomElement glassPane = createDomElement('some-tag-for-tests');
final DomCSSStyleDeclaration style = glassPane.style;
expect(glassPane.isConnected, isFalse);
expect(style.position, '',
reason: 'Should not have any specific position.');
expect(style.top, '',
reason:
'Should not have any top/right/bottom/left positioning/inset.');
strategy.attachGlassPane(glassPane);
// Assert injection into <body>
expect(glassPane.isConnected, isTrue,
reason: 'Should inject glassPane into the document.');
expect(glassPane.parent, domDocument.body,
reason: 'Should inject glassPane into the <body>');
final DomCSSStyleDeclaration styleAfter = glassPane.style;
// Assert required styling to cover the viewport
expect(styleAfter.position, 'absolute',
reason: 'Should be absolutely positioned.');
expect(styleAfter.top, '0px', reason: 'Should cover the whole viewport.');
expect(styleAfter.right, '0px',
reason: 'Should cover the whole viewport.');
expect(styleAfter.bottom, '0px',
reason: 'Should cover the whole viewport.');
expect(styleAfter.left, '0px',
reason: 'Should cover the whole viewport.');
});
});
group('attachResourcesHost', () {
late DomElement glassPane;
setUp(() {
glassPane = createDomElement('some-tag-for-tests');
strategy = FullPageEmbeddingStrategy();
strategy.initialize();
strategy.attachGlassPane(glassPane);
});
test(
'Should attach resources host into target (body), `nextTo` other element',
() async {
final DomElement resources = createDomElement('resources-host-element');
expect(resources.isConnected, isFalse);
strategy.attachResourcesHost(resources, nextTo: glassPane);
expect(resources.isConnected, isTrue,
reason: 'Should inject resources host somewhere in the document.');
expect(resources.parent, domDocument.body,
reason: 'Should inject resources host into the <body>');
expect(resources.nextSibling, glassPane,
reason: 'Should be injected `nextTo` the passed element.');
});
});
}

View File

@@ -0,0 +1,82 @@
// 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.
@TestOn('browser')
import 'package:js/js_util.dart';
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine/dom.dart';
import 'package:ui/src/engine/view_embedder/hot_restart_cache_handler.dart';
void main() {
internalBootstrapBrowserTest(() => doTests);
}
void doTests() {
group('Constructor', () {
test('Creates a cache in the JS environment', () async {
final HotRestartCacheHandler cache = HotRestartCacheHandler();
expect(cache, isNotNull);
final List<DomElement?>? domCache = getDomCache();
expect(domCache, isNotNull);
expect(domCache, isEmpty);
});
});
group('registerElement', () {
HotRestartCacheHandler? cache;
List<DomElement?>? domCache;
setUp(() {
cache = HotRestartCacheHandler();
domCache = getDomCache();
});
test('Registers an element in the DOM cache', () async {
final DomElement element = createDomElement('for-test');
cache!.registerElement(element);
expect(domCache, hasLength(1));
expect(domCache!.last, element);
});
test('Registers elements in the DOM cache', () async {
final DomElement element = createDomElement('for-test');
domDocument.body!.append(element);
cache!.registerElement(element);
expect(domCache, hasLength(1));
expect(domCache!.last, element);
});
test('Clears registered elements from the DOM and the cache upon restart',
() async {
final DomElement element = createDomElement('for-test');
final DomElement element2 = createDomElement('for-test-two');
domDocument.body!.append(element);
domDocument.body!.append(element2);
cache!.registerElement(element);
expect(element.isConnected, isTrue);
expect(element2.isConnected, isTrue);
// Simulate a hot restart...
cache = HotRestartCacheHandler();
expect(domCache, hasLength(0));
expect(element.isConnected, isFalse); // Removed
expect(element2.isConnected, isTrue);
});
});
}
List<DomElement?>? getDomCache() => getProperty<List<DomElement?>?>(
domWindow, HotRestartCacheHandler.defaultCacheName);