From b4ff5ca6aefa2468a2f602575da2af5fc7477e7c Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Thu, 17 Sep 2015 19:29:01 -0700 Subject: [PATCH] Prototype of fn3 This patch contains a prototype of a new widget framework. In this framework, Components can be reused in the tree as many times as the author desires. Also, StatefulComponent is split into two pieces, a ComponentConfiguration and a ComponentState. The ComponentConfiguration is created by the author and can be reused as many times as desired. When mounted into the tree, the ComponentConfiguration creates a ComponentState to hold the state for the component. The state remains in the tree and cannot be reused. --- packages/flutter/lib/src/fn3.dart | 8 + packages/flutter/lib/src/fn3/basic.dart | 57 +++ packages/flutter/lib/src/fn3/framework.dart | 340 ++++++++++++++++++ .../flutter/lib/src/widgets/framework.dart | 2 +- .../test/fn3/render_object_widget_test.dart | 160 +++++++++ packages/unit/test/fn3/widget_tester.dart | 44 +++ 6 files changed, 610 insertions(+), 1 deletion(-) create mode 100644 packages/flutter/lib/src/fn3.dart create mode 100644 packages/flutter/lib/src/fn3/basic.dart create mode 100644 packages/flutter/lib/src/fn3/framework.dart create mode 100644 packages/unit/test/fn3/render_object_widget_test.dart create mode 100644 packages/unit/test/fn3/widget_tester.dart diff --git a/packages/flutter/lib/src/fn3.dart b/packages/flutter/lib/src/fn3.dart new file mode 100644 index 00000000000..5d6fe52f513 --- /dev/null +++ b/packages/flutter/lib/src/fn3.dart @@ -0,0 +1,8 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +library fn3; + +export 'fn3/basic.dart'; +export 'fn3/framework.dart'; diff --git a/packages/flutter/lib/src/fn3/basic.dart b/packages/flutter/lib/src/fn3/basic.dart new file mode 100644 index 00000000000..57bb78afaf6 --- /dev/null +++ b/packages/flutter/lib/src/fn3/basic.dart @@ -0,0 +1,57 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:sky/rendering.dart'; +import 'package:sky/src/fn3/framework.dart'; + +export 'package:sky/rendering.dart' show + BackgroundImage, + BlockDirection, + Border, + BorderSide, + BoxConstraints, + BoxDecoration, + BoxDecorationPosition, + BoxShadow, + Color, + EdgeDims, + EventDisposition, + FlexAlignItems, + FlexDirection, + FlexJustifyContent, + Offset, + Paint, + Path, + Point, + Rect, + ScrollDirection, + Shape, + ShrinkWrap, + Size, + ValueChanged; + +class DecoratedBox extends OneChildRenderObjectWidget { + DecoratedBox({ + Key key, + this.decoration, + this.position: BoxDecorationPosition.background, + Widget child + }) : super(key: key, child: child) { + assert(decoration != null); + assert(position != null); + } + + final BoxDecoration decoration; + final BoxDecorationPosition position; + + RenderObject createRenderObject() => new RenderDecoratedBox( + decoration: decoration, + position: position + ); + + void updateRenderObject(RenderDecoratedBox renderObject) { + renderObject.decoration = decoration; + renderObject.position = position; + } +} diff --git a/packages/flutter/lib/src/fn3/framework.dart b/packages/flutter/lib/src/fn3/framework.dart new file mode 100644 index 00000000000..f0dccabc336 --- /dev/null +++ b/packages/flutter/lib/src/fn3/framework.dart @@ -0,0 +1,340 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:sky/rendering.dart'; + +abstract class Key { +} + +abstract class Widget { + Widget(this.key); + final Key key; + + Element createElement(); +} + +typedef Widget WidgetBuilder(); + +abstract class RenderObjectWidget extends Widget { + RenderObjectWidget({ Key key }) : super(key); + + Element createElement() => new RenderObjectElement(this); + + RenderObject createRenderObject(); + void updateRenderObject(RenderObject renderObject); +} + +abstract class OneChildRenderObjectWidget extends RenderObjectWidget { + OneChildRenderObjectWidget({ Key key, Widget this.child }) : super(key: key); + + final Widget child; + + Element createElement() => new OneChildRenderObjectElement(this); +} + +abstract class MultiChildRenderObjectWidget extends RenderObjectWidget { + MultiChildRenderObjectWidget({ Key key, List this.children }) + : super(key: key); + + final List children; + + Element createElement() => new MultiChildRenderObjectElement(this); +} + +abstract class Component extends Widget { + Component({ Key key }) : super(key); + Element createElement() => new ComponentElement(this); + + Widget build(); +} + +abstract class ComponentState { + ComponentStateElement _holder; + + void setState(void fn()) { + fn(); + _holder.scheduleBuild(); + } + + T get config => _config; + T _config; + + /// Override this setter to update additional state when the config changes. + void set config(T config) { + _config = config; + } + + /// Called when this object is removed from the tree + void didUnmount() { } + + Widget build(); +} + +abstract class ComponentConfiguration extends Widget { + ComponentConfiguration({ Key key }) : super(key); + + ComponentStateElement createElement() => new ComponentStateElement(this); + + ComponentState createState(); +} + +bool _canUpdate(Widget oldWidget, Widget newWidget) { + return oldWidget.runtimeType == newWidget.runtimeType + && oldWidget.key == newWidget.key; +} + +void _debugReportException(String context, dynamic exception, StackTrace stack) { + print('------------------------------------------------------------------------'); + 'Exception caught while $context'.split('\n').forEach(print); + print('$exception'); + print('Stack trace:'); + '$stack'.split('\n').forEach(print); + print('------------------------------------------------------------------------'); +} + +enum _ElementLifecycle { + initial, + mounted, + defunct, +} + +typedef void ElementVisitor(Element element); + +abstract class Element { + Element(T widget) : _widget = widget { + assert(_widget != null); + } + + Element _parent; + T _widget; + + _ElementLifecycle _lifecycleState = _ElementLifecycle.initial; + + void visitChildren(ElementVisitor visitor) { } + + void visitDescendants(ElementVisitor visitor) { + void walk(Element element) { + visitor(element); + element.visitChildren(walk); + } + visitChildren(walk); + } + + void mount(dynamic slot) { + assert(_lifecycleState == _ElementLifecycle.initial); + assert(_widget != null); + _lifecycleState = _ElementLifecycle.mounted; + assert(_parent == null || _parent._lifecycleState == _ElementLifecycle.mounted); + } + + void update(T updated, dynamic slot) { + assert(_lifecycleState == _ElementLifecycle.mounted); + assert(_widget != null); + assert(updated != null); + assert(_canUpdate(_widget, updated)); + _widget = updated; + } + + void unmount() { + assert(_lifecycleState == _ElementLifecycle.mounted); + assert(_widget != null); + _lifecycleState = _ElementLifecycle.defunct; + } + + void _detachChild(Element child) { + if (child == null) + return; + child._parent = null; + + bool haveDetachedRenderObject = false; + void detach(Element descendant) { + if (!haveDetachedRenderObject && descendant is RenderObjectElement) { + descendant.detachRenderObject(); + haveDetachedRenderObject = true; + } + descendant.unmount(); + } + + detach(child); + child.visitDescendants(detach); + } + + Element _updateChild(Element child, Widget updated, dynamic slot) { + if (updated == null) { + _detachChild(child); + return null; + } + + if (child != null) { + if (_canUpdate(child._widget, updated)) { + child.update(updated, slot); + return child; + } + _detachChild(child); + assert(child._parent == null); + } + + Element newChild = updated.createElement(); + newChild._parent = this; + newChild.mount(slot); + return newChild; + } + +} + +abstract class BuildableElement extends Element { + BuildableElement(T widget) : super(widget); + + WidgetBuilder _builder; + Element _child; + + void _rebuild(dynamic slot) { + Widget built; + try { + built = _builder(); + assert(built != null); + } catch (e, stack) { + _debugReportException('building $this', e, stack); + } + _child = _updateChild(_child, built, slot); + } + + void visitChildren(ElementVisitor visitor) { + if (_child != null) + visitor(_child); + } + + void mount(dynamic slot) { + super.mount(slot); + assert(_child == null); + _rebuild(slot); + assert(_child != null); + } +} + +class ComponentElement extends BuildableElement { + ComponentElement(Component component) : super(component) { + _builder = component.build; + } + + void update(Component updated, dynamic slot) { + super.update(updated, slot); + assert(_widget == updated); + _builder = _widget.build; + _rebuild(slot); + } +} + +class ComponentStateElement extends BuildableElement { + ComponentStateElement(ComponentConfiguration configuration) + : _state = configuration.createState(), super(configuration) { + _builder = _state.build; + _state._holder = this; + _state.config = configuration; + } + + ComponentState _state; + + void update(ComponentConfiguration updated, dynamic slot) { + super.update(updated, slot); + assert(_widget == updated); + _state.config = _widget; + _rebuild(slot); + } + + void unmount() { + super.unmount(); + _state.didUnmount(); + _state = null; + } + + void scheduleBuild() { + // TODO(abarth): Implement rebuilding. + } +} + +RenderObjectElement _findAncestorRenderObjectElement(Element ancestor) { + while (ancestor != null && ancestor is! RenderObjectElement) + ancestor = ancestor._parent; + return ancestor; +} + +class RenderObjectElement extends Element { + RenderObjectElement(T widget) + : renderObject = widget.createRenderObject(), super(widget); + + final RenderObject renderObject; + RenderObjectElement _ancestorRenderObjectElement; + + void mount(dynamic slot) { + super.mount(slot); + assert(_ancestorRenderObjectElement == null); + _ancestorRenderObjectElement = _findAncestorRenderObjectElement(_parent); + if (_ancestorRenderObjectElement != null) + _ancestorRenderObjectElement.insertChildRenderObject(renderObject, slot); + } + + void update(T updated, dynamic slot) { + super.update(updated, slot); + assert(_widget == updated); + _widget.updateRenderObject(renderObject); + } + + void detachRenderObject() { + if (_ancestorRenderObjectElement != null) { + _ancestorRenderObjectElement.removeChildRenderObject(renderObject); + _ancestorRenderObjectElement = null; + } + } + + void insertChildRenderObject(RenderObject child, dynamic slot) { + assert(false); + } + + void removeChildRenderObject(RenderObject child) { + assert(false); + } +} + +class OneChildRenderObjectElement extends RenderObjectElement { + OneChildRenderObjectElement(T widget) : super(widget); + + Element _child; + + void visitChildren(ElementVisitor visitor) { + if (_child != null) + visitor(_child); + } + + void mount(dynamic slot) { + super.mount(slot); + _child = _updateChild(_child, _widget.child, null); + } + + void update(T updated, dynamic slot) { + super.update(updated, slot); + assert(_widget == updated); + _child = _updateChild(_child, _widget.child, null); + } + + void insertChildRenderObject(RenderObject child, dynamic slot) { + final renderObject = this.renderObject; // TODO(ianh): Remove this once the analyzer is cleverer + assert(renderObject is RenderObjectWithChildMixin); + assert(slot == null); + renderObject.child = child; + assert(renderObject == this.renderObject); // TODO(ianh): Remove this once the analyzer is cleverer + } + + void removeChildRenderObject(RenderObject child) { + final renderObject = this.renderObject; // TODO(ianh): Remove this once the analyzer is cleverer + assert(renderObject is RenderObjectWithChildMixin); + assert(renderObject.child == child); + renderObject.child = null; + assert(renderObject == this.renderObject); // TODO(ianh): Remove this once the analyzer is cleverer + } +} + +class MultiChildRenderObjectElement extends RenderObjectElement { + MultiChildRenderObjectElement(T widget) : super(widget); +} diff --git a/packages/flutter/lib/src/widgets/framework.dart b/packages/flutter/lib/src/widgets/framework.dart index 4a6c25fd1aa..8f9e9754aa5 100644 --- a/packages/flutter/lib/src/widgets/framework.dart +++ b/packages/flutter/lib/src/widgets/framework.dart @@ -22,7 +22,7 @@ export 'package:sky/src/rendering/object.dart' show Point, Offset, Size, Rect, C final bool _shouldLogRenderDuration = false; // see also 'enableProfilingLoop' argument to runApp() typedef Widget Builder(); -typedef void WidgetTreeWalker(Widget); +typedef void WidgetTreeWalker(Widget widget); abstract class Key { const Key.constructor(); // so that subclasses can call us, since the Key() factory constructor shadows the implicit constructor diff --git a/packages/unit/test/fn3/render_object_widget_test.dart b/packages/unit/test/fn3/render_object_widget_test.dart new file mode 100644 index 00000000000..c4436a4fc46 --- /dev/null +++ b/packages/unit/test/fn3/render_object_widget_test.dart @@ -0,0 +1,160 @@ +import 'package:sky/rendering.dart'; +import 'package:sky/src/fn3.dart'; +import 'package:test/test.dart'; + +import 'widget_tester.dart'; + +final BoxDecoration kBoxDecorationA = new BoxDecoration(); +final BoxDecoration kBoxDecorationB = new BoxDecoration(); +final BoxDecoration kBoxDecorationC = new BoxDecoration(); + +void main() { + test('RenderObjectWidget smoke test', () { + WidgetTester tester = new WidgetTester(); + + tester.pumpFrame(new DecoratedBox(decoration: kBoxDecorationA)); + OneChildRenderObjectElement element = + tester.findElement((element) => element is OneChildRenderObjectElement); + expect(element, isNotNull); + expect(element.renderObject is RenderDecoratedBox, isTrue); + RenderDecoratedBox renderObject = element.renderObject; + expect(renderObject.decoration, equals(kBoxDecorationA)); + expect(renderObject.position, equals(BoxDecorationPosition.background)); + + tester.pumpFrame(new DecoratedBox(decoration: kBoxDecorationB)); + element = tester.findElement((element) => element is OneChildRenderObjectElement); + expect(element, isNotNull); + expect(element.renderObject is RenderDecoratedBox, isTrue); + renderObject = element.renderObject; + expect(renderObject.decoration, equals(kBoxDecorationB)); + expect(renderObject.position, equals(BoxDecorationPosition.background)); + }); + + test('RenderObjectWidget can add and remove children', () { + WidgetTester tester = new WidgetTester(); + + void checkFullTree() { + OneChildRenderObjectElement element = + tester.findElement((element) => element is OneChildRenderObjectElement); + expect(element, isNotNull); + expect(element.renderObject is RenderDecoratedBox, isTrue); + RenderDecoratedBox renderObject = element.renderObject; + expect(renderObject.decoration, equals(kBoxDecorationA)); + expect(renderObject.position, equals(BoxDecorationPosition.background)); + expect(renderObject.child, isNotNull); + expect(renderObject.child is RenderDecoratedBox, isTrue); + RenderDecoratedBox child = renderObject.child; + expect(child.decoration, equals(kBoxDecorationB)); + expect(child.position, equals(BoxDecorationPosition.background)); + expect(child.child, isNull); + } + + void childBareTree() { + OneChildRenderObjectElement element = + tester.findElement((element) => element is OneChildRenderObjectElement); + expect(element, isNotNull); + expect(element.renderObject is RenderDecoratedBox, isTrue); + RenderDecoratedBox renderObject = element.renderObject; + expect(renderObject.decoration, equals(kBoxDecorationA)); + expect(renderObject.position, equals(BoxDecorationPosition.background)); + expect(renderObject.child, isNull); + } + + tester.pumpFrame(new DecoratedBox( + decoration: kBoxDecorationA, + child: new DecoratedBox( + decoration: kBoxDecorationB + ) + )); + + checkFullTree(); + + tester.pumpFrame(new DecoratedBox( + decoration: kBoxDecorationA, + child: new TestComponent( + child: new DecoratedBox( + decoration: kBoxDecorationB + ) + ) + )); + + checkFullTree(); + + tester.pumpFrame(new DecoratedBox( + decoration: kBoxDecorationA, + child: new DecoratedBox( + decoration: kBoxDecorationB + ) + )); + + checkFullTree(); + + tester.pumpFrame(new DecoratedBox( + decoration: kBoxDecorationA + )); + + childBareTree(); + + tester.pumpFrame(new DecoratedBox( + decoration: kBoxDecorationA, + child: new TestComponent( + child: new TestComponent( + child: new DecoratedBox( + decoration: kBoxDecorationB + ) + ) + ) + )); + + checkFullTree(); + + tester.pumpFrame(new DecoratedBox( + decoration: kBoxDecorationA + )); + + childBareTree(); + }); + + test('Detached render tree is intact', () { + WidgetTester tester = new WidgetTester(); + + tester.pumpFrame(new DecoratedBox( + decoration: kBoxDecorationA, + child: new DecoratedBox( + decoration: kBoxDecorationB, + child: new DecoratedBox( + decoration: kBoxDecorationC + ) + ) + )); + + OneChildRenderObjectElement element = + tester.findElement((element) => element is OneChildRenderObjectElement); + expect(element.renderObject is RenderDecoratedBox, isTrue); + RenderDecoratedBox parent = element.renderObject; + expect(parent.child is RenderDecoratedBox, isTrue); + RenderDecoratedBox child = parent.child; + expect(child.decoration, equals(kBoxDecorationB)); + expect(child.child is RenderDecoratedBox, isTrue); + RenderDecoratedBox grandChild = child.child; + expect(grandChild.decoration, equals(kBoxDecorationC)); + expect(grandChild.child, isNull); + + tester.pumpFrame(new DecoratedBox( + decoration: kBoxDecorationA + )); + + element = + tester.findElement((element) => element is OneChildRenderObjectElement); + expect(element.renderObject is RenderDecoratedBox, isTrue); + expect(element.renderObject, equals(parent)); + expect(parent.child, isNull); + + expect(child.parent, isNull); + expect(child.decoration, equals(kBoxDecorationB)); + expect(child.child, equals(grandChild)); + expect(grandChild.parent, equals(child)); + expect(grandChild.decoration, equals(kBoxDecorationC)); + expect(grandChild.child, isNull); + }); +} diff --git a/packages/unit/test/fn3/widget_tester.dart b/packages/unit/test/fn3/widget_tester.dart new file mode 100644 index 00000000000..24d0bf65a9b --- /dev/null +++ b/packages/unit/test/fn3/widget_tester.dart @@ -0,0 +1,44 @@ +import 'package:sky/src/fn3/framework.dart'; + +class TestComponent extends Component { + TestComponent({ this.child }); + final Widget child; + Widget build() => child; +} + +class WidgetTester { + ComponentElement _rootElement; + + void walkElements(ElementVisitor visitor) { + void walk(Element element) { + visitor(element); + element.visitChildren(walk); + } + + _rootElement.visitChildren(walk); + } + + Element findElement(bool predicate(Element widget)) { + try { + walkElements((Element widget) { + if (predicate(widget)) + throw widget; + }); + } catch (e) { + if (e is Element) + return e; + rethrow; + } + return null; + } + + void pumpFrame(Widget widget) { + if (_rootElement == null) { + _rootElement = new ComponentElement(new TestComponent(child: widget)); + _rootElement.mount(null); + } else { + _rootElement.update(new TestComponent(child: widget), null); + } + } + +}