diff --git a/integration_tests/lib/custom/custom_element.dart b/integration_tests/lib/custom/custom_element.dart index 095c10ae2b..33a79e77ae 100644 --- a/integration_tests/lib/custom/custom_element.dart +++ b/integration_tests/lib/custom/custom_element.dart @@ -1,42 +1,79 @@ import 'dart:async'; -import 'package:kraken/dom.dart'; +import 'package:kraken/dom.dart' as dom; import 'package:kraken/widget.dart'; -import 'package:flutter/material.dart' show TextDirection, TextStyle, Color, Image, Text, AssetImage, Widget, BuildContext hide Element; +import 'package:waterfall_flow/waterfall_flow.dart'; +import 'package:flutter/material.dart'; + +class WaterfallFlowWidgetElement extends WidgetElement { + WaterfallFlowWidgetElement(dom.EventTargetContext? context) : + super(context); + + List _children = []; + + Widget _func (BuildContext context, int index) { + return _children[index]; + } + + @override + Widget build(BuildContext context, Map properties, List children) { + _children = children; + + return WaterfallFlow.builder( + itemBuilder: _func, + padding: EdgeInsets.all(5.0), + gridDelegate: SliverWaterfallFlowDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + crossAxisSpacing: 5.0, + mainAxisSpacing: 5.0, + lastChildLayoutTypeBuilder: (index) => index == children.length + ? LastChildLayoutType.foot + : LastChildLayoutType.none, + ), + ); + } +} class TextWidgetElement extends WidgetElement { - TextWidgetElement(EventTargetContext? context) : + TextWidgetElement(dom.EventTargetContext? context) : super(context); @override - Widget build(BuildContext context, Map properties) { + Widget build(BuildContext context, Map properties, List children) { return Text(properties['value'] ?? '', textDirection: TextDirection.ltr, style: TextStyle(color: Color.fromARGB(255, 100, 100, 100))); } } class ImageWidgetElement extends WidgetElement { - ImageWidgetElement(EventTargetContext? context) : + ImageWidgetElement(dom.EventTargetContext? context) : super(context); @override - Widget build(BuildContext context, Map properties) { + Widget build(BuildContext context, Map properties, List children) { return Image(image: AssetImage(properties['src'])); } } -void defineKrakenCustomElements() { - Kraken.defineCustomElement('sample-element', (EventTargetContext? context) { - return SampleElement(context); - }); - Kraken.defineCustomElement('flutter-text', (EventTargetContext? context) { - return TextWidgetElement(context); - }); - Kraken.defineCustomElement('flutter-asset-image', (EventTargetContext? context) { - return ImageWidgetElement(context); - }); +class ContainerWidgetElement extends WidgetElement { + ContainerWidgetElement(dom.EventTargetContext? context) : + super(context); + + @override + Widget build(BuildContext context, Map properties, List children) { + return Container( + width: 200, + height: 200, + decoration: const BoxDecoration( + border: Border( top: BorderSide( width: 5, color: Colors.red ), bottom: BorderSide( width: 5, color: Colors.red ), left: BorderSide( width: 5, color: Colors.red ), right: BorderSide( width: 5, color: Colors.red )), + ), + child: Column( + children: children, + ), + ); + } } -class SampleElement extends Element { - SampleElement(EventTargetContext? context) +class SampleElement extends dom.Element { + SampleElement(dom.EventTargetContext? context) : super(context); @override @@ -74,4 +111,20 @@ class SampleElement extends Element { } } - +void defineKrakenCustomElements() { + Kraken.defineCustomElement('waterfall-flow', (dom.EventTargetContext? context) { + return WaterfallFlowWidgetElement(context); + }); + Kraken.defineCustomElement('flutter-container', (dom.EventTargetContext? context) { + return ContainerWidgetElement(context); + }); + Kraken.defineCustomElement('sample-element', (dom.EventTargetContext? context) { + return SampleElement(context); + }); + Kraken.defineCustomElement('flutter-text', (dom.EventTargetContext? context) { + return TextWidgetElement(context); + }); + Kraken.defineCustomElement('flutter-asset-image', (dom.EventTargetContext? context) { + return ImageWidgetElement(context); + }); +} diff --git a/integration_tests/pubspec.yaml b/integration_tests/pubspec.yaml index 8f8a195183..bc8898bd43 100644 --- a/integration_tests/pubspec.yaml +++ b/integration_tests/pubspec.yaml @@ -30,6 +30,7 @@ dependencies: kraken_video_player: ^2.0.0-dev.0 kraken_websocket: ^1.0.0 kraken_webview: ^2.0.0-dev.0 + waterfall_flow: ^3.0.1 dev_dependencies: flutter_test: diff --git a/integration_tests/snapshots/dom/elements/custom-element.ts.400d17681.png b/integration_tests/snapshots/dom/elements/custom-element.ts.400d17681.png new file mode 100644 index 0000000000..b77ea8e249 Binary files /dev/null and b/integration_tests/snapshots/dom/elements/custom-element.ts.400d17681.png differ diff --git a/integration_tests/snapshots/dom/elements/custom-element.ts.633308271.png b/integration_tests/snapshots/dom/elements/custom-element.ts.633308271.png new file mode 100644 index 0000000000..00965cc9aa Binary files /dev/null and b/integration_tests/snapshots/dom/elements/custom-element.ts.633308271.png differ diff --git a/integration_tests/snapshots/dom/elements/custom-element.ts.7122b2171.png b/integration_tests/snapshots/dom/elements/custom-element.ts.7122b2171.png new file mode 100644 index 0000000000..f39b9c2de3 Binary files /dev/null and b/integration_tests/snapshots/dom/elements/custom-element.ts.7122b2171.png differ diff --git a/integration_tests/snapshots/dom/elements/custom-element.ts.7253dbaf1.png b/integration_tests/snapshots/dom/elements/custom-element.ts.7253dbaf1.png new file mode 100644 index 0000000000..deaa789156 Binary files /dev/null and b/integration_tests/snapshots/dom/elements/custom-element.ts.7253dbaf1.png differ diff --git a/integration_tests/snapshots/dom/elements/custom-element.ts.9319a30e1.png b/integration_tests/snapshots/dom/elements/custom-element.ts.9319a30e1.png new file mode 100644 index 0000000000..681f03fa2a Binary files /dev/null and b/integration_tests/snapshots/dom/elements/custom-element.ts.9319a30e1.png differ diff --git a/integration_tests/snapshots/dom/elements/custom-element.ts.ae130e951.png b/integration_tests/snapshots/dom/elements/custom-element.ts.ae130e951.png new file mode 100644 index 0000000000..0fb902b0b2 Binary files /dev/null and b/integration_tests/snapshots/dom/elements/custom-element.ts.ae130e951.png differ diff --git a/integration_tests/snapshots/dom/elements/custom-element.ts.ece784ea1.png b/integration_tests/snapshots/dom/elements/custom-element.ts.ece784ea1.png new file mode 100644 index 0000000000..2fa76f7234 Binary files /dev/null and b/integration_tests/snapshots/dom/elements/custom-element.ts.ece784ea1.png differ diff --git a/integration_tests/specs/dom/elements/custom-element.ts b/integration_tests/specs/dom/elements/custom-element.ts index e0144ef591..adc918270a 100644 --- a/integration_tests/specs/dom/elements/custom-element.ts +++ b/integration_tests/specs/dom/elements/custom-element.ts @@ -39,6 +39,107 @@ describe('custom widget element', () => { simulateClick(20, 20); }); + + it('text node should be child of flutter container', async () => { + const container = document.createElement('flutter-container'); + const text = document.createTextNode('text'); + document.body.appendChild(container); + container.appendChild(text); + await snapshot(); + }); + + it('element should be child of flutter container', async () => { + const container = document.createElement('flutter-container'); + const element = document.createElement('div'); + element.style.width = '30px'; + element.style.height = '30px'; + element.style.backgroundColor = 'red'; + container.appendChild(element); + document.body.appendChild(container); + await snapshot(); + }); + + it('flutter widget should be child of flutter container', async () => { + const container = document.createElement('flutter-container'); + const fluttetText = document.createElement('flutter-text'); + fluttetText.setAttribute('value', 'text'); + container.appendChild(fluttetText); + document.body.appendChild(container); + + await snapshot(); + }); + + it('flutter widget and dom node should be child of flutter container', async () => { + const container = document.createElement('flutter-container'); + document.body.appendChild(container); + + const element = document.createElement('div'); + element.style.backgroundColor = 'red'; + element.appendChild(document.createTextNode('div element')); + container.appendChild(element); + + const fluttetText = document.createElement('flutter-text'); + fluttetText.setAttribute('value', 'text'); + container.appendChild(fluttetText); + + const text = document.createTextNode('text'); + container.appendChild(text); + + await snapshot(); + }); + + it('flutter widget should be child of element', async () => { + const container = document.createElement('div'); + container.style.width = '100px'; + container.style.height = '100px'; + container.style.backgroundColor = 'red'; + const element = document.createElement('flutter-text'); + element.setAttribute('value', 'text'); + container.appendChild(element); + document.body.appendChild(container); + + await snapshot(); + }); + + it('flutter widget should be child of element and the element should be child of flutter widget', async () => { + const container = document.createElement('flutter-container'); + document.body.appendChild(container); + + const childContainer = document.createElement('div'); + container.appendChild(childContainer); + + const fluttetText = document.createElement('flutter-text'); + fluttetText.setAttribute('value', 'text'); + childContainer.appendChild(fluttetText); + + await snapshot(); + }); + + it('should work with waterfall-flow', async () => { + const flutterContainer = document.createElement('waterfall-flow'); + flutterContainer.style.height = '100vh'; + flutterContainer.style.display = 'block'; + + document.body.appendChild(flutterContainer); + + const colors = ['red', 'yellow', 'black', 'blue', 'green']; + + for (let i = 0; i < 10; i++) { + const div = document.createElement('div'); + div.style.width = '100%'; + div.style.border = `1px solid ${colors[i % colors.length]}`; + div.appendChild(document.createTextNode(`${i}`)); + + const img = document.createElement('img'); + img.src = 'https://gw.alicdn.com/tfs/TB1CxCYq5_1gK0jSZFqXXcpaXXa-128-90.png'; + div.appendChild(img); + img.style.width = '100px'; + + flutterContainer.appendChild(div); + } + + await snapshot(); + }); }); describe('custom html element', () => { diff --git a/kraken/lib/src/dom/element.dart b/kraken/lib/src/dom/element.dart index b0e135ff23..0eebe70277 100644 --- a/kraken/lib/src/dom/element.dart +++ b/kraken/lib/src/dom/element.dart @@ -15,6 +15,7 @@ import 'package:flutter/scheduler.dart'; import 'package:kraken/css.dart'; import 'package:kraken/dom.dart'; import 'package:kraken/rendering.dart'; +import 'package:kraken/widget.dart'; import 'package:kraken/src/dom/element_event.dart'; import 'package:kraken/src/dom/element_view.dart'; import 'package:meta/meta.dart'; @@ -577,7 +578,10 @@ class Element extends Node } if (renderer != null) { - _attachRenderBoxModel(parent.renderer!, renderer!, after: after); + // If element attach WidgetElement, render obeject should be attach to render tree when mount. + if (parent is! WidgetElement) { + _attachRenderBoxModel(parent.renderer!, renderer!, after: after); + } // Flush pending style before child attached. style.flushPendingProperties(); @@ -636,7 +640,7 @@ class Element extends Node RenderLayoutBox? renderLayoutBox = _renderLayoutBox; if (isRendererAttached) { // Only append child renderer when which is not attached. - if (!child.isRendererAttached && renderLayoutBox != null) { + if (!child.isRendererAttached && renderLayoutBox != null && this is! WidgetElement) { RenderBox? after; RenderLayoutBox? scrollingContentBox = renderLayoutBox.renderScrollingContent; if (scrollingContentBox != null) { @@ -805,7 +809,10 @@ class Element extends Node _updateRenderBoxModel(); // Attach renderBoxModel to parent if change from `display: none` to other values. if (!isRendererAttached && parentElement != null && parentElement!.isRendererAttached) { - _addToContainingBlock(after: previousSibling?.renderer); + // If element attach WidgetElement, render obeject should be attach to render tree when mount. + if (parentNode is! WidgetElement) { + _addToContainingBlock(after: previousSibling?.renderer); + } ensureChildAttached(); } } diff --git a/kraken/lib/src/dom/node.dart b/kraken/lib/src/dom/node.dart index 1af4b86187..6de608c7ab 100644 --- a/kraken/lib/src/dom/node.dart +++ b/kraken/lib/src/dom/node.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:kraken/dom.dart'; import 'package:meta/meta.dart'; +import 'package:kraken/widget.dart'; enum NodeType { ELEMENT_NODE, @@ -71,6 +72,8 @@ abstract class LifecycleCallbacks { } abstract class Node extends EventTarget implements RenderObjectNode, LifecycleCallbacks { + KrakenElementToFlutterElementAdaptor? flutterElement; + KrakenElementToWidgetAdaptor? flutterWidget; List childNodes = []; /// The Node.parentNode read-only property returns the parent of the specified node in the DOM tree. Node? parentNode; diff --git a/kraken/lib/src/launcher/controller.dart b/kraken/lib/src/launcher/controller.dart index 547e34182d..e3250b0bcc 100644 --- a/kraken/lib/src/launcher/controller.dart +++ b/kraken/lib/src/launcher/controller.dart @@ -10,6 +10,7 @@ import 'dart:io'; import 'dart:typed_data'; import 'dart:ui' as ui; +import 'package:flutter/widgets.dart' show RenderObjectElement; import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; @@ -24,6 +25,7 @@ import 'package:kraken/rendering.dart'; import 'package:kraken/widget.dart'; import 'package:kraken/src/dom/element_registry.dart' as element_registry; + import 'bundle.dart'; const int WINDOW_ID = -1; @@ -769,6 +771,8 @@ class KrakenController { UriParser? uriParser; + late RenderObjectElement rootFlutterElement; + static KrakenController? getControllerOfJSContextId(int? contextId) { if (!_controllerMap.containsKey(contextId)) { return null; @@ -820,22 +824,25 @@ class KrakenController { final GestureListener? _gestureListener; - KrakenController(String? name, double viewportWidth, double viewportHeight, - {bool showPerformanceOverlay = false, - enableDebug = false, - Color? background, - GestureListener? gestureListener, - KrakenNavigationDelegate? navigationDelegate, - KrakenMethodChannel? methodChannel, - this.widgetDelegate, - this.bundle, - this.onLoad, - this.onLoadError, - this.onJSError, - this.httpClientInterceptor, - this.devToolsService, - this.uriParser}) - : _name = name, + KrakenController( + String? name, + double viewportWidth, + double viewportHeight, { + bool showPerformanceOverlay = false, + enableDebug = false, + Color? background, + GestureListener? gestureListener, + KrakenNavigationDelegate? navigationDelegate, + KrakenMethodChannel? methodChannel, + this.widgetDelegate, + this.bundle, + this.onLoad, + this.onLoadError, + this.onJSError, + this.httpClientInterceptor, + this.devToolsService, + this.uriParser, + }) : _name = name, _gestureListener = gestureListener { if (kProfileMode) { PerformanceTiming.instance().mark(PERF_CONTROLLER_PROPERTY_INIT); diff --git a/kraken/lib/src/rendering/flow.dart b/kraken/lib/src/rendering/flow.dart index b13b5b3fbb..cb4178b22c 100644 --- a/kraken/lib/src/rendering/flow.dart +++ b/kraken/lib/src/rendering/flow.dart @@ -542,8 +542,11 @@ class RenderFlowLayout extends RenderLayoutBox { childConstraints = child.getConstraints(); } else if (child is RenderTextBox) { childConstraints = child.getConstraints(); - } else { + } else if (child is RenderPositionPlaceholder) { childConstraints = BoxConstraints(); + } else { + // Custom element. + childConstraints = constraints; } // Whether child need to layout @@ -629,8 +632,12 @@ class RenderFlowLayout extends RenderLayoutBox { runMainAxisExtent += childMainAxisExtent; /// Calculate baseline extent of layout box - RenderStyle childRenderStyle = _getChildRenderStyle(child)!; - VerticalAlign verticalAlign = childRenderStyle.verticalAlign; + RenderStyle? childRenderStyle = _getChildRenderStyle(child); + VerticalAlign verticalAlign = VerticalAlign.baseline; + if (childRenderStyle != null) { + verticalAlign = childRenderStyle.verticalAlign; + } + bool isLineHeightValid = _isLineHeightValid(child); diff --git a/kraken/lib/src/rendering/text.dart b/kraken/lib/src/rendering/text.dart index cdbe985b67..c15caba509 100644 --- a/kraken/lib/src/rendering/text.dart +++ b/kraken/lib/src/rendering/text.dart @@ -102,9 +102,9 @@ class RenderTextBox extends RenderBox if (parentRenderBoxModel.isScrollingContentBox) { maxConstraintWidth = parentConstraints.minWidth; } else if (parentConstraints.maxWidth == double.infinity) { - final RenderLayoutParentData parentParentData = parentRenderBoxModel.parentData as RenderLayoutParentData; + final ParentData? parentParentData = parentRenderBoxModel.parentData; // Width of positioned element does not constrained by parent. - if (parentParentData.isPositioned) { + if (parentParentData is RenderLayoutParentData && parentParentData.isPositioned) { maxConstraintWidth = double.infinity; } else { maxConstraintWidth = parentRenderBoxModel.renderStyle.contentMaxConstraintsWidth; diff --git a/kraken/lib/src/widget/element_to_widget_adaptor.dart b/kraken/lib/src/widget/element_to_widget_adaptor.dart new file mode 100644 index 0000000000..0674f4c9d3 --- /dev/null +++ b/kraken/lib/src/widget/element_to_widget_adaptor.dart @@ -0,0 +1,49 @@ +import 'package:flutter/widgets.dart'; +import 'package:kraken/dom.dart' as dom; + +class KrakenElementToWidgetAdaptor extends RenderObjectWidget { + final dom.Node _krakenNode; + + KrakenElementToWidgetAdaptor(this._krakenNode, { Key? key }): super(key: key) { + _krakenNode.flutterWidget = this; + } + + @override + RenderObjectElement createElement() { + _krakenNode.flutterElement = KrakenElementToFlutterElementAdaptor(this); + return _krakenNode.flutterElement as RenderObjectElement; + } + + @override + RenderObject createRenderObject(BuildContext context) { + return _krakenNode.renderer!; + } +} + +class KrakenElementToFlutterElementAdaptor extends RenderObjectElement { + KrakenElementToFlutterElementAdaptor(RenderObjectWidget widget) : super(widget); + + @override + KrakenElementToWidgetAdaptor get widget => super.widget as KrakenElementToWidgetAdaptor; + + @override + void mount(Element? parent, Object? newSlot) { + widget._krakenNode.createRenderer(); + super.mount(parent, newSlot); + + widget._krakenNode.ensureChildAttached(); + + if (widget._krakenNode is dom.Element) { + (widget._krakenNode as dom.Element).style.flushPendingProperties(); + } + } + + @override + void unmount() { + super.unmount(); + (widget._krakenNode as dom.Element).disposeRenderObject(); + } + + @override + void insertRenderObjectChild(RenderObject child, Object? slot) {} +} diff --git a/kraken/lib/src/widget/kraken.dart b/kraken/lib/src/widget/kraken.dart new file mode 100644 index 0000000000..3e4cecf97b --- /dev/null +++ b/kraken/lib/src/widget/kraken.dart @@ -0,0 +1,955 @@ +/* + * Copyright (C) 2019-present Alibaba Inc. All rights reserved. + * Author: Kraken Team. + */ +import 'dart:io'; +import 'dart:ui'; +import 'dart:typed_data'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; +import 'package:kraken/kraken.dart'; +import 'package:kraken/rendering.dart'; +import 'package:kraken/dom.dart' as dom; +import 'package:kraken/module.dart'; +import 'package:kraken/gesture.dart'; +import 'package:kraken/css.dart'; +import 'package:kraken/src/dom/element_registry.dart'; + +/// Get context of current widget. +typedef GetContext = BuildContext Function(); +/// Request focus of current widget. +typedef RequestFocus = void Function(); +/// Get the target platform. +typedef GetTargetPlatform = TargetPlatform Function(); +/// Get the cursor color according to the widget theme and platform theme. +typedef GetCursorColor = Color Function(); +/// Get the selection color according to the widget theme and platform theme. +typedef GetSelectionColor = Color Function(); +/// Get the cursor radius according to the target platform. +typedef GetCursorRadius = Radius Function(); +/// Get the text selection controls according to the target platform. +typedef GetTextSelectionControls = TextSelectionControls Function(); + +/// Delegate methods of widget +class WidgetDelegate { + GetContext getContext; + RequestFocus requestFocus; + GetTargetPlatform getTargetPlatform; + GetCursorColor getCursorColor; + GetSelectionColor getSelectionColor; + GetCursorRadius getCursorRadius; + GetTextSelectionControls getTextSelectionControls; + + WidgetDelegate( + this.getContext, + this.requestFocus, + this.getTargetPlatform, + this.getCursorColor, + this.getSelectionColor, + this.getCursorRadius, + this.getTextSelectionControls, + ); +} + +class Kraken extends StatefulWidget { + // The background color for viewport, default to transparent. + final Color? background; + + // the width of krakenWidget + final double? viewportWidth; + + // the height of krakenWidget + final double? viewportHeight; + + // The initial bundle to load. + final KrakenBundle? bundle; + + // The animationController of Flutter Route object. + // Pass this object to KrakenWidget to make sure Kraken execute JavaScripts scripts after route transition animation completed. + final AnimationController? animationController; + + // The methods of the KrakenNavigateDelegation help you implement custom behaviors that are triggered + // during a kraken view's process of loading, and completing a navigation request. + final KrakenNavigationDelegate? navigationDelegate; + + // A method channel for receiving messaged from JavaScript code and sending message to JavaScript. + final KrakenMethodChannel? javaScriptChannel; + + // Register the RouteObserver to observer page navigation. + // This is useful if you wants to pause kraken timers and callbacks when kraken widget are hidden by page route. + // https://api.flutter.dev/flutter/widgets/RouteObserver-class.html + final RouteObserver>? routeObserver; + + final LoadErrorHandler? onLoadError; + + final LoadHandler? onLoad; + + final JSErrorHandler ?onJSError; + + // Open a service to support Chrome DevTools for debugging. + // https://github.com/openkraken/devtools + final DevToolsService? devToolsService; + + final GestureListener? gestureListener; + + final HttpClientInterceptor? httpClientInterceptor; + + final UriParser? uriParser; + + KrakenController? get controller { + return KrakenController.getControllerOfName(shortHash(this)); + } + + // Set kraken http cache mode. + static void setHttpCacheMode(HttpCacheMode mode) { + HttpCacheController.mode = mode; + if (kDebugMode) { + print('Kraken http cache mode set to $mode.'); + } + } + + static bool _isValidCustomElementName(localName) { + return RegExp(r'^[a-z][.0-9_a-z]*-[\-.0-9_a-z]*$').hasMatch(localName); + } + + static void defineCustomElement(String tagName, ElementCreator creator) { + if (!_isValidCustomElementName(tagName)) { + throw ArgumentError('The element name "$tagName" is not valid.'); + } + defineElement(tagName.toUpperCase(), creator); + } + + loadBundle(KrakenBundle bundle) async { + await controller!.unload(); + await controller!.loadBundle( + bundle: bundle + ); + _evalBundle(controller!, animationController); + } + + @deprecated + loadContent(String bundleContent) async { + await controller!.unload(); + await controller!.loadBundle( + bundle: KrakenBundle.fromContent(bundleContent) + ); + _evalBundle(controller!, animationController); + } + + @deprecated + loadByteCode(Uint8List bundleByteCode) async { + await controller!.unload(); + await controller!.loadBundle( + bundle: KrakenBundle.fromBytecode(bundleByteCode) + ); + _evalBundle(controller!, animationController); + } + + @deprecated + loadURL(String bundleURL, { String? bundleContent, Uint8List? bundleByteCode }) async { + await controller!.unload(); + + KrakenBundle bundle; + if (bundleByteCode != null) { + bundle = KrakenBundle.fromBytecode(bundleByteCode, url: bundleURL); + } else if (bundleContent != null) { + bundle = KrakenBundle.fromContent(bundleContent, url: bundleURL); + } else { + bundle = KrakenBundle.fromUrl(bundleURL); + } + + await controller!.loadBundle( + bundle: bundle + ); + _evalBundle(controller!, animationController); + } + + @deprecated + loadPath(String bundlePath, { String? bundleContent, Uint8List? bundleByteCode }) async { + await controller!.unload(); + + KrakenBundle bundle; + if (bundleByteCode != null) { + bundle = KrakenBundle.fromBytecode(bundleByteCode, url: bundlePath); + } else if (bundleContent != null) { + bundle = KrakenBundle.fromContent(bundleContent, url: bundlePath); + } else { + bundle = KrakenBundle.fromUrl(bundlePath); + } + + await controller!.loadBundle( + bundle: bundle + ); + _evalBundle(controller!, animationController); + } + + reload() async { + await controller!.reload(); + } + + Kraken({ + Key? key, + this.viewportWidth, + this.viewportHeight, + this.bundle, + this.onLoad, + this.navigationDelegate, + this.javaScriptChannel, + this.background, + this.gestureListener, + this.devToolsService, + // Kraken's http client interceptor. + this.httpClientInterceptor, + this.uriParser, + this.routeObserver, + // Kraken's viewportWidth options only works fine when viewportWidth is equal to window.physicalSize.width / window.devicePixelRatio. + // Maybe got unexpected error when change to other values, use this at your own risk! + // We will fixed this on next version released. (v0.6.0) + // Disable viewportWidth check and no assertion error report. + bool disableViewportWidthAssertion = false, + // Kraken's viewportHeight options only works fine when viewportHeight is equal to window.physicalSize.height / window.devicePixelRatio. + // Maybe got unexpected error when change to other values, use this at your own risk! + // We will fixed this on next version release. (v0.6.0) + // Disable viewportHeight check and no assertion error report. + bool disableViewportHeightAssertion = false, + // Callback functions when loading Javascript scripts failed. + this.onLoadError, + this.animationController, + this.onJSError + }) : super(key: key); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('viewportWidth', viewportWidth)); + properties.add(DiagnosticsProperty('viewportHeight', viewportHeight)); + } + + @override + _KrakenState createState() => _KrakenState(); + +} +class _KrakenState extends State with RouteAware { + Map>? _actionMap; + + final FocusNode _focusNode = FocusNode(); + + @override + void initState() { + super.initState(); + _actionMap = >{ + // Action of focus. + NextFocusIntent: CallbackAction(onInvoke: _handleNextFocus), + PreviousFocusIntent: CallbackAction(onInvoke: _handlePreviousFocus), + + // Action of mouse move hotkeys. + MoveSelectionRightByLineTextIntent: CallbackAction(onInvoke: _handleMoveSelectionRightByLineText), + MoveSelectionLeftByLineTextIntent: CallbackAction(onInvoke: _handleMoveSelectionLeftByLineText), + MoveSelectionRightByWordTextIntent: CallbackAction(onInvoke: _handleMoveSelectionRightByWordText), + MoveSelectionLeftByWordTextIntent: CallbackAction(onInvoke: _handleMoveSelectionLeftByWordText), + MoveSelectionUpTextIntent: CallbackAction(onInvoke: _handleMoveSelectionUpText), + MoveSelectionDownTextIntent: CallbackAction(onInvoke: _handleMoveSelectionDownText), + MoveSelectionLeftTextIntent: CallbackAction(onInvoke: _handleMoveSelectionLeftText), + MoveSelectionRightTextIntent: CallbackAction(onInvoke: _handleMoveSelectionRightText), + MoveSelectionToStartTextIntent: CallbackAction(onInvoke: _handleMoveSelectionToStartText), + MoveSelectionToEndTextIntent: CallbackAction(onInvoke: _handleMoveSelectionToEndText), + + // Action of selection hotkeys. + ExtendSelectionLeftTextIntent: CallbackAction(onInvoke: _handleExtendSelectionLeftText), + ExtendSelectionRightTextIntent: CallbackAction(onInvoke: _handleExtendSelectionRightText), + ExtendSelectionUpTextIntent: CallbackAction(onInvoke: _handleExtendSelectionUpText), + ExtendSelectionDownTextIntent: CallbackAction(onInvoke: _handleExtendSelectionDownText), + ExpandSelectionToEndTextIntent: CallbackAction(onInvoke: _handleExtendSelectionToEndText), + ExpandSelectionToStartTextIntent: CallbackAction(onInvoke: _handleExtendSelectionToStartText), + ExpandSelectionLeftByLineTextIntent: CallbackAction(onInvoke: _handleExtendSelectionLeftByLineText), + ExpandSelectionRightByLineTextIntent: CallbackAction(onInvoke: _handleExtendSelectionRightByLineText), + ExtendSelectionLeftByWordTextIntent: CallbackAction(onInvoke: _handleExtendSelectionLeftByWordText), + ExtendSelectionRightByWordTextIntent: CallbackAction(onInvoke: _handleExtendSelectionRightByWordText), + }; + } + + @override + Widget build(BuildContext context) { + return RepaintBoundary( + child: FocusableActionDetector( + actions: _actionMap, + focusNode: _focusNode, + onFocusChange: _handleFocusChange, + child: _KrakenRenderObjectWidget( + context.widget as Kraken, + widgetDelegate, + ) + ) + ); + } + + WidgetDelegate get widgetDelegate { + return WidgetDelegate( + _getContext, + _requestFocus, + _getTargetPlatform, + _getCursorColor, + _getSelectionColor, + _getCursorRadius, + _getTextSelectionControls, + ); + } + + // Get context of current widget. + BuildContext _getContext() { + return context; + } + + // Request focus of current widget. + void _requestFocus() { + _focusNode.requestFocus(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (widget.routeObserver != null) { + widget.routeObserver!.subscribe(this, ModalRoute.of(context)!); + } + } + + // Resume call timer and callbacks when kraken widget change to visible. + @override + void didPopNext() { + assert(widget.controller != null); + widget.controller!.resume(); + } + + // Pause all timer and callbacks when kraken widget has been invisible. + @override + void didPushNext() { + assert(widget.controller != null); + widget.controller!.pause(); + } + + @override + void dispose() { + if (widget.routeObserver != null) { + widget.routeObserver!.unsubscribe(this); + } + super.dispose(); + } + + + // Get the target platform. + TargetPlatform _getTargetPlatform() { + final ThemeData theme = Theme.of(context); + return theme.platform; + } + + // Get the cursor color according to the widget theme and platform theme. + Color _getCursorColor() { + Color cursorColor = CSSColor.initial; + TextSelectionThemeData selectionTheme = TextSelectionTheme.of(context); + ThemeData theme = Theme.of(context); + + switch (theme.platform) { + case TargetPlatform.iOS: + final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context); + cursorColor = selectionTheme.cursorColor ?? cupertinoTheme.primaryColor; + break; + + case TargetPlatform.macOS: + final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context); + cursorColor = selectionTheme.cursorColor ?? cupertinoTheme.primaryColor; + break; + + case TargetPlatform.android: + case TargetPlatform.fuchsia: + cursorColor = selectionTheme.cursorColor ?? theme.colorScheme.primary; + break; + + case TargetPlatform.linux: + case TargetPlatform.windows: + cursorColor = selectionTheme.cursorColor ?? theme.colorScheme.primary; + break; + } + + return cursorColor; + } + + // Get the selection color according to the widget theme and platform theme. + Color _getSelectionColor() { + Color selectionColor = CSSColor.initial.withOpacity(0.4); + TextSelectionThemeData selectionTheme = TextSelectionTheme.of(context); + ThemeData theme = Theme.of(context); + + switch (theme.platform) { + case TargetPlatform.iOS: + final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context); + selectionColor = selectionTheme.selectionColor ?? cupertinoTheme.primaryColor.withOpacity(0.40); + break; + + case TargetPlatform.macOS: + final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context); + selectionColor = selectionTheme.selectionColor ?? cupertinoTheme.primaryColor.withOpacity(0.40); + break; + + case TargetPlatform.android: + case TargetPlatform.fuchsia: + selectionColor = selectionTheme.selectionColor ?? theme.colorScheme.primary.withOpacity(0.40); + break; + + case TargetPlatform.linux: + case TargetPlatform.windows: + selectionColor = selectionTheme.selectionColor ?? theme.colorScheme.primary.withOpacity(0.40); + break; + } + + return selectionColor; + } + + // Get the cursor radius according to the target platform. + Radius _getCursorRadius() { + Radius cursorRadius = const Radius.circular(2.0); + TargetPlatform platform = _getTargetPlatform(); + + switch (platform) { + case TargetPlatform.iOS: + cursorRadius = const Radius.circular(2.0); + break; + + case TargetPlatform.macOS: + cursorRadius = const Radius.circular(2.0); + break; + + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + break; + } + + return cursorRadius; + } + + // Get the text selection controls according to the target platform. + TextSelectionControls _getTextSelectionControls() { + TextSelectionControls _selectionControls; + TargetPlatform platform = _getTargetPlatform(); + + switch (platform) { + case TargetPlatform.iOS: + _selectionControls = cupertinoTextSelectionControls; + break; + + case TargetPlatform.macOS: + _selectionControls = cupertinoDesktopTextSelectionControls; + break; + + case TargetPlatform.android: + case TargetPlatform.fuchsia: + _selectionControls = materialTextSelectionControls; + break; + + case TargetPlatform.linux: + case TargetPlatform.windows: + _selectionControls = desktopTextSelectionControls; + break; + } + + return _selectionControls; + } + + // Handle focus change of focusNode. + void _handleFocusChange(bool focused) { + dom.Element rootElement = _findRootElement(); + List focusableElements = _findFocusableElements(rootElement); + if (focusableElements.isNotEmpty) { + dom.Element? focusedElement = _findFocusedElement(focusableElements); + // Currently only input element is focusable. + if (focused) { + if (dom.InputElement.focusInputElement == null) { + (focusableElements[0] as dom.InputElement).focus(); + } + } else { + if (focusedElement != null) { + (focusedElement as dom.InputElement).blur(); + } + } + } + } + + // Handle focus action usually by pressing the [Tab] hotkey. + void _handleNextFocus(NextFocusIntent intent) { + dom.Element rootElement = _findRootElement(); + List focusableElements = _findFocusableElements(rootElement); + if (focusableElements.isNotEmpty) { + dom.Element? focusedElement = _findFocusedElement(focusableElements); + // None focusable element is focused, focus the first focusable element. + if (focusedElement == null) { + _focusNode.requestFocus(); + (focusableElements[0] as dom.InputElement).focus(); + + // Some focusable element is focused, focus the next element, if it is the last focusable element, + // then focus the next widget. + } else { + int idx = focusableElements.indexOf(focusedElement); + if (idx == focusableElements.length - 1) { + _focusNode.nextFocus(); + (focusableElements[focusableElements.length - 1] as dom.InputElement).blur(); + } else { + _focusNode.requestFocus(); + (focusableElements[idx] as dom.InputElement).blur(); + (focusableElements[idx + 1] as dom.InputElement).focus(); + } + } + + // None focusable element exists, focus the next widget. + } else { + _focusNode.nextFocus(); + } + } + + // Handle focus action usually by pressing the [Shift]+[Tab] hotkey in the reverse direction. + void _handlePreviousFocus(PreviousFocusIntent intent) { + dom.Element rootElement = _findRootElement(); + List focusableElements = _findFocusableElements(rootElement); + if (focusableElements.isNotEmpty) { + dom.Element? focusedElement = _findFocusedElement(focusableElements); + // None editable is focused, focus the last editable. + if (focusedElement == null) { + _focusNode.requestFocus(); + (focusableElements[focusableElements.length - 1] as dom.InputElement).focus(); + + // Some editable is focused, focus the previous editable, if it is the first editable, + // then focus the previous widget. + } else { + int idx = focusableElements.indexOf(focusedElement); + if (idx == 0) { + _focusNode.previousFocus(); + (focusableElements[0] as dom.InputElement).blur(); + } else { + _focusNode.requestFocus(); + (focusableElements[idx] as dom.InputElement).blur(); + (focusableElements[idx - 1] as dom.InputElement).focus(); + } + } + // None editable exists, focus the previous widget. + } else { + _focusNode.previousFocus(); + } + } + + void _handleMoveSelectionRightByLineText(MoveSelectionRightByLineTextIntent intent) { + dom.Element? focusedElement = _findFocusedElement(); + if (focusedElement != null) { + RenderEditable? focusedRenderEditable = (focusedElement as dom.InputElement).renderEditable; + if (focusedRenderEditable != null) { + focusedRenderEditable.moveSelectionRightByLine(SelectionChangedCause.keyboard); + // Make caret visible while moving cursor. + focusedElement.scrollToCaret(); + } + } + } + + void _handleMoveSelectionLeftByLineText(MoveSelectionLeftByLineTextIntent intent) { + dom.Element? focusedElement = _findFocusedElement(); + if (focusedElement != null) { + RenderEditable? focusedRenderEditable = (focusedElement as dom.InputElement).renderEditable; + if (focusedRenderEditable != null) { + focusedRenderEditable.moveSelectionLeftByLine(SelectionChangedCause.keyboard); + // Make caret visible while moving cursor. + focusedElement.scrollToCaret(); + } + } + } + + void _handleMoveSelectionRightByWordText(MoveSelectionRightByWordTextIntent intent) { + dom.Element? focusedElement = _findFocusedElement(); + if (focusedElement != null) { + RenderEditable? focusedRenderEditable = (focusedElement as dom.InputElement).renderEditable; + if (focusedRenderEditable != null) { + focusedRenderEditable.moveSelectionRightByWord(SelectionChangedCause.keyboard); + // Make caret visible while moving cursor. + focusedElement.scrollToCaret(); + } + } + } + + void _handleMoveSelectionLeftByWordText(MoveSelectionLeftByWordTextIntent intent) { + dom.Element? focusedElement = _findFocusedElement(); + if (focusedElement != null) { + RenderEditable? focusedRenderEditable = (focusedElement as dom.InputElement).renderEditable; + if (focusedRenderEditable != null) { + focusedRenderEditable.moveSelectionLeftByWord(SelectionChangedCause.keyboard); + // Make caret visible while moving cursor. + focusedElement.scrollToCaret(); + } + } + } + + void _handleMoveSelectionUpText(MoveSelectionUpTextIntent intent) { + dom.Element? focusedElement = _findFocusedElement(); + if (focusedElement != null) { + RenderEditable? focusedRenderEditable = (focusedElement as dom.InputElement).renderEditable; + if (focusedRenderEditable != null) { + focusedRenderEditable.moveSelectionUp(SelectionChangedCause.keyboard); + // Make caret visible while moving cursor. + focusedElement.scrollToCaret(); + } + } + } + + void _handleMoveSelectionDownText(MoveSelectionDownTextIntent intent) { + dom.Element? focusedElement = _findFocusedElement(); + if (focusedElement != null) { + RenderEditable? focusedRenderEditable = (focusedElement as dom.InputElement).renderEditable; + if (focusedRenderEditable != null) { + focusedRenderEditable.moveSelectionDown(SelectionChangedCause.keyboard); + // Make caret visible while moving cursor. + focusedElement.scrollToCaret(); + } + } + } + + void _handleMoveSelectionLeftText(MoveSelectionLeftTextIntent intent) { + dom.Element? focusedElement = _findFocusedElement(); + if (focusedElement != null) { + RenderEditable? focusedRenderEditable = (focusedElement as dom.InputElement).renderEditable; + if (focusedRenderEditable != null) { + focusedRenderEditable.moveSelectionLeft(SelectionChangedCause.keyboard); + // Make caret visible while moving cursor. + focusedElement.scrollToCaret(); + } + } + } + + void _handleMoveSelectionRightText(MoveSelectionRightTextIntent intent) { + dom.Element? focusedElement = _findFocusedElement(); + if (focusedElement != null) { + RenderEditable? focusedRenderEditable = (focusedElement as dom.InputElement).renderEditable; + if (focusedRenderEditable != null) { + focusedRenderEditable.moveSelectionRight(SelectionChangedCause.keyboard); + // Make caret visible while moving cursor. + focusedElement.scrollToCaret(); + } + } + } + + void _handleMoveSelectionToEndText(MoveSelectionToEndTextIntent intent) { + dom.Element? focusedElement = _findFocusedElement(); + if (focusedElement != null) { + RenderEditable? focusedRenderEditable = (focusedElement as dom.InputElement).renderEditable; + if (focusedRenderEditable != null) { + focusedRenderEditable.moveSelectionToEnd(SelectionChangedCause.keyboard); + // Make caret visible while moving cursor. + focusedElement.scrollToCaret(); + } + } + } + + void _handleMoveSelectionToStartText(MoveSelectionToStartTextIntent intent) { + dom.Element? focusedElement = _findFocusedElement(); + if (focusedElement != null) { + RenderEditable? focusedRenderEditable = (focusedElement as dom.InputElement).renderEditable; + if (focusedRenderEditable != null) { + focusedRenderEditable.moveSelectionToStart(SelectionChangedCause.keyboard); + // Make caret visible while moving cursor. + focusedElement.scrollToCaret(); + } + } + } + + void _handleExtendSelectionLeftText(ExtendSelectionLeftTextIntent intent) { + dom.Element? focusedElement = _findFocusedElement(); + if (focusedElement != null) { + RenderEditable? focusedRenderEditable = (focusedElement as dom.InputElement).renderEditable; + if (focusedRenderEditable != null) { + focusedRenderEditable.extendSelectionLeft(SelectionChangedCause.keyboard); + } + } + } + + void _handleExtendSelectionRightText(ExtendSelectionRightTextIntent intent) { + dom.Element? focusedElement = _findFocusedElement(); + if (focusedElement != null) { + RenderEditable? focusedRenderEditable = (focusedElement as dom.InputElement).renderEditable; + if (focusedRenderEditable != null) { + focusedRenderEditable.extendSelectionRight(SelectionChangedCause.keyboard); + } + } + } + + void _handleExtendSelectionUpText(ExtendSelectionUpTextIntent intent) { + dom.Element? focusedElement = _findFocusedElement(); + if (focusedElement != null) { + RenderEditable? focusedRenderEditable = (focusedElement as dom.InputElement).renderEditable; + if (focusedRenderEditable != null) { + focusedRenderEditable.extendSelectionUp(SelectionChangedCause.keyboard); + } + } + } + + void _handleExtendSelectionDownText(ExtendSelectionDownTextIntent intent) { + dom.Element? focusedElement = _findFocusedElement(); + if (focusedElement != null) { + RenderEditable? focusedRenderEditable = (focusedElement as dom.InputElement).renderEditable; + if (focusedRenderEditable != null) { + focusedRenderEditable.extendSelectionDown(SelectionChangedCause.keyboard); + } + } + } + + void _handleExtendSelectionToEndText(ExpandSelectionToEndTextIntent intent) { + dom.Element? focusedElement = _findFocusedElement(); + if (focusedElement != null) { + RenderEditable? focusedRenderEditable = (focusedElement as dom.InputElement).renderEditable; + if (focusedRenderEditable != null) { + focusedRenderEditable.expandSelectionToEnd(SelectionChangedCause.keyboard); + } + } + } + + void _handleExtendSelectionToStartText(ExpandSelectionToStartTextIntent intent) { + dom.Element? focusedElement = _findFocusedElement(); + if (focusedElement != null) { + RenderEditable? focusedRenderEditable = (focusedElement as dom.InputElement).renderEditable; + if (focusedRenderEditable != null) { + focusedRenderEditable.expandSelectionToStart(SelectionChangedCause.keyboard); + } + } + } + + void _handleExtendSelectionLeftByLineText(ExpandSelectionLeftByLineTextIntent intent) { + dom.Element? focusedElement = _findFocusedElement(); + if (focusedElement != null) { + RenderEditable? focusedRenderEditable = (focusedElement as dom.InputElement).renderEditable; + if (focusedRenderEditable != null) { + focusedRenderEditable.expandSelectionLeftByLine(SelectionChangedCause.keyboard); + } + } + } + + void _handleExtendSelectionRightByLineText(ExpandSelectionRightByLineTextIntent intent) { + dom.Element? focusedElement = _findFocusedElement(); + if (focusedElement != null) { + RenderEditable? focusedRenderEditable = (focusedElement as dom.InputElement).renderEditable; + if (focusedRenderEditable != null) { + focusedRenderEditable.expandSelectionRightByLine(SelectionChangedCause.keyboard); + } + } + } + + void _handleExtendSelectionLeftByWordText(ExtendSelectionLeftByWordTextIntent intent) { + dom.Element? focusedElement = _findFocusedElement(); + if (focusedElement != null) { + RenderEditable? focusedRenderEditable = (focusedElement as dom.InputElement).renderEditable; + if (focusedRenderEditable != null) { + focusedRenderEditable.extendSelectionLeftByWord(SelectionChangedCause.keyboard); + } + } + } + + void _handleExtendSelectionRightByWordText(ExtendSelectionRightByWordTextIntent intent) { + dom.Element? focusedElement = _findFocusedElement(); + if (focusedElement != null) { + RenderEditable? focusedRenderEditable = (focusedElement as dom.InputElement).renderEditable; + if (focusedRenderEditable != null) { + focusedRenderEditable.extendSelectionRightByWord(SelectionChangedCause.keyboard); + } + } + } + + // Find RenderViewportBox in the renderObject tree. + RenderViewportBox? _findRenderViewportBox(RenderObject parent) { + RenderViewportBox? result; + parent.visitChildren((RenderObject child) { + if (child is RenderViewportBox) { + result = child; + } else { + result = _findRenderViewportBox(child); + } + }); + return result; + } + + // Find root element of dom tree. + dom.Element _findRootElement() { + RenderObject? _rootRenderObject = context.findRenderObject(); + RenderViewportBox? renderViewportBox = _findRenderViewportBox(_rootRenderObject!); + KrakenController controller = (renderViewportBox as RenderObjectWithControllerMixin).controller!; + dom.Element documentElement = controller.view.document.documentElement!; + return documentElement; + } + + // Find all the focusable elements in the element tree. + List _findFocusableElements(dom.Element element) { + List result = []; + traverseElement(element, (dom.Element child) { + // Currently only input element is focusable. + if (child is dom.InputElement) { + result.add(child); + } + }); + return result; + } + + // Find the focused element in the element tree. + dom.Element? _findFocusedElement([List? focusableElements]) { + dom.Element? result; + if (focusableElements == null) { + dom.Element rootElement = _findRootElement(); + focusableElements = _findFocusableElements(rootElement); + } + + if (focusableElements.isNotEmpty) { + // Currently only input element is focusable. + for (dom.Element inputElement in focusableElements) { + RenderEditable? renderEditable = (inputElement as dom.InputElement).renderEditable; + if (renderEditable != null && renderEditable.hasFocus) { + result = inputElement; + break; + } + } + } + return result; + } +} + +class _KrakenRenderObjectWidget extends SingleChildRenderObjectWidget { + // Creates a widget that visually hides its child. + const _KrakenRenderObjectWidget( + Kraken widget, + WidgetDelegate widgetDelegate, + {Key? key} + ) : _krakenWidget = widget, + _widgetDelegate = widgetDelegate, + super(key: key); + + final Kraken _krakenWidget; + final WidgetDelegate _widgetDelegate; + + @override + RenderObject createRenderObject(BuildContext context) { + if (kProfileMode) { + PerformanceTiming.instance().mark(PERF_CONTROLLER_INIT_START); + } + + double viewportWidth = _krakenWidget.viewportWidth ?? window.physicalSize.width / window.devicePixelRatio; + double viewportHeight = _krakenWidget.viewportHeight ?? window.physicalSize.height / window.devicePixelRatio; + + if (viewportWidth == 0.0 && viewportHeight == 0.0) { + throw FlutterError('''Can't get viewportSize from window. Please set viewportWidth and viewportHeight manually. +This situation often happened when you trying creating kraken when FlutterView not initialized.'''); + } + + KrakenController controller = KrakenController( + shortHash(_krakenWidget.hashCode), + viewportWidth, + viewportHeight, + background: _krakenWidget.background, + showPerformanceOverlay: Platform.environment[ENABLE_PERFORMANCE_OVERLAY] != null, + bundle: _krakenWidget.bundle, + onLoad: _krakenWidget.onLoad, + onLoadError: _krakenWidget.onLoadError, + onJSError: _krakenWidget.onJSError, + methodChannel: _krakenWidget.javaScriptChannel, + gestureListener: _krakenWidget.gestureListener, + navigationDelegate: _krakenWidget.navigationDelegate, + devToolsService: _krakenWidget.devToolsService, + httpClientInterceptor: _krakenWidget.httpClientInterceptor, + widgetDelegate: _widgetDelegate, + uriParser: _krakenWidget.uriParser + ); + + if (kProfileMode) { + PerformanceTiming.instance().mark(PERF_CONTROLLER_INIT_END); + } + + return controller.view.getRootRenderObject(); + } + + @override + void updateRenderObject(BuildContext context, covariant RenderObject renderObject) { + super.updateRenderObject(context, renderObject); + KrakenController controller = (renderObject as RenderObjectWithControllerMixin).controller!; + controller.name = shortHash(_krakenWidget.hashCode); + + bool viewportWidthHasChanged = controller.view.viewportWidth != _krakenWidget.viewportWidth; + bool viewportHeightHasChanged = controller.view.viewportHeight != _krakenWidget.viewportHeight; + + double viewportWidth = _krakenWidget.viewportWidth ?? window.physicalSize.width / window.devicePixelRatio; + double viewportHeight = _krakenWidget.viewportHeight ?? window.physicalSize.height / window.devicePixelRatio; + + if (controller.view.document.documentElement == null) return; + + if (viewportWidthHasChanged) { + controller.view.viewportWidth = viewportWidth; + controller.view.document.documentElement!.renderStyle.width = CSSLengthValue(viewportWidth, CSSLengthType.PX); + } + + if (viewportHeightHasChanged) { + controller.view.viewportHeight = viewportHeight; + controller.view.document.documentElement!.renderStyle.height = CSSLengthValue(viewportHeight, CSSLengthType.PX); + } + } + + @override + void didUnmountRenderObject(covariant RenderObject renderObject) { + KrakenController controller = (renderObject as RenderObjectWithControllerMixin).controller!; + controller.dispose(); + } + + @override + _KrakenRenderObjectElement createElement() { + return _KrakenRenderObjectElement(this); + } +} + +class _KrakenRenderObjectElement extends SingleChildRenderObjectElement { + _KrakenRenderObjectElement(_KrakenRenderObjectWidget widget) : super(widget); + + @override + void mount(Element? parent, Object? newSlot) async { + super.mount(parent, newSlot); + + KrakenController controller = (renderObject as RenderObjectWithControllerMixin).controller!; + + // We should make sure every flutter elements created under kraken can be walk up to the root. + // So we bind _KrakenRenderObjectElement into KrakenController, and widgetElements created by controller can follow this to the root. + controller.rootFlutterElement = this; + + if (controller.bundle == null || (controller.bundle?.content == null && controller.bundle?.bytecode == null && controller.bundle?.src == null)) { + return; + } + + await controller.loadBundle(); + + _evalBundle(controller, widget._krakenWidget.animationController); + } + + // RenderObjects created by kraken are manager by kraken itself. There are no needs to operate renderObjects on _KrakenRenderObjectElement. + @override + void insertRenderObjectChild(RenderObject child, Object? slot) {} + @override + void moveRenderObjectChild(RenderObject child, Object? oldSlot, Object? newSlot) {} + @override + void removeRenderObjectChild(RenderObject child, Object? slot) {} + + @override + _KrakenRenderObjectWidget get widget => super.widget as _KrakenRenderObjectWidget; +} + +void _evalBundle(KrakenController controller, AnimationController? animationController) async { + // Execute JavaScript scripts will block the Flutter UI Threads. + // Listen for animationController listener to make sure to execute Javascript after route transition had completed. + if (animationController != null) { + animationController.addStatusListener((AnimationStatus status) { + if (status == AnimationStatus.completed) { + controller.evalBundle(); + } + }); + } else { + await controller.evalBundle(); + } +} diff --git a/kraken/lib/src/widget/widget_to_element_adaptor.dart b/kraken/lib/src/widget/widget_to_element_adaptor.dart new file mode 100644 index 0000000000..620a3b9073 --- /dev/null +++ b/kraken/lib/src/widget/widget_to_element_adaptor.dart @@ -0,0 +1,323 @@ +/* + * Copyright (C) 2019-present Alibaba Inc. All rights reserved. + * Author: Kraken Team. + */ + +import 'package:flutter/foundation.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:kraken/kraken.dart'; +import 'package:kraken/dom.dart' as dom; +import 'package:kraken/css.dart'; + +import 'element_to_widget_adaptor.dart'; + +const Map _defaultStyle = { + DISPLAY: INLINE_BLOCK, + POSITION: RELATIVE +}; + +class KrakenRenderObjectToWidgetAdapter extends RenderObjectWidget { + /// Creates a bridge from a [RenderObject] to an [Element] tree. + /// + /// Used by [WidgetsBinding] to attach the root widget to the [RenderView]. + KrakenRenderObjectToWidgetAdapter({ + this.child, + required this.container, + this.debugShortDescription, + }) : super(key: GlobalObjectKey(container)); + + /// The widget below this widget in the tree. + /// + /// {@macro flutter.widgets.ProxyWidget.child} + final Widget? child; + + /// The [RenderObject] that is the parent of the [Element] created by this widget. + final ContainerRenderObjectMixin> container; + + /// A short description of this widget used by debugging aids. + final String? debugShortDescription; + + @override + KrakenRenderObjectToWidgetElement createElement() => KrakenRenderObjectToWidgetElement(this); + + @override + ContainerRenderObjectMixin> createRenderObject(BuildContext context) => container; + + @override + void updateRenderObject(BuildContext context, RenderObject renderObject) { } + + /// Inflate this widget and actually set the resulting [RenderObject] as the + /// child of [container]. + KrakenRenderObjectToWidgetElement attachToRenderTree(BuildOwner owner, RenderObjectElement parentElement, bool needBuild) { + Element? element; + + owner.lockState(() { + element = createElement(); + assert(element != null); + }); + + // If renderview is building,skip the buildScope phase. + if (!needBuild) { + if (element != null) { + element?.mount(parentElement, null); + } + } else { + owner.buildScope(element!, () { + if (element != null) { + element?.mount(parentElement, null); + } + }); + } + + return element! as KrakenRenderObjectToWidgetElement; + } + + @override + String toStringShort() => debugShortDescription ?? super.toStringShort(); +} + +class KrakenRenderObjectToWidgetElement extends RenderObjectElement { + /// Creates an element that is hosted by a [RenderObject]. + /// + /// The [RenderObject] created by this element is not automatically set as a + /// child of the hosting [RenderObject]. To actually attach this element to + /// the render tree, call [RenderObjectToWidgetAdapter.attachToRenderTree]. + KrakenRenderObjectToWidgetElement(KrakenRenderObjectToWidgetAdapter widget) : super(widget); + + @override + KrakenRenderObjectToWidgetAdapter get widget => super.widget as KrakenRenderObjectToWidgetAdapter; + + Element? _child; + + static const Object _rootChildSlot = Object(); + + @override + void visitChildren(ElementVisitor visitor) { + if (_child != null) + visitor(_child!); + } + + @override + void forgetChild(Element child) { + assert(child == _child); + _child = null; + super.forgetChild(child); + } + + @override + void mount(Element? parent, Object? newSlot) { + super.mount(parent, newSlot); + _rebuild(); + assert(_child != null); + } + + @override + void update(RenderObjectToWidgetAdapter newWidget) { + super.update(newWidget); + assert(widget == newWidget); + _rebuild(); + } + + // When we are assigned a new widget, we store it here + // until we are ready to update to it. + Widget? _newWidget; + + @override + void performRebuild() { + if (_newWidget != null) { + // _newWidget can be null if, for instance, we were rebuilt + // due to a reassemble. + final Widget newWidget = _newWidget!; + _newWidget = null; + update(newWidget as RenderObjectToWidgetAdapter); + } + super.performRebuild(); + assert(_newWidget == null); + } + + void _rebuild() { + try { + _child = updateChild(_child, widget.child, _rootChildSlot); + } catch (exception, stack) { + final FlutterErrorDetails details = FlutterErrorDetails( + exception: exception, + stack: stack, + library: 'widgets library', + context: ErrorDescription('attaching to the render tree'), + ); + FlutterError.reportError(details); + final Widget error = ErrorWidget.builder(details); + _child = updateChild(null, error, _rootChildSlot); + } + } + + @override + ContainerRenderObjectMixin> get renderObject => super.renderObject as ContainerRenderObjectMixin>; + + @override + void insertRenderObjectChild(RenderObject child, Object? slot) { + assert(renderObject.debugValidateChild(child)); + renderObject.add(child as RenderBox); + } + + @override + void moveRenderObjectChild(RenderObject child, Object? oldSlot, Object? newSlot) { + assert(false); + } + + @override + void removeRenderObjectChild(RenderObject child, Object? slot) { + renderObject.remove(child as RenderBox); + } +} + +abstract class WidgetElement extends dom.Element { + late Widget _widget; + _KrakenAdapterWidgetState? _state; + + WidgetElement(dom.EventTargetContext? context) + : super( + context, + defaultStyle: _defaultStyle + ) { + WidgetsFlutterBinding.ensureInitialized(); + _state = _KrakenAdapterWidgetState(this, properties, childNodes); + _widget = _KrakenAdapterWidget(_state!); + } + + Widget build(BuildContext context, Map properties, List children); + + @override + void didDetachRenderer() { + super.didDetachRenderer(); + } + + @override + void didAttachRenderer() { + super.didAttachRenderer(); + + _attachWidget(_widget); + } + + @override + void removeProperty(String key) { + super.removeProperty(key); + if (_state != null) { + _state!.onAttributeChanged(properties); + } + } + + @override + void setProperty(String key, dynamic value) { + super.setProperty(key, value); + if (_state != null) { + _state!.onAttributeChanged(properties); + } + } + + @override + dom.Node appendChild(dom.Node child) { + super.appendChild(child); + + if (_state != null) { + _state!.onChildrenChanged(childNodes); + } + + return child; + } + + @override + dom.Node removeChild(dom.Node child) { + super.removeChild(child); + + if (_state != null) { + _state!.onChildrenChanged(children); + } + + return child; + } + + RenderObjectElement? renderObjectElement; + + void _attachWidget(Widget widget) { + RenderObjectElement rootFlutterElement = ownerDocument.controller.rootFlutterElement; + + KrakenRenderObjectToWidgetAdapter adaptor = KrakenRenderObjectToWidgetAdapter( + child: widget, + container: renderBoxModel as ContainerRenderObjectMixin> + ); + + Element? parentFlutterElement; + if (parentNode is WidgetElement) { + parentFlutterElement = (parentNode as WidgetElement).renderObjectElement; + } else { + parentFlutterElement = (parentNode as dom.Element).flutterElement; + } + + renderObjectElement = adaptor.attachToRenderTree(rootFlutterElement.owner!, (parentFlutterElement ?? rootFlutterElement) as RenderObjectElement, parentFlutterElement == null); + } +} + +class _KrakenAdapterWidget extends StatefulWidget { + final _KrakenAdapterWidgetState _state; + + _KrakenAdapterWidget(this._state); + + + @override + State createState() { + return _state; + } +} + + +class _KrakenAdapterWidgetState extends State<_KrakenAdapterWidget> { + Map _properties; + final WidgetElement _element; + late List _childNodes; + + _KrakenAdapterWidgetState(this._element, this._properties, List childNodes) { + _childNodes = childNodes; + } + + void onAttributeChanged(Map properties) { + if (mounted) { + setState(() { + _properties = properties; + }); + } else { + _properties = properties; + } + } + + List convertNodeListToWidgetList(List childNodes) { + List children = List.generate(childNodes.length, (index) { + if (childNodes[index] is WidgetElement) { + _KrakenAdapterWidgetState state = (childNodes[index] as WidgetElement)._state!; + return state.build(context); + } else { + return childNodes[index].flutterWidget ?? KrakenElementToWidgetAdaptor(childNodes[index], key: Key(index.toString())); + } + }); + + return children; + } + + void onChildrenChanged(List childNodes) { + if (mounted) { + setState(() { + _childNodes = childNodes; + }); + } else { + _childNodes = childNodes; + } + } + + @override + Widget build(BuildContext context) { + return _element.build(context, _properties, convertNodeListToWidgetList(_childNodes)); + } +} diff --git a/kraken/lib/widget.dart b/kraken/lib/widget.dart index 7a4bb2c584..5a0d077f0b 100644 --- a/kraken/lib/widget.dart +++ b/kraken/lib/widget.dart @@ -2,1038 +2,7 @@ * Copyright (C) 2019-present Alibaba Inc. All rights reserved. * Author: Kraken Team. */ -import 'dart:io'; -import 'dart:ui'; -import 'dart:typed_data'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter/scheduler.dart'; -import 'package:kraken/kraken.dart'; -import 'package:kraken/rendering.dart'; -import 'package:kraken/dom.dart' as dom; -import 'package:kraken/module.dart'; -import 'package:kraken/gesture.dart'; -import 'package:kraken/css.dart'; -import 'package:kraken/src/dom/element_registry.dart'; - -/// Get context of current widget. -typedef GetContext = BuildContext Function(); -/// Request focus of current widget. -typedef RequestFocus = void Function(); -/// Get the target platform. -typedef GetTargetPlatform = TargetPlatform Function(); -/// Get the cursor color according to the widget theme and platform theme. -typedef GetCursorColor = Color Function(); -/// Get the selection color according to the widget theme and platform theme. -typedef GetSelectionColor = Color Function(); -/// Get the cursor radius according to the target platform. -typedef GetCursorRadius = Radius Function(); -/// Get the text selection controls according to the target platform. -typedef GetTextSelectionControls = TextSelectionControls Function(); - -/// Delegate methods of widget -class WidgetDelegate { - GetContext getContext; - RequestFocus requestFocus; - GetTargetPlatform getTargetPlatform; - GetCursorColor getCursorColor; - GetSelectionColor getSelectionColor; - GetCursorRadius getCursorRadius; - GetTextSelectionControls getTextSelectionControls; - - WidgetDelegate( - this.getContext, - this.requestFocus, - this.getTargetPlatform, - this.getCursorColor, - this.getSelectionColor, - this.getCursorRadius, - this.getTextSelectionControls, - ); -} - -const Map _defaultStyle = { - DISPLAY: INLINE_BLOCK, -}; - -abstract class WidgetElement extends dom.Element { - late Element _renderViewElement; - late BuildOwner _buildOwner; - late Widget _widget; - _KrakenAdapterWidgetPropertiesState? _propertiesState; - WidgetElement(dom.EventTargetContext? context) - : super( - context, - isIntrinsicBox: true, - defaultStyle: _defaultStyle - ); - - Widget build(BuildContext context, Map properties); - - @override - void didAttachRenderer() { - super.didAttachRenderer(); - - WidgetsFlutterBinding.ensureInitialized(); - - _propertiesState = _KrakenAdapterWidgetPropertiesState(this, properties); - _widget = _KrakenAdapterWidget(_propertiesState!); - _attachWidget(_widget); - } - - @override - void removeProperty(String key) { - super.removeProperty(key); - if (_propertiesState != null) { - _propertiesState!.onAttributeChanged(properties); - } - } - - @override - void setProperty(String key, dynamic value) { - super.setProperty(key, value); - if (_propertiesState != null) { - _propertiesState!.onAttributeChanged(properties); - } - } - - void _handleBuildScheduled() { - // Register drawFrame callback same with [WidgetsBinding.drawFrame] - SchedulerBinding.instance!.addPostFrameCallback((Duration timeStamp) { - _buildOwner.buildScope(_renderViewElement); - // ignore: invalid_use_of_protected_member - RendererBinding.instance!.drawFrame(); - _buildOwner.finalizeTree(); - }); - SchedulerBinding.instance!.ensureVisualUpdate(); - } - - void _attachWidget(Widget widget) { - // A new buildOwner difference with flutter's buildOwner - _buildOwner = BuildOwner(focusManager: WidgetsBinding.instance!.buildOwner!.focusManager); - _buildOwner.onBuildScheduled = _handleBuildScheduled; - _renderViewElement = RenderObjectToWidgetAdapter( - child: widget, - container: renderBoxModel as RenderObjectWithChildMixin, - ).attachToRenderTree(_buildOwner); - } -} - -class _KrakenAdapterWidget extends StatefulWidget { - final _KrakenAdapterWidgetPropertiesState _state; - _KrakenAdapterWidget(this._state); - @override - State createState() { - return _state; - } -} - -class _KrakenAdapterWidgetPropertiesState extends State<_KrakenAdapterWidget> { - Map _properties; - final WidgetElement _element; - _KrakenAdapterWidgetPropertiesState(this._element, this._properties); - - void onAttributeChanged(Map properties) { - setState(() { - _properties = properties; - }); - } - - @override - Widget build(BuildContext context) { - return _element.build(context, _properties); - } -} - -class Kraken extends StatefulWidget { - // The background color for viewport, default to transparent. - final Color? background; - - // the width of krakenWidget - final double? viewportWidth; - - // the height of krakenWidget - final double? viewportHeight; - - // The initial bundle to load. - final KrakenBundle? bundle; - - // The animationController of Flutter Route object. - // Pass this object to KrakenWidget to make sure Kraken execute JavaScripts scripts after route transition animation completed. - final AnimationController? animationController; - - // The methods of the KrakenNavigateDelegation help you implement custom behaviors that are triggered - // during a kraken view's process of loading, and completing a navigation request. - final KrakenNavigationDelegate? navigationDelegate; - - // A method channel for receiving messaged from JavaScript code and sending message to JavaScript. - final KrakenMethodChannel? javaScriptChannel; - - // Register the RouteObserver to observer page navigation. - // This is useful if you wants to pause kraken timers and callbacks when kraken widget are hidden by page route. - // https://api.flutter.dev/flutter/widgets/RouteObserver-class.html - final RouteObserver>? routeObserver; - - final LoadErrorHandler? onLoadError; - - final LoadHandler? onLoad; - - final JSErrorHandler ?onJSError; - - // Open a service to support Chrome DevTools for debugging. - // https://github.com/openkraken/devtools - final DevToolsService? devToolsService; - - final GestureListener? gestureListener; - - final HttpClientInterceptor? httpClientInterceptor; - - final UriParser? uriParser; - - KrakenController? get controller { - return KrakenController.getControllerOfName(shortHash(this)); - } - - // Set kraken http cache mode. - static void setHttpCacheMode(HttpCacheMode mode) { - HttpCacheController.mode = mode; - if (kDebugMode) { - print('Kraken http cache mode set to $mode.'); - } - } - - static bool _isValidCustomElementName(localName) { - return RegExp(r'^[a-z][.0-9_a-z]*-[\-.0-9_a-z]*$').hasMatch(localName); - } - - static void defineCustomElement(String tagName, ElementCreator creator) { - if (!_isValidCustomElementName(tagName)) { - throw ArgumentError('The element name "$tagName" is not valid.'); - } - defineElement(tagName.toUpperCase(), creator); - } - - loadBundle(KrakenBundle bundle) async { - await controller!.unload(); - await controller!.loadBundle( - bundle: bundle - ); - _evalBundle(controller!, animationController); - } - - @deprecated - loadContent(String bundleContent) async { - await controller!.unload(); - await controller!.loadBundle( - bundle: KrakenBundle.fromContent(bundleContent) - ); - _evalBundle(controller!, animationController); - } - - @deprecated - loadByteCode(Uint8List bundleByteCode) async { - await controller!.unload(); - await controller!.loadBundle( - bundle: KrakenBundle.fromBytecode(bundleByteCode) - ); - _evalBundle(controller!, animationController); - } - - @deprecated - loadURL(String bundleURL, { String? bundleContent, Uint8List? bundleByteCode }) async { - await controller!.unload(); - - KrakenBundle bundle; - if (bundleByteCode != null) { - bundle = KrakenBundle.fromBytecode(bundleByteCode, url: bundleURL); - } else if (bundleContent != null) { - bundle = KrakenBundle.fromContent(bundleContent, url: bundleURL); - } else { - bundle = KrakenBundle.fromUrl(bundleURL); - } - - await controller!.loadBundle( - bundle: bundle - ); - _evalBundle(controller!, animationController); - } - - @deprecated - loadPath(String bundlePath, { String? bundleContent, Uint8List? bundleByteCode }) async { - await controller!.unload(); - - KrakenBundle bundle; - if (bundleByteCode != null) { - bundle = KrakenBundle.fromBytecode(bundleByteCode, url: bundlePath); - } else if (bundleContent != null) { - bundle = KrakenBundle.fromContent(bundleContent, url: bundlePath); - } else { - bundle = KrakenBundle.fromUrl(bundlePath); - } - - await controller!.loadBundle( - bundle: bundle - ); - _evalBundle(controller!, animationController); - } - - reload() async { - await controller!.reload(); - } - - Kraken({ - Key? key, - this.viewportWidth, - this.viewportHeight, - this.bundle, - this.onLoad, - this.navigationDelegate, - this.javaScriptChannel, - this.background, - this.gestureListener, - this.devToolsService, - // Kraken's http client interceptor. - this.httpClientInterceptor, - this.uriParser, - this.routeObserver, - // Kraken's viewportWidth options only works fine when viewportWidth is equal to window.physicalSize.width / window.devicePixelRatio. - // Maybe got unexpected error when change to other values, use this at your own risk! - // We will fixed this on next version released. (v0.6.0) - // Disable viewportWidth check and no assertion error report. - bool disableViewportWidthAssertion = false, - // Kraken's viewportHeight options only works fine when viewportHeight is equal to window.physicalSize.height / window.devicePixelRatio. - // Maybe got unexpected error when change to other values, use this at your own risk! - // We will fixed this on next version release. (v0.6.0) - // Disable viewportHeight check and no assertion error report. - bool disableViewportHeightAssertion = false, - // Callback functions when loading Javascript scripts failed. - this.onLoadError, - this.animationController, - this.onJSError - }) : super(key: key); - - @override - void debugFillProperties(DiagnosticPropertiesBuilder properties) { - super.debugFillProperties(properties); - properties.add(DiagnosticsProperty('viewportWidth', viewportWidth)); - properties.add(DiagnosticsProperty('viewportHeight', viewportHeight)); - } - - @override - _KrakenState createState() => _KrakenState(); - -} -class _KrakenState extends State with RouteAware { - Map>? _actionMap; - - final FocusNode _focusNode = FocusNode(); - - @override - void initState() { - super.initState(); - _actionMap = >{ - // Action of focus. - NextFocusIntent: CallbackAction(onInvoke: _handleNextFocus), - PreviousFocusIntent: CallbackAction(onInvoke: _handlePreviousFocus), - - // Action of mouse move hotkeys. - MoveSelectionRightByLineTextIntent: CallbackAction(onInvoke: _handleMoveSelectionRightByLineText), - MoveSelectionLeftByLineTextIntent: CallbackAction(onInvoke: _handleMoveSelectionLeftByLineText), - MoveSelectionRightByWordTextIntent: CallbackAction(onInvoke: _handleMoveSelectionRightByWordText), - MoveSelectionLeftByWordTextIntent: CallbackAction(onInvoke: _handleMoveSelectionLeftByWordText), - MoveSelectionUpTextIntent: CallbackAction(onInvoke: _handleMoveSelectionUpText), - MoveSelectionDownTextIntent: CallbackAction(onInvoke: _handleMoveSelectionDownText), - MoveSelectionLeftTextIntent: CallbackAction(onInvoke: _handleMoveSelectionLeftText), - MoveSelectionRightTextIntent: CallbackAction(onInvoke: _handleMoveSelectionRightText), - MoveSelectionToStartTextIntent: CallbackAction(onInvoke: _handleMoveSelectionToStartText), - MoveSelectionToEndTextIntent: CallbackAction(onInvoke: _handleMoveSelectionToEndText), - - // Action of selection hotkeys. - ExtendSelectionLeftTextIntent: CallbackAction(onInvoke: _handleExtendSelectionLeftText), - ExtendSelectionRightTextIntent: CallbackAction(onInvoke: _handleExtendSelectionRightText), - ExtendSelectionUpTextIntent: CallbackAction(onInvoke: _handleExtendSelectionUpText), - ExtendSelectionDownTextIntent: CallbackAction(onInvoke: _handleExtendSelectionDownText), - ExpandSelectionToEndTextIntent: CallbackAction(onInvoke: _handleExtendSelectionToEndText), - ExpandSelectionToStartTextIntent: CallbackAction(onInvoke: _handleExtendSelectionToStartText), - ExpandSelectionLeftByLineTextIntent: CallbackAction(onInvoke: _handleExtendSelectionLeftByLineText), - ExpandSelectionRightByLineTextIntent: CallbackAction(onInvoke: _handleExtendSelectionRightByLineText), - ExtendSelectionLeftByWordTextIntent: CallbackAction(onInvoke: _handleExtendSelectionLeftByWordText), - ExtendSelectionRightByWordTextIntent: CallbackAction(onInvoke: _handleExtendSelectionRightByWordText), - }; - } - - @override - Widget build(BuildContext context) { - return RepaintBoundary( - child: FocusableActionDetector( - actions: _actionMap, - focusNode: _focusNode, - onFocusChange: _handleFocusChange, - child: _KrakenRenderObjectWidget( - context.widget as Kraken, - widgetDelegate, - ) - ) - ); - } - - WidgetDelegate get widgetDelegate { - return WidgetDelegate( - _getContext, - _requestFocus, - _getTargetPlatform, - _getCursorColor, - _getSelectionColor, - _getCursorRadius, - _getTextSelectionControls, - ); - } - - // Get context of current widget. - BuildContext _getContext() { - return context; - } - - // Request focus of current widget. - void _requestFocus() { - _focusNode.requestFocus(); - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - if (widget.routeObserver != null) { - widget.routeObserver!.subscribe(this, ModalRoute.of(context)!); - } - } - - // Resume call timer and callbacks when kraken widget change to visible. - @override - void didPopNext() { - assert(widget.controller != null); - widget.controller!.resume(); - } - - // Pause all timer and callbacks when kraken widget has been invisible. - @override - void didPushNext() { - assert(widget.controller != null); - widget.controller!.pause(); - } - - @override - void dispose() { - if (widget.routeObserver != null) { - widget.routeObserver!.unsubscribe(this); - } - super.dispose(); - } - - - // Get the target platform. - TargetPlatform _getTargetPlatform() { - final ThemeData theme = Theme.of(context); - return theme.platform; - } - - // Get the cursor color according to the widget theme and platform theme. - Color _getCursorColor() { - Color cursorColor = CSSColor.initial; - TextSelectionThemeData selectionTheme = TextSelectionTheme.of(context); - ThemeData theme = Theme.of(context); - - switch (theme.platform) { - case TargetPlatform.iOS: - final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context); - cursorColor = selectionTheme.cursorColor ?? cupertinoTheme.primaryColor; - break; - - case TargetPlatform.macOS: - final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context); - cursorColor = selectionTheme.cursorColor ?? cupertinoTheme.primaryColor; - break; - - case TargetPlatform.android: - case TargetPlatform.fuchsia: - cursorColor = selectionTheme.cursorColor ?? theme.colorScheme.primary; - break; - - case TargetPlatform.linux: - case TargetPlatform.windows: - cursorColor = selectionTheme.cursorColor ?? theme.colorScheme.primary; - break; - } - - return cursorColor; - } - - // Get the selection color according to the widget theme and platform theme. - Color _getSelectionColor() { - Color selectionColor = CSSColor.initial.withOpacity(0.4); - TextSelectionThemeData selectionTheme = TextSelectionTheme.of(context); - ThemeData theme = Theme.of(context); - - switch (theme.platform) { - case TargetPlatform.iOS: - final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context); - selectionColor = selectionTheme.selectionColor ?? cupertinoTheme.primaryColor.withOpacity(0.40); - break; - - case TargetPlatform.macOS: - final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context); - selectionColor = selectionTheme.selectionColor ?? cupertinoTheme.primaryColor.withOpacity(0.40); - break; - - case TargetPlatform.android: - case TargetPlatform.fuchsia: - selectionColor = selectionTheme.selectionColor ?? theme.colorScheme.primary.withOpacity(0.40); - break; - - case TargetPlatform.linux: - case TargetPlatform.windows: - selectionColor = selectionTheme.selectionColor ?? theme.colorScheme.primary.withOpacity(0.40); - break; - } - - return selectionColor; - } - - // Get the cursor radius according to the target platform. - Radius _getCursorRadius() { - Radius cursorRadius = const Radius.circular(2.0); - TargetPlatform platform = _getTargetPlatform(); - - switch (platform) { - case TargetPlatform.iOS: - cursorRadius = const Radius.circular(2.0); - break; - - case TargetPlatform.macOS: - cursorRadius = const Radius.circular(2.0); - break; - - case TargetPlatform.android: - case TargetPlatform.fuchsia: - case TargetPlatform.linux: - case TargetPlatform.windows: - break; - } - - return cursorRadius; - } - - // Get the text selection controls according to the target platform. - TextSelectionControls _getTextSelectionControls() { - TextSelectionControls _selectionControls; - TargetPlatform platform = _getTargetPlatform(); - - switch (platform) { - case TargetPlatform.iOS: - _selectionControls = cupertinoTextSelectionControls; - break; - - case TargetPlatform.macOS: - _selectionControls = cupertinoDesktopTextSelectionControls; - break; - - case TargetPlatform.android: - case TargetPlatform.fuchsia: - _selectionControls = materialTextSelectionControls; - break; - - case TargetPlatform.linux: - case TargetPlatform.windows: - _selectionControls = desktopTextSelectionControls; - break; - } - - return _selectionControls; - } - - // Handle focus change of focusNode. - void _handleFocusChange(bool focused) { - dom.Element rootElement = _findRootElement(); - List focusableElements = _findFocusableElements(rootElement); - if (focusableElements.isNotEmpty) { - dom.Element? focusedElement = _findFocusedElement(focusableElements); - // Currently only input element is focusable. - if (focused) { - if (dom.InputElement.focusInputElement == null) { - (focusableElements[0] as dom.InputElement).focus(); - } - } else { - if (focusedElement != null) { - (focusedElement as dom.InputElement).blur(); - } - } - } - } - - // Handle focus action usually by pressing the [Tab] hotkey. - void _handleNextFocus(NextFocusIntent intent) { - dom.Element rootElement = _findRootElement(); - List focusableElements = _findFocusableElements(rootElement); - if (focusableElements.isNotEmpty) { - dom.Element? focusedElement = _findFocusedElement(focusableElements); - // None focusable element is focused, focus the first focusable element. - if (focusedElement == null) { - _focusNode.requestFocus(); - (focusableElements[0] as dom.InputElement).focus(); - - // Some focusable element is focused, focus the next element, if it is the last focusable element, - // then focus the next widget. - } else { - int idx = focusableElements.indexOf(focusedElement); - if (idx == focusableElements.length - 1) { - _focusNode.nextFocus(); - (focusableElements[focusableElements.length - 1] as dom.InputElement).blur(); - } else { - _focusNode.requestFocus(); - (focusableElements[idx] as dom.InputElement).blur(); - (focusableElements[idx + 1] as dom.InputElement).focus(); - } - } - - // None focusable element exists, focus the next widget. - } else { - _focusNode.nextFocus(); - } - } - - // Handle focus action usually by pressing the [Shift]+[Tab] hotkey in the reverse direction. - void _handlePreviousFocus(PreviousFocusIntent intent) { - dom.Element rootElement = _findRootElement(); - List focusableElements = _findFocusableElements(rootElement); - if (focusableElements.isNotEmpty) { - dom.Element? focusedElement = _findFocusedElement(focusableElements); - // None editable is focused, focus the last editable. - if (focusedElement == null) { - _focusNode.requestFocus(); - (focusableElements[focusableElements.length - 1] as dom.InputElement).focus(); - - // Some editable is focused, focus the previous editable, if it is the first editable, - // then focus the previous widget. - } else { - int idx = focusableElements.indexOf(focusedElement); - if (idx == 0) { - _focusNode.previousFocus(); - (focusableElements[0] as dom.InputElement).blur(); - } else { - _focusNode.requestFocus(); - (focusableElements[idx] as dom.InputElement).blur(); - (focusableElements[idx - 1] as dom.InputElement).focus(); - } - } - // None editable exists, focus the previous widget. - } else { - _focusNode.previousFocus(); - } - } - - void _handleMoveSelectionRightByLineText(MoveSelectionRightByLineTextIntent intent) { - dom.Element? focusedElement = _findFocusedElement(); - if (focusedElement != null) { - RenderEditable? focusedRenderEditable = (focusedElement as dom.InputElement).renderEditable; - if (focusedRenderEditable != null) { - focusedRenderEditable.moveSelectionRightByLine(SelectionChangedCause.keyboard); - // Make caret visible while moving cursor. - focusedElement.scrollToCaret(); - } - } - } - - void _handleMoveSelectionLeftByLineText(MoveSelectionLeftByLineTextIntent intent) { - dom.Element? focusedElement = _findFocusedElement(); - if (focusedElement != null) { - RenderEditable? focusedRenderEditable = (focusedElement as dom.InputElement).renderEditable; - if (focusedRenderEditable != null) { - focusedRenderEditable.moveSelectionLeftByLine(SelectionChangedCause.keyboard); - // Make caret visible while moving cursor. - focusedElement.scrollToCaret(); - } - } - } - - void _handleMoveSelectionRightByWordText(MoveSelectionRightByWordTextIntent intent) { - dom.Element? focusedElement = _findFocusedElement(); - if (focusedElement != null) { - RenderEditable? focusedRenderEditable = (focusedElement as dom.InputElement).renderEditable; - if (focusedRenderEditable != null) { - focusedRenderEditable.moveSelectionRightByWord(SelectionChangedCause.keyboard); - // Make caret visible while moving cursor. - focusedElement.scrollToCaret(); - } - } - } - - void _handleMoveSelectionLeftByWordText(MoveSelectionLeftByWordTextIntent intent) { - dom.Element? focusedElement = _findFocusedElement(); - if (focusedElement != null) { - RenderEditable? focusedRenderEditable = (focusedElement as dom.InputElement).renderEditable; - if (focusedRenderEditable != null) { - focusedRenderEditable.moveSelectionLeftByWord(SelectionChangedCause.keyboard); - // Make caret visible while moving cursor. - focusedElement.scrollToCaret(); - } - } - } - - void _handleMoveSelectionUpText(MoveSelectionUpTextIntent intent) { - dom.Element? focusedElement = _findFocusedElement(); - if (focusedElement != null) { - RenderEditable? focusedRenderEditable = (focusedElement as dom.InputElement).renderEditable; - if (focusedRenderEditable != null) { - focusedRenderEditable.moveSelectionUp(SelectionChangedCause.keyboard); - // Make caret visible while moving cursor. - focusedElement.scrollToCaret(); - } - } - } - - void _handleMoveSelectionDownText(MoveSelectionDownTextIntent intent) { - dom.Element? focusedElement = _findFocusedElement(); - if (focusedElement != null) { - RenderEditable? focusedRenderEditable = (focusedElement as dom.InputElement).renderEditable; - if (focusedRenderEditable != null) { - focusedRenderEditable.moveSelectionDown(SelectionChangedCause.keyboard); - // Make caret visible while moving cursor. - focusedElement.scrollToCaret(); - } - } - } - - void _handleMoveSelectionLeftText(MoveSelectionLeftTextIntent intent) { - dom.Element? focusedElement = _findFocusedElement(); - if (focusedElement != null) { - RenderEditable? focusedRenderEditable = (focusedElement as dom.InputElement).renderEditable; - if (focusedRenderEditable != null) { - focusedRenderEditable.moveSelectionLeft(SelectionChangedCause.keyboard); - // Make caret visible while moving cursor. - focusedElement.scrollToCaret(); - } - } - } - - void _handleMoveSelectionRightText(MoveSelectionRightTextIntent intent) { - dom.Element? focusedElement = _findFocusedElement(); - if (focusedElement != null) { - RenderEditable? focusedRenderEditable = (focusedElement as dom.InputElement).renderEditable; - if (focusedRenderEditable != null) { - focusedRenderEditable.moveSelectionRight(SelectionChangedCause.keyboard); - // Make caret visible while moving cursor. - focusedElement.scrollToCaret(); - } - } - } - - void _handleMoveSelectionToEndText(MoveSelectionToEndTextIntent intent) { - dom.Element? focusedElement = _findFocusedElement(); - if (focusedElement != null) { - RenderEditable? focusedRenderEditable = (focusedElement as dom.InputElement).renderEditable; - if (focusedRenderEditable != null) { - focusedRenderEditable.moveSelectionToEnd(SelectionChangedCause.keyboard); - // Make caret visible while moving cursor. - focusedElement.scrollToCaret(); - } - } - } - - void _handleMoveSelectionToStartText(MoveSelectionToStartTextIntent intent) { - dom.Element? focusedElement = _findFocusedElement(); - if (focusedElement != null) { - RenderEditable? focusedRenderEditable = (focusedElement as dom.InputElement).renderEditable; - if (focusedRenderEditable != null) { - focusedRenderEditable.moveSelectionToStart(SelectionChangedCause.keyboard); - // Make caret visible while moving cursor. - focusedElement.scrollToCaret(); - } - } - } - - void _handleExtendSelectionLeftText(ExtendSelectionLeftTextIntent intent) { - dom.Element? focusedElement = _findFocusedElement(); - if (focusedElement != null) { - RenderEditable? focusedRenderEditable = (focusedElement as dom.InputElement).renderEditable; - if (focusedRenderEditable != null) { - focusedRenderEditable.extendSelectionLeft(SelectionChangedCause.keyboard); - } - } - } - - void _handleExtendSelectionRightText(ExtendSelectionRightTextIntent intent) { - dom.Element? focusedElement = _findFocusedElement(); - if (focusedElement != null) { - RenderEditable? focusedRenderEditable = (focusedElement as dom.InputElement).renderEditable; - if (focusedRenderEditable != null) { - focusedRenderEditable.extendSelectionRight(SelectionChangedCause.keyboard); - } - } - } - - void _handleExtendSelectionUpText(ExtendSelectionUpTextIntent intent) { - dom.Element? focusedElement = _findFocusedElement(); - if (focusedElement != null) { - RenderEditable? focusedRenderEditable = (focusedElement as dom.InputElement).renderEditable; - if (focusedRenderEditable != null) { - focusedRenderEditable.extendSelectionUp(SelectionChangedCause.keyboard); - } - } - } - - void _handleExtendSelectionDownText(ExtendSelectionDownTextIntent intent) { - dom.Element? focusedElement = _findFocusedElement(); - if (focusedElement != null) { - RenderEditable? focusedRenderEditable = (focusedElement as dom.InputElement).renderEditable; - if (focusedRenderEditable != null) { - focusedRenderEditable.extendSelectionDown(SelectionChangedCause.keyboard); - } - } - } - - void _handleExtendSelectionToEndText(ExpandSelectionToEndTextIntent intent) { - dom.Element? focusedElement = _findFocusedElement(); - if (focusedElement != null) { - RenderEditable? focusedRenderEditable = (focusedElement as dom.InputElement).renderEditable; - if (focusedRenderEditable != null) { - focusedRenderEditable.expandSelectionToEnd(SelectionChangedCause.keyboard); - } - } - } - - void _handleExtendSelectionToStartText(ExpandSelectionToStartTextIntent intent) { - dom.Element? focusedElement = _findFocusedElement(); - if (focusedElement != null) { - RenderEditable? focusedRenderEditable = (focusedElement as dom.InputElement).renderEditable; - if (focusedRenderEditable != null) { - focusedRenderEditable.expandSelectionToStart(SelectionChangedCause.keyboard); - } - } - } - - void _handleExtendSelectionLeftByLineText(ExpandSelectionLeftByLineTextIntent intent) { - dom.Element? focusedElement = _findFocusedElement(); - if (focusedElement != null) { - RenderEditable? focusedRenderEditable = (focusedElement as dom.InputElement).renderEditable; - if (focusedRenderEditable != null) { - focusedRenderEditable.expandSelectionLeftByLine(SelectionChangedCause.keyboard); - } - } - } - - void _handleExtendSelectionRightByLineText(ExpandSelectionRightByLineTextIntent intent) { - dom.Element? focusedElement = _findFocusedElement(); - if (focusedElement != null) { - RenderEditable? focusedRenderEditable = (focusedElement as dom.InputElement).renderEditable; - if (focusedRenderEditable != null) { - focusedRenderEditable.expandSelectionRightByLine(SelectionChangedCause.keyboard); - } - } - } - - void _handleExtendSelectionLeftByWordText(ExtendSelectionLeftByWordTextIntent intent) { - dom.Element? focusedElement = _findFocusedElement(); - if (focusedElement != null) { - RenderEditable? focusedRenderEditable = (focusedElement as dom.InputElement).renderEditable; - if (focusedRenderEditable != null) { - focusedRenderEditable.extendSelectionLeftByWord(SelectionChangedCause.keyboard); - } - } - } - - void _handleExtendSelectionRightByWordText(ExtendSelectionRightByWordTextIntent intent) { - dom.Element? focusedElement = _findFocusedElement(); - if (focusedElement != null) { - RenderEditable? focusedRenderEditable = (focusedElement as dom.InputElement).renderEditable; - if (focusedRenderEditable != null) { - focusedRenderEditable.extendSelectionRightByWord(SelectionChangedCause.keyboard); - } - } - } - - // Find RenderViewportBox in the renderObject tree. - RenderViewportBox? _findRenderViewportBox(RenderObject parent) { - RenderViewportBox? result; - parent.visitChildren((RenderObject child) { - if (child is RenderViewportBox) { - result = child; - } else { - result = _findRenderViewportBox(child); - } - }); - return result; - } - - // Find root element of dom tree. - dom.Element _findRootElement() { - RenderObject? _rootRenderObject = context.findRenderObject(); - RenderViewportBox? renderViewportBox = _findRenderViewportBox(_rootRenderObject!); - KrakenController controller = (renderViewportBox as RenderObjectWithControllerMixin).controller!; - dom.Element documentElement = controller.view.document.documentElement!; - return documentElement; - } - - // Find all the focusable elements in the element tree. - List _findFocusableElements(dom.Element element) { - List result = []; - traverseElement(element, (dom.Element child) { - // Currently only input element is focusable. - if (child is dom.InputElement) { - result.add(child); - } - }); - return result; - } - - // Find the focused element in the element tree. - dom.Element? _findFocusedElement([List? focusableElements]) { - dom.Element? result; - if (focusableElements == null) { - dom.Element rootElement = _findRootElement(); - focusableElements = _findFocusableElements(rootElement); - } - - if (focusableElements.isNotEmpty) { - // Currently only input element is focusable. - for (dom.Element inputElement in focusableElements) { - RenderEditable? renderEditable = (inputElement as dom.InputElement).renderEditable; - if (renderEditable != null && renderEditable.hasFocus) { - result = inputElement; - break; - } - } - } - return result; - } -} - -class _KrakenRenderObjectWidget extends SingleChildRenderObjectWidget { - // Creates a widget that visually hides its child. - const _KrakenRenderObjectWidget( - Kraken widget, - WidgetDelegate widgetDelegate, - {Key? key} - ) : _krakenWidget = widget, - _widgetDelegate = widgetDelegate, - super(key: key); - - final Kraken _krakenWidget; - final WidgetDelegate _widgetDelegate; - - @override - RenderObject createRenderObject(BuildContext context) { - if (kProfileMode) { - PerformanceTiming.instance().mark(PERF_CONTROLLER_INIT_START); - } - - double viewportWidth = _krakenWidget.viewportWidth ?? window.physicalSize.width / window.devicePixelRatio; - double viewportHeight = _krakenWidget.viewportHeight ?? window.physicalSize.height / window.devicePixelRatio; - - if (viewportWidth == 0.0 && viewportHeight == 0.0) { - throw FlutterError('''Can't get viewportSize from window. Please set viewportWidth and viewportHeight manually. -This situation often happened when you trying creating kraken when FlutterView not initialized.'''); - } - - KrakenController controller = KrakenController( - shortHash(_krakenWidget.hashCode), - viewportWidth, - viewportHeight, - background: _krakenWidget.background, - showPerformanceOverlay: Platform.environment[ENABLE_PERFORMANCE_OVERLAY] != null, - bundle: _krakenWidget.bundle, - onLoad: _krakenWidget.onLoad, - onLoadError: _krakenWidget.onLoadError, - onJSError: _krakenWidget.onJSError, - methodChannel: _krakenWidget.javaScriptChannel, - gestureListener: _krakenWidget.gestureListener, - navigationDelegate: _krakenWidget.navigationDelegate, - devToolsService: _krakenWidget.devToolsService, - httpClientInterceptor: _krakenWidget.httpClientInterceptor, - widgetDelegate: _widgetDelegate, - uriParser: _krakenWidget.uriParser - ); - - if (kProfileMode) { - PerformanceTiming.instance().mark(PERF_CONTROLLER_INIT_END); - } - - return controller.view.getRootRenderObject(); - } - - @override - void updateRenderObject(BuildContext context, covariant RenderObject renderObject) { - super.updateRenderObject(context, renderObject); - KrakenController controller = (renderObject as RenderObjectWithControllerMixin).controller!; - controller.name = shortHash(_krakenWidget.hashCode); - - bool viewportWidthHasChanged = controller.view.viewportWidth != _krakenWidget.viewportWidth; - bool viewportHeightHasChanged = controller.view.viewportHeight != _krakenWidget.viewportHeight; - - double viewportWidth = _krakenWidget.viewportWidth ?? window.physicalSize.width / window.devicePixelRatio; - double viewportHeight = _krakenWidget.viewportHeight ?? window.physicalSize.height / window.devicePixelRatio; - - if (controller.view.document.documentElement == null) return; - - if (viewportWidthHasChanged) { - controller.view.viewportWidth = viewportWidth; - controller.view.document.documentElement!.renderStyle.width = CSSLengthValue(viewportWidth, CSSLengthType.PX); - } - - if (viewportHeightHasChanged) { - controller.view.viewportHeight = viewportHeight; - controller.view.document.documentElement!.renderStyle.height = CSSLengthValue(viewportHeight, CSSLengthType.PX); - } - } - - @override - void didUnmountRenderObject(covariant RenderObject renderObject) { - KrakenController controller = (renderObject as RenderObjectWithControllerMixin).controller!; - controller.dispose(); - } - - @override - _KrakenRenderObjectElement createElement() { - return _KrakenRenderObjectElement(this); - } -} - -class _KrakenRenderObjectElement extends SingleChildRenderObjectElement { - _KrakenRenderObjectElement(_KrakenRenderObjectWidget widget) : super(widget); - - @override - void mount(Element? parent, Object? newSlot) async { - super.mount(parent, newSlot); - - KrakenController controller = (renderObject as RenderObjectWithControllerMixin).controller!; - - - if (controller.bundle == null || (controller.bundle?.content == null && controller.bundle?.bytecode == null && controller.bundle?.src == null)) { - return; - } - - await controller.loadBundle(); - - _evalBundle(controller, widget._krakenWidget.animationController); - } - - @override - _KrakenRenderObjectWidget get widget => super.widget as _KrakenRenderObjectWidget; -} - -void _evalBundle(KrakenController controller, AnimationController? animationController) async { - // Execute JavaScript scripts will block the Flutter UI Threads. - // Listen for animationController listener to make sure to execute Javascript after route transition had completed. - if (animationController != null) { - animationController.addStatusListener((AnimationStatus status) { - if (status == AnimationStatus.completed) { - controller.evalBundle(); - } - }); - } else { - await controller.evalBundle(); - } -} +export 'src/widget/kraken.dart'; +export 'src/widget/widget_to_element_adaptor.dart'; +export 'src/widget/element_to_widget_adaptor.dart';