[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:
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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.');
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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.');
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user