提交 8a42f9ec 编写于 作者: R Rafael Weinstein

Initial commit of Effen reactive framework experiment for Sky

This is just a proof of concept. If we like this direction, it will move out of the examples directory (likely re-written) and be committed in smaller pieces with unit tests and formal reviews.

TBR=abarth
BUG=

Review URL: https://codereview.chromium.org/971183002
上级 11f48297
Effen (fn)
===
Effen is a prototype of a functional-reactive framework for sky which takes inspiration from [React](http://facebook.github.io/react/). The code as you see it here is a first-draft, is unreviewed, untested and will probably catch your house on fire. It is a proof of concept.
Effen is comprised of three main parts: a virtual-dom and diffing engine, a component mechanism and a very early set of widgets for use in creating applications.
If you just want to dive into code, see the `sky/examples/stocks-fn`.
Is this the official framework for Sky?
---------------------------------------
Nope, it's just an experiment. We're testing how well it works and how we like it.
Hello World
-----------
To build an application, create a subclass of App and instantiate it.
```HTML
<!-- In hello.sky -->
<script>
import 'helloworld.dart';
main() {
new HelloWorldApp();
}
</script>
```
```JavaScript
// In helloworld.dart
import '../fn/lib/fn.dart';
class HelloWorldApp extends App {
Node render() {
return new Text('Hello, World!');
}
}
```
An app is comprised of (and is, itself, a) components. A component's main job is to implement `Node render()`. The idea here is that the `render` method describes the DOM of a component at any given point during its lifetime. In this case, our `HelloWorldApp`'s `render` method just returns a `Text` node which displays the obligatory line of text.
Nodes
-----
A component's `render` method must return a single `Node` which *may* have children (and so on, forming a *subtree*). Effen comes with a few built-in nodes which mirror the built-in nodes/elements of sky: `Text`, `Anchor` (`<a />`, `Image` (`<img />`) and `Container` (`<div />`). `render` can return a tree of Nodes comprised of any of these nodes and plus any other imported object which extends `Component`.
How to structure you app
------------------------
If you're familiar with React, the basic idea is the same: Application data flows *down* from components which have data to components & nodes which they construct via construction parameters. Generally speaking, View-Model data (data which is derived from *model* data, but exists only because the view needs it), is computed during the course of `render` and is short-lived, being handed into nodes & components as configuration data.
What does "data flowing down the tree" mean?
--------------------------------------------
Consider the case of a checkbox. (i.e. `widgets/checkbox.dart`). The `Checkbox` constructor looks like this:
```JavaScript
ValueChanged onChanged;
bool checked;
Checkbox({ Object key, this.onChanged, this.checked }) : super(key: key);
```
What this means is that the `Checkbox` component is *never* "owns" the state of the checkbox. It's current state is handed into the `checked` parameter, and when a click occurs, the checkbox invokes its `onChanged` callback with the value it thinks it should be changed to -- but it never directly changes the value itself. This is a bit odd at first look, but if you think about it: a control isn't very useful unless it gets its value out to someone and if you think about databinding, the same thing happens: databinding basically tells a control to *treat some remote variable as its storage*. That's all that is happening here. In this case, some owning component probably has a set of values which describe a form.
Stateful vs. Stateless components
---------------------------------
All components have access to two kinds of state: (1) data which is handing in from their owner (the component which constructed them) and (2) data which they mutate themselves. While react components have explicit property bags for these two kinds of state (`this.prop` and `this.state`), Effen maps these ideas to the public and private fields of the component. Constructor arguments should (by convention) be reflected as public fields of the component and state should only be set on private (with a leading underbar `_`) fields.
All nodes and most components should be stateless, never needing to mutate themselves and only reacting to data which is handed into them. Some components will be stateful. This state will likely encapsulate transient states of the UI, such as scroll position, animation state, uncommitted form values, etc...
A component can become stateful in two ways: (1) by passing `super(stateful: true)` to its call to the superclasses constructor, or by calling `setState(Function fn)`. The former is a way to have a component start its life stateful, and the later results in the component becoming statefull *as well as* scheduling the component to re-render at the end of the current animation frame.
What does it mean to be stateful? It means that the diffing mechanism retains the specific *instance* of the component as long as the component which renders it continues to require its presence. The component which constructed it may have provided new configuration in form of different values for the constructor parameters, but these values (public fields) will be copied (using reflection) onto the retained instance whose privates fields are left unmodified.
Rendering
---------
At the end of each animation frame, all components (including the root `App`) which have `setState` on themselves will be re-rendered and the resulting changes will be minimally applied to the DOM. Note that components of lower "order" (those near the root of the tree) will render first because their rendering may require re-rendering of higher order (those near the leaves), thus avoiding the possibility that a component which is dirty render more than once during a single cycle.
Keys
----
In order to efficiently apply changes to the DOM and to ensure that stateful components are correctly identified, Effen requires that `no two nodes (except Text) or components of the same type may exist as children of another element without being distinguished by unique keys`. [`Text` is excused from this rule]. In many cases, nodes don't require a key because there is only one type amongst its siblings -- but if there is more one, you must assign each a key. This is why most nodes will take `({ Object key })` as an optional constructor parameter. In development mode (i.e. when sky is built `Debug`) Effen will throw an error if you forget to do this.
Event Handling
--------------
To handle an event is to receive a callback. All elements, (e.g. `Container`, `Anchor`, and `Image`) have optional named constructor arguments named `on*` whose type is function that takes a single `sky.Event` as a parameter. To handle an event, implement a callback on your component and pass it to the appropriate node. If you need to expose the event callback to an owner component, just pipe it through your constructor arguments:
```JavaScript
class MyComp extends Component {
MyComp({
Object key,
sky.EventListener onClick // delegated handler
}) : super(key: key);
Node render() {
return new Container(
onClick: onClick,
onScrollStart: _handleScroll // direct handler
);
}
_handleScroll(sky.Event e) {
setState(() {
// update the scroll position
});
}
}
```
*Note: Only a subset of the events defined in sky are currently exposed on Element. If you need one which isn't present, feel free to post a patch which adds it.*
Styling
-------
Styling is the part of Effen which is least designed and is likely to change. At the moment, there are two ways to apply style to an element: (1) by handing a `Style` object to the `style` constructor parameter, or by passing a `String` to the `inlineStyle` constructor parameter. Both take a string of CSS, but the construction of a `Style` object presently causes a new `<style />` element to be created at the document level which can quickly be applied to components by Effen setting their class -- while inlineStyle does what you would expect.
`Style` objects are for most styling which is static and `inlineStyle`s are for styling which is dynamic (e.g. `display: ` or `transform: translate*()` which may change as a result of animating of transient UI state).
Animation
---------
Animation is still an area of exploration. The pattern which is presently used in the `stocks-fn` example is the following: Components which are animatable should contain within their implementation file an Animation object whose job it is to react to events and control an animation by exposing one or more Dart `stream`s of data. The `Animation` object is owned by the owner (or someone even higher) and the stream is passed into the animating component via its constructor. The first time the component renders, it listens on the stream and calls `setState` on itself for each value which emerges from the stream [See the `drawer.dart` widget for an example].
Performance
-----------
Isn't diffing the DOM expensive? This is kind of a subject question with a few answers, but the biggest issue is what do you mean by "fast"?
The stock answer is that diffing the DOM is fast because you compute the diff of the current VDOM from the previous VDOM and only apply the diffs to the actual DOM. The truth that this is fast, but not really fast enough to re-render everything on the screen for 60 or 120fps animations on a mobile device.
The answer that many people don't get is that there are really two logical types of renders: (1) When underlying model data changes: This generally requires handing in new data to the root component (in Effen, this means the `App` calling `setState` on itself). (2) When user interaction updates a control or an animation takes place. (1) is generally more expensive because it requires a full rendering & diff, but tends to happen infrequently. (2) tends to happen frequently, but at nodes which are near the leafs of the tree, so the number of nodes which must be reconsiled is generally small.
React provides a way to manually insist that a componet not re-render based on its old and new state (and they encourage the use of immutable data structures because discovering the data is the same can be accomplished with a reference comparison). A similar mechanism is in the works for Effen.
Lastly, Effen does something unique: Because its diffing is component-wise, it can be smart about not forcing the re-render of components which are handed in as *arguments* when only the component itself is dirty. For example, the `drawer.dart` component knows how to animate out & back and expose a content pane -- but it takes its content pane as an argument. When the animation mutates the inlineStyle of the `Drawer`'s `Container`, it must schedule itself for re-render -- but -- because the content was handed in to its constructor, its configuration can't have changed and Effen doesn't require it to re-render.
It is a design goal that it should be *possible* to arrange that all "render" cycles which happen during animations can complete in less than one milliesecond on a Nexus 5.
part of fn;
List<Component> _dirtyComponents = new List<Component>();
bool _renderScheduled = false;
void _renderDirtyComponents() {
Stopwatch sw = new Stopwatch()..start();
_dirtyComponents.sort((a, b) => a._order - b._order);
for (var comp in _dirtyComponents) {
comp._renderIfDirty();
}
_dirtyComponents.clear();
_renderScheduled = false;
sw.stop();
print("Render took ${sw.elapsedMicroseconds} microseconds");
}
void _scheduleComponentForRender(Component c) {
_dirtyComponents.add(c);
if (!_renderScheduled) {
_renderScheduled = true;
new Future.microtask(_renderDirtyComponents);
}
}
abstract class Component extends Node {
bool _dirty = true; // components begin dirty because they haven't rendered.
Node _rendered = null;
bool _removed = false;
int _order;
static int _currentOrder = 0;
bool _stateful;
static Component _currentlyRendering;
Component({ Object key, bool stateful })
: _stateful = stateful != null ? stateful : false,
_order = _currentOrder + 1,
super(key:key);
void willUnmount() {}
void _remove() {
assert(_rendered != null);
assert(_root != null);
willUnmount();
_rendered._remove();
_rendered = null;
_root = null;
_removed = true;
}
// TODO(rafaelw): It seems wrong to expose DOM at all. This is presently
// needed to get sizing info.
sky.Node getRoot() => _root;
bool _sync(Node old, sky.Node host, sky.Node insertBefore) {
Component oldComponent = old as Component;
if (oldComponent == null || oldComponent == this) {
_renderInternal(host, insertBefore);
return false;
}
assert(oldComponent != null);
assert(_dirty);
assert(_rendered == null);
if (oldComponent._stateful) {
_stateful = false; // prevent iloop from _renderInternal below.
reflect.copyPublicFields(this, oldComponent);
oldComponent._dirty = true;
_dirty = false;
oldComponent._renderInternal(host, insertBefore);
return true; // Must retain old component
}
_rendered = oldComponent._rendered;
_renderInternal(host, insertBefore);
return false;
}
void _renderInternal(sky.Node host, sky.Node insertBefore) {
if (!_dirty) {
assert(_rendered != null);
return;
}
var oldRendered = _rendered;
int lastOrder = _currentOrder;
_currentOrder = _order;
_currentlyRendering = this;
_rendered = render();
_currentlyRendering = null;
_currentOrder = lastOrder;
_dirty = false;
// TODO(rafaelw): This prevents components from returning different node
// types as their root node at different times. Consider relaxing.
assert(oldRendered == null ||
_rendered.runtimeType == oldRendered.runtimeType);
if (_rendered._sync(oldRendered, host, insertBefore)) {
_rendered = oldRendered; // retain stateful component
}
_root = _rendered._root;
assert(_rendered._root is sky.Node);
}
void _renderIfDirty() {
assert(_rendered != null);
assert(!_removed);
var rendered = _rendered;
while (rendered is Component) {
rendered = rendered._rendered;
}
sky.Node root = rendered._root;
_renderInternal(root.parentNode, root.nextSibling);
}
void setState(Function fn()) {
assert(_rendered != null); // cannot setState before mounting.
_stateful = true;
fn();
if (_currentlyRendering != this) {
_dirty = true;
_scheduleComponentForRender(this);
}
}
Node render();
}
abstract class App extends Component {
sky.Node _host = null;
App()
: super(stateful: true) {
_host = sky.document.createElement('div');
sky.document.appendChild(_host);
new Future.microtask(() {
Stopwatch sw = new Stopwatch()..start();
_sync(null, _host, null);
assert(_root is sky.Node);
sw.stop();
print("Initial render: ${sw.elapsedMicroseconds} microseconds");
});
}
}
import 'dart:async';
void assertHasParentNode(Node n) { assert(n.parentNode != null); }
void assertHasParentNodes(List<Node> list) {
for (var n in list) {
assertHasParentNode(n);
}
}
class Node {
ParentNode parentNode;
Node nextSibling;
Node previousSibling;
Node();
void insertBefore(List<Node> nodes) {
int count = nodes.length;
while (count-- > 0) {
parentNode._insertBefore(nodes[count], this);
}
assertHasParentNodes(nodes);
}
remove() {
if (parentNode == null) {
return;
}
if (nextSibling != null) {
nextSibling.previousSibling = previousSibling;
} else {
parentNode.lastChild = previousSibling;
}
if (previousSibling != null) {
previousSibling.nextSibling = nextSibling;
} else {
parentNode.firstChild = nextSibling;
}
parentNode = null;
nextSibling = null;
previousSibling = null;
}
}
class Text extends Node {
String data;
Text(this.data) : super();
}
class ParentNode extends Node {
Node firstChild;
Node lastChild;
ParentNode() : super();
Node setChild(Node node) {
firstChild = node;
lastChild = node;
node.parentNode = this;
assertHasParentNode(node);
return node;
}
Node _insertBefore(Node node, Node ref) {
assert(ref == null || ref.parentNode == this);
if (node.parentNode != null) {
node.remove();
}
node.parentNode = this;
if (firstChild == null && lastChild == null) {
firstChild = node;
lastChild = node;
} else if (ref == null) {
node.previousSibling = lastChild;
lastChild.nextSibling = node;
lastChild = node;
} else {
if (ref == firstChild) {
assert(ref.previousSibling == null);
firstChild = node;
}
node.previousSibling = ref.previousSibling;
ref.previousSibling = node;
node.nextSibling = ref;
}
assertHasParentNode(node);
return node;
}
Node appendChild(Node node) {
return _insertBefore(node, null);
}
}
class Element extends ParentNode {
void addEventListener(String type, EventListener listener, [bool useCapture = false]) {}
void removeEventListener(String type, EventListener listener) {}
void setAttribute(String name, [String value]) {}
}
class Document extends ParentNode {
Document();
Element createElement(String tagName) {
switch (tagName) {
case 'img' : return new HTMLImageElement();
default : return new Element();
}
}
}
class HTMLImageElement extends Element {
Image();
String src;
Object style = {};
}
class Event {
Event();
}
typedef EventListener(Event event);
void _callRAF(Function fn) {
fn(new DateTime.now().millisecondsSinceEpoch.toDouble());
}
class Window {
int requestAnimationFrame(Function fn) {
new Timer(const Duration(milliseconds: 16), () {
_callRAF(fn);
});
}
void cancelAnimationFrame(int id) {
}
}
Document document = new Document();
Window window = new Window();
library fn;
import 'dart:async';
import 'dart:collection';
import 'dart:sky' as sky;
import 'reflect.dart' as reflect;
part 'component.dart';
part 'node.dart';
part 'style.dart';
bool _checkedMode;
bool debugWarnings() {
void testFn(double i) {}
if (_checkedMode == null) {
_checkedMode = false;
try {
testFn('not a double');
} catch (ex) {
_checkedMode = true;
}
}
return _checkedMode;
}
part of fn;
void parentInsertBefore(sky.ParentNode parent,
sky.Node node,
sky.Node ref) {
if (ref != null) {
ref.insertBefore([node]);
} else {
parent.appendChild(node);
}
}
abstract class Node {
String _key = null;
sky.Node _root = null;
Node({ Object key }) {
_key = key == null ? "$runtimeType" : "$runtimeType-$key";
}
// Return true IFF the old node has *become* the new node (should be
// retained because it is stateful)
bool _sync(Node old, sky.ParentNode host, sky.Node insertBefore);
void _remove() {
assert(_root != null);
_root.remove();
_root = null;
}
}
class Text extends Node {
String data;
// Text nodes are special cases of having non-unique keys (which don't need
// to be assigned as part of the API). Since they are unique in not having
// children, there's little point to reordering, so we always just re-assign
// the data.
Text(this.data) : super(key:'*text*');
bool _sync(Node old, sky.ParentNode host, sky.Node insertBefore) {
if (old == null) {
_root = new sky.Text(data);
parentInsertBefore(host, _root, insertBefore);
return false;
}
_root = old._root;
(_root as sky.Text).data = data;
return false;
}
}
var _emptyList = new List<Node>();
abstract class Element extends Node {
String get _tagName;
Element get _emptyElement;
String inlineStyle;
sky.EventListener onClick;
sky.EventListener onFlingCancel;
sky.EventListener onFlingStart;
sky.EventListener onGestureTap;
sky.EventListener onPointerCancel;
sky.EventListener onPointerDown;
sky.EventListener onPointerMove;
sky.EventListener onPointerUp;
sky.EventListener onScrollEnd;
sky.EventListener onScrollStart;
sky.EventListener onScrollUpdate;
sky.EventListener onWheel;
List<Node> _children = null;
String _className = '';
Element({
Object key,
List<Node> children,
Style style,
this.inlineStyle,
// Events
this.onClick,
this.onFlingCancel,
this.onFlingStart,
this.onGestureTap,
this.onPointerCancel,
this.onPointerDown,
this.onPointerMove,
this.onPointerUp,
this.onScrollEnd,
this.onScrollStart,
this.onScrollUpdate,
this.onWheel
}) : super(key:key) {
_className = style == null ? '': style._className;
_children = children == null ? _emptyList : children;
if (debugWarnings()) {
_debugReportDuplicateIds();
}
}
void _remove() {
super._remove();
if (_children != null) {
for (var child in _children) {
child._remove();
}
}
_children = null;
}
void _debugReportDuplicateIds() {
var idSet = new HashSet<String>();
for (var child in _children) {
if (child is Text) {
continue; // Text nodes all have the same key and are never reordered.
}
if (!idSet.add(child._key)) {
throw '''If multiple (non-Text) nodes of the same type exist as children
of another node, they must have unique keys.''';
}
}
}
void _syncEvent(String eventName, sky.EventListener listener,
sky.EventListener oldListener) {
sky.Element root = _root as sky.Element;
if (listener == oldListener)
return;
if (oldListener != null) {
root.removeEventListener(eventName, oldListener);
}
if (listener != null) {
root.addEventListener(eventName, listener);
}
}
void _syncEvents([Element old]) {
_syncEvent('click', onClick, old.onClick);
_syncEvent('gestureflingcancel', onFlingCancel, old.onFlingCancel);
_syncEvent('gestureflingstart', onFlingStart, old.onFlingStart);
_syncEvent('gesturescrollend', onScrollEnd, old.onScrollEnd);
_syncEvent('gesturescrollstart', onScrollStart, old.onScrollStart);
_syncEvent('gesturescrollupdate', onScrollUpdate, old.onScrollUpdate);
_syncEvent('gesturetap', onGestureTap, old.onGestureTap);
_syncEvent('pointercancel', onPointerCancel, old.onPointerCancel);
_syncEvent('pointerdown', onPointerDown, old.onPointerDown);
_syncEvent('pointermove', onPointerMove, old.onPointerMove);
_syncEvent('pointerup', onPointerUp, old.onPointerUp);
_syncEvent('wheel', onWheel, old.onWheel);
}
void _syncNode([Element old]) {
if (old == null) {
old = _emptyElement;
}
_syncEvents(old);
sky.Element root = _root as sky.Element;
if (_className != old._className) {
root.setAttribute('class', _className);
}
if (inlineStyle != old.inlineStyle) {
root.setAttribute('style', inlineStyle);
}
}
bool _sync(Node old, sky.ParentNode host, sky.Node insertBefore) {
// print("---Syncing children of $_key");
Element oldElement = old as Element;
if (oldElement == null) {
// print("...no oldElement, initial render");
_root = sky.document.createElement(_tagName);
_syncNode();
for (var child in _children) {
child._sync(null, _root, null);
assert(child._root is sky.Node);
}
parentInsertBefore(host, _root, insertBefore);
return false;
}
_root = oldElement._root;
oldElement._root = null;
sky.Element root = (_root as sky.Element);
_syncNode(oldElement);
var startIndex = 0;
var endIndex = _children.length;
var oldChildren = oldElement._children;
var oldStartIndex = 0;
var oldEndIndex = oldChildren.length;
sky.Node nextSibling = null;
Node currentNode = null;
Node oldNode = null;
void sync(int atIndex) {
if (currentNode._sync(oldNode, root, nextSibling)) {
// oldNode was stateful and must be retained.
assert(oldNode != null);
currentNode = oldNode;
_children[atIndex] = currentNode;
}
assert(currentNode._root is sky.Node);
}
// Scan backwards from end of list while nodes can be directly synced
// without reordering.
// print("...scanning backwards");
while (endIndex > startIndex && oldEndIndex > oldStartIndex) {
currentNode = _children[endIndex - 1];
oldNode = oldChildren[oldEndIndex - 1];
if (currentNode._key != oldNode._key) {
break;
}
// print('> syncing matched at: $endIndex : $oldEndIndex');
endIndex--;
oldEndIndex--;
sync(endIndex);
nextSibling = currentNode._root;
}
HashMap<String, Node> oldNodeIdMap = null;
bool oldNodeReordered(String key) {
return oldNodeIdMap != null &&
oldNodeIdMap.containsKey(key) &&
oldNodeIdMap[key] == null;
}
void advanceOldStartIndex() {
oldStartIndex++;
while (oldStartIndex < oldEndIndex &&
oldNodeReordered(oldChildren[oldStartIndex]._key)) {
oldStartIndex++;
}
}
void ensureOldIdMap() {
if (oldNodeIdMap != null)
return;
oldNodeIdMap = new HashMap<String, Node>();
for (int i = oldStartIndex; i < oldEndIndex; i++) {
var node = oldChildren[i];
if (node is! Text) {
oldNodeIdMap.putIfAbsent(node._key, () => node);
}
}
}
bool searchForOldNode() {
if (currentNode is Text)
return false; // Never re-order Text nodes.
ensureOldIdMap();
oldNode = oldNodeIdMap[currentNode._key];
if (oldNode == null)
return false;
oldNodeIdMap[currentNode._key] = null; // mark it reordered.
// print("Reparenting ${currentNode._key}");
parentInsertBefore(root, oldNode._root, nextSibling);
return true;
}
// Scan forwards, this time we may re-order;
// print("...scanning forward");
nextSibling = root.firstChild;
while (startIndex < endIndex && oldStartIndex < oldEndIndex) {
currentNode = _children[startIndex];
oldNode = oldChildren[oldStartIndex];
if (currentNode._key == oldNode._key) {
// print('> syncing matched at: $startIndex : $oldStartIndex');
assert(currentNode.runtimeType == oldNode.runtimeType);
nextSibling = nextSibling.nextSibling;
sync(startIndex);
startIndex++;
advanceOldStartIndex();
continue;
}
oldNode = null;
if (searchForOldNode()) {
// print('> reordered to $startIndex');
} else {
// print('> inserting at $startIndex');
}
sync(startIndex);
startIndex++;
}
// New insertions
oldNode = null;
// print('...processing remaining insertions');
while (startIndex < endIndex) {
// print('> inserting at $startIndex');
currentNode = _children[startIndex];
sync(startIndex);
startIndex++;
}
// Removals
// print('...processing remaining removals');
currentNode = null;
while (oldStartIndex < oldEndIndex) {
oldNode = oldChildren[oldStartIndex];
// print('> ${oldNode._key} removing from $oldEndIndex');
oldNode._remove();
advanceOldStartIndex();
}
oldElement._children = null;
return false;
}
}
class Container extends Element {
String get _tagName => 'div';
static Container _emptyContainer = new Container();
Element get _emptyElement => _emptyContainer;
Container({
Object key,
List<Node> children,
Style style,
String inlineStyle,
sky.EventListener onClick,
sky.EventListener onFlingCancel,
sky.EventListener onFlingStart,
sky.EventListener onGestureTap,
sky.EventListener onPointerCancel,
sky.EventListener onPointerDown,
sky.EventListener onPointerMove,
sky.EventListener onPointerUp,
sky.EventListener onScrollEnd,
sky.EventListener onScrollStart,
sky.EventListener onScrollUpdate,
sky.EventListener onWheel
}) : super(
key: key,
children: children,
style: style,
inlineStyle: inlineStyle,
onClick: onClick,
onFlingCancel: onFlingCancel,
onFlingStart: onFlingStart,
onGestureTap: onGestureTap,
onPointerCancel: onPointerCancel,
onPointerDown: onPointerDown,
onPointerMove: onPointerMove,
onPointerUp: onPointerUp,
onScrollEnd: onScrollEnd,
onScrollStart: onScrollStart,
onScrollUpdate: onScrollUpdate,
onWheel: onWheel
);
}
class Image extends Element {
String get _tagName => 'img';
static Image _emptyImage = new Image();
Element get _emptyElement => _emptyImage;
String src;
int width;
int height;
Image({
Object key,
List<Node> children,
Style style,
String inlineStyle,
sky.EventListener onClick,
sky.EventListener onFlingCancel,
sky.EventListener onFlingStart,
sky.EventListener onGestureTap,
sky.EventListener onPointerCancel,
sky.EventListener onPointerDown,
sky.EventListener onPointerMove,
sky.EventListener onPointerUp,
sky.EventListener onScrollEnd,
sky.EventListener onScrollStart,
sky.EventListener onScrollUpdate,
sky.EventListener onWheel,
this.width,
this.height,
this.src
}) : super(
key: key,
children: children,
style: style,
inlineStyle: inlineStyle,
onClick: onClick,
onFlingCancel: onFlingCancel,
onFlingStart: onFlingStart,
onGestureTap: onGestureTap,
onPointerCancel: onPointerCancel,
onPointerDown: onPointerDown,
onPointerMove: onPointerMove,
onPointerUp: onPointerUp,
onScrollEnd: onScrollEnd,
onScrollStart: onScrollStart,
onScrollUpdate: onScrollUpdate,
onWheel: onWheel
);
void _syncNode([Element old]) {
super._syncNode(old);
Image oldImage = old != null ? old : _emptyImage;
sky.HTMLImageElement skyImage = _root as sky.HTMLImageElement;
if (src != oldImage.src) {
skyImage.src = src;
}
if (width != oldImage.width) {
skyImage.style['width'] = '${width}px';
}
if (height != oldImage.height) {
skyImage.style['height'] = '${height}px';
}
}
}
class Anchor extends Element {
String get _tagName => 'a';
static Anchor _emptyAnchor = new Anchor();
String href;
Anchor({
Object key,
List<Node> children,
Style style,
String inlineStyle,
sky.EventListener onClick,
sky.EventListener onFlingCancel,
sky.EventListener onFlingStart,
sky.EventListener onGestureTap,
sky.EventListener onPointerCancel,
sky.EventListener onPointerDown,
sky.EventListener onPointerMove,
sky.EventListener onPointerUp,
sky.EventListener onScrollEnd,
sky.EventListener onScrollStart,
sky.EventListener onScrollUpdate,
sky.EventListener onWheel,
this.width,
this.height,
this.href
}) : super(
key: key,
children: children,
style: style,
inlineStyle: inlineStyle,
onClick: onClick,
onFlingCancel: onFlingCancel,
onFlingStart: onFlingStart,
onGestureTap: onGestureTap,
onPointerCancel: onPointerCancel,
onPointerDown: onPointerDown,
onPointerMove: onPointerMove,
onPointerUp: onPointerUp,
onScrollEnd: onScrollEnd,
onScrollStart: onScrollStart,
onScrollUpdate: onScrollUpdate,
onWheel: onWheel
);
void _syncNode([Element old]) {
Anchor oldAnchor = old != null ? old as Anchor : _emptyAnchor;
super._syncNode(oldAnchor);
sky.HTMLAnchorElement skyAnchor = _root as sky.HTMLAnchorElement;
if (href != oldAnchor.href) {
skyAnchor.href = href;
}
}
}
library reflect;
import 'dart:mirrors';
import 'dart:collection';
HashMap<ClassMirror, List> _fieldCache = new HashMap<ClassMirror, List>();
List<Symbol> _getPublicFields(ClassMirror mirror) {
var fields = _fieldCache[mirror];
if (fields == null) {
fields = new List<Symbol>();
_fieldCache[mirror] = fields;
while (mirror != null) {
var decls = mirror.declarations;
fields.addAll(decls.keys.where((symbol) {
var mirror = decls[symbol];
if (mirror is! VariableMirror) {
return false;
}
var vMirror = mirror as VariableMirror;
return !vMirror.isPrivate && !vMirror.isStatic && !vMirror.isFinal;
}));
mirror = mirror.superclass;
}
}
return fields;
}
void copyPublicFields(Object source, Object target) {
assert(source.runtimeType == target.runtimeType);
var sourceMirror = reflect(source);
var targetMirror = reflect(target);
for (var symbol in _getPublicFields(sourceMirror.type)) {
targetMirror.setField(symbol, sourceMirror.getField(symbol).reflectee);
}
}
part of fn;
class Style {
final String _className;
static Map<String, Style> _cache = null;
static int nextStyleId = 1;
static String nextClassName(String styles) {
assert(sky.document != null);
var className = "style$nextStyleId";
nextStyleId++;
var styleNode = sky.document.createElement('style');
styleNode.setChild(new sky.Text(".$className { $styles }"));
sky.document.appendChild(styleNode);
return className;
}
factory Style(String styles) {
if (_cache == null) {
_cache = new HashMap<String, Style>();
}
var style = _cache[styles];
if (style == null) {
style = new Style._internal(nextClassName(styles));
_cache[styles] = style;
}
return style;
}
Style._internal(this._className);
}
part of widgets;
class FrameGenerator {
Function onDone;
StreamController _controller;
Stream<double> get onTick => _controller.stream;
int _animationId = 0;
bool _cancelled = false;
FrameGenerator({this.onDone}) {
_controller = new StreamController(
sync: true,
onListen: _scheduleTick,
onCancel: cancel);
}
void cancel() {
if (_cancelled) {
return;
}
if (_animationId != 0) {
sky.window.cancelAnimationFrame(_animationId);
}
_animationId = 0;
_cancelled = true;
if (onDone != null) {
onDone();
}
}
void _scheduleTick() {
assert(_animationId == 0);
_animationId = sky.window.requestAnimationFrame(_tick);
}
void _tick(double timeStamp) {
_animationId = 0;
_controller.add(timeStamp);
if (!_cancelled) {
_scheduleTick();
}
}
}
const double _kFrameTime = 1000 / 60;
class AnimationGenerator extends FrameGenerator {
Stream<double> get onTick => _stream;
final double duration;
final double begin;
final double end;
final Curve curve;
Stream<double> _stream;
AnimationGenerator(this.duration, {
this.begin: 0.0,
this.end: 1.0,
this.curve: linear,
Function onDone
}):super(onDone: onDone) {
double startTime = 0.0;
double targetTime = 0.0;
bool done = false;
_stream = super.onTick.map((timeStamp) {
if (startTime == 0.0) {
startTime = timeStamp;
targetTime = startTime + duration;
}
// Clamp the final frame to target time so we terminate the series with
// 1.0 exactly.
if ((timeStamp - targetTime).abs() <= _kFrameTime) {
return 1.0;
}
return (timeStamp - startTime) / duration;
})
.takeWhile((t) => t <= 1.0)
.map((t) => begin + (end - begin) * curve.transform(t));
}
}
double _evaluateCubic(double a, double b, double m) {
// TODO(abarth): Would Math.pow be faster?
return 3 * a * (1 - m) * (1 - m) * m + 3 * b * (1 - m) * m * m + m * m * m;
}
const double _kCubicErrorBound = 0.001;
abstract class Curve {
double transform(double t);
}
class Linear implements Curve {
const Linear();
double transform(double t) {
return t;
}
}
class Cubic implements Curve {
final double a;
final double b;
final double c;
final double d;
const Cubic(this.a, this.b, this.c, this.d);
double transform(double t) {
if (t == 0.0 || t == 1.0)
return t;
double start = 0.0;
double end = 1.0;
while (true) {
double midpoint = (start + end) / 2;
double estimate = _evaluateCubic(a, c, midpoint);
if ((t - estimate).abs() < _kCubicErrorBound)
return _evaluateCubic(b, d, midpoint);
if (estimate < t)
start = midpoint;
else
end = midpoint;
}
}
}
const Linear linear = const Linear();
const Cubic ease = const Cubic(0.25, 0.1, 0.25, 1.0);
const Cubic easeIn = const Cubic(0.42, 0.0, 1.0, 1.0);
const Cubic easeOut = const Cubic(0.0, 0.0, 0.58, 1.0);
const Cubic easeInOut = const Cubic(0.42, 0.0, 0.58, 1.0);
part of widgets;
class Box extends Component {
static Style _style = new Style('''
display: flex;
flex-direction: column;
border-radius: 4px;
border: 1px solid gray;
margin: 10px;'''
);
static Style _titleStyle = new Style('''
flex: 1;
text-align: center;
font-size: 10px;
padding: 8px 8px 4px 8px;'''
);
static Style _contentStyle = new Style('''
flex: 1;
padding: 4px 8px 8px 8px;'''
);
String title;
List<Node> children;
Box({String key, this.title, this.children }) : super(key: key);
Node render() {
return new Container(
style: _style,
children: [
new Container(
key: 'Title',
style: _titleStyle,
children: [new Text(title)]
),
new Container(
key: 'Content',
style: _contentStyle,
children: children
),
]
);
}
}
part of widgets;
class Button extends ButtonBase {
static Style _style = new Style('''
display: inline-flex;
border-radius: 4px;
justify-content: center;
align-items: center;
border: 1px solid blue;
-webkit-user-select: none;
margin: 5px;'''
);
static Style _highlightStyle = new Style('''
display: inline-flex;
border-radius: 4px;
justify-content: center;
align-items: center;
border: 1px solid blue;
-webkit-user-select: none;
margin: 5px;
background-color: orange;'''
);
Node content;
sky.EventListener onClick;
Button({ Object key, this.content, this.onClick }) : super(key: key);
Node render() {
return new Container(
key: 'Button',
style: _highlight ? _highlightStyle : _style,
onClick: onClick,
onPointerDown: _handlePointerDown,
onPointerUp: _handlePointerUp,
onPointerCancel: _handlePointerCancel,
children: [content]
);
}
}
part of widgets;
abstract class ButtonBase extends Component {
bool _highlight = false;
ButtonBase({ Object key }) : super(key: key);
void _handlePointerDown(_) {
setState(() {
_highlight = true;
});
}
void _handlePointerUp(_) {
setState(() {
_highlight = false;
});
}
void _handlePointerCancel(_) {
setState(() {
_highlight = false;
});
}
}
part of widgets;
class Checkbox extends ButtonBase {
bool checked;
ValueChanged onChanged;
static Style _style = new Style('''
display: flex;
justify-content: center;
align-items: center;
-webkit-user-select: none;
cursor: pointer;
width: 30px;
height: 30px;'''
);
static Style _containerStyle = new Style('''
border: solid 2px;
border-color: rgba(90, 90, 90, 0.25);
width: 10px;
height: 10px;'''
);
static Style _containerHighlightStyle = new Style('''
border: solid 2px;
border-color: rgba(90, 90, 90, 0.25);
width: 10px;
height: 10px;
border-radius: 10px;
background-color: orange;
border-color: orange;'''
);
static Style _uncheckedStyle = new Style('''
top: 0px;
left: 0px;'''
);
static Style _checkedStyle = new Style('''
top: 0px;
left: 0px;
transform: translate(2px, -15px) rotate(45deg);
width: 10px;
height: 20px;
border-style: solid;
border-top: none;
border-left: none;
border-right-width: 2px;
border-bottom-width: 2px;
border-color: #0f9d58;'''
);
Checkbox({ Object key, this.onChanged, this.checked }) : super(key: key);
Node render() {
return new Container(
style: _style,
onClick: _handleClick,
onPointerDown: _handlePointerDown,
onPointerUp: _handlePointerUp,
onPointerCancel: _handlePointerCancel,
children: [
new Container(
style: _highlight ? _containerHighlightStyle : _containerStyle,
children: [
new Container(
style: checked ? _checkedStyle : _uncheckedStyle
)
]
)
]
);
}
void _handleClick(sky.Event e) {
onChanged(!checked);
}
}
part of widgets;
const double _kWidth = 256.0;
const double _kMinFlingVelocity = 0.4;
const double _kMinAnimationDurationMS = 246.0;
const double _kMaxAnimationDurationMS = 600.0;
const Cubic _kAnimationCurve = easeOut;
class DrawerAnimation {
Stream<double> get onPositionChanged => _controller.stream;
StreamController _controller;
AnimationGenerator _animation;
double _position;
bool get _isAnimating => _animation != null;
bool get _isMostlyClosed => _position <= -_kWidth / 2;
DrawerAnimation() {
_controller = new StreamController(sync: true);
_setPosition(-_kWidth);
}
void toggle(_) => _isMostlyClosed ? _open() : _close();
void handleMaskTap(_) => _close();
void handlePointerDown(_) => _cancelAnimation();
void handlePointerMove(sky.PointerEvent event) {
assert(_animation == null);
_setPosition(_position + event.dx);
}
void handlePointerUp(_) {
if (!_isAnimating)
_settle();
}
void handlePointerCancel(_) {
if (!_isAnimating)
_settle();
}
void _open() => _animateToPosition(0.0);
void _close() => _animateToPosition(-_kWidth);
void _settle() => _isMostlyClosed ? _close() : _open();
void _setPosition(double value) {
_position = math.min(0.0, math.max(value, -_kWidth));
_controller.add(_position);
}
void _cancelAnimation() {
if (_animation != null) {
_animation.cancel();
_animation = null;
}
}
void _animate(double duration, double begin, double end, Curve curve) {
_cancelAnimation();
_animation = new AnimationGenerator(duration, begin: begin, end: end,
curve: curve);
_animation.onTick.listen(_setPosition, onDone: () {
_animation = null;
});
}
void _animateToPosition(double targetPosition) {
double distance = (targetPosition - _position).abs();
double duration = math.max(
_kMinAnimationDurationMS,
_kMaxAnimationDurationMS * distance / _kWidth);
_animate(duration, _position, targetPosition, _kAnimationCurve);
}
void handleFlingStart(event) {
double direction = event.velocityX.sign;
double velocityX = event.velocityX.abs() / 1000;
if (velocityX < _kMinFlingVelocity)
return;
double targetPosition = direction < 0.0 ? -_kWidth : 0.0;
double distance = (targetPosition - _position).abs();
double duration = distance / velocityX;
_animate(duration, _position, targetPosition, linear);
}
}
class Drawer extends Component {
static Style _style = new Style('''
position: absolute;
z-index: 2;
top: 0;
left: 0;
bottom: 0;
right: 0;
box-shadow: 0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23);'''
);
static Style _maskStyle = new Style('''
background-color: black;
will-change: opacity;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;'''
);
static Style _contentStyle = new Style('''
background-color: #FAFAFA;
will-change: transform;
position: absolute;
width: 256px;
top: 0;
left: 0;
bottom: 0;'''
);
Stream<double> onPositionChanged;
sky.EventListener handleMaskFling;
sky.EventListener handleMaskTap;
sky.EventListener handlePointerCancel;
sky.EventListener handlePointerDown;
sky.EventListener handlePointerMove;
sky.EventListener handlePointerUp;
List<Node> children;
Drawer({
Object key,
this.onPositionChanged,
this.handleMaskFling,
this.handleMaskTap,
this.handlePointerCancel,
this.handlePointerDown,
this.handlePointerMove,
this.handlePointerUp,
this.children
}) : super(key: key);
double _position = -_kWidth;
bool _listening = false;
void _ensureListening() {
if (_listening)
return;
_listening = true;
onPositionChanged.listen((position) {
setState(() {
_position = position;
});
});
}
Node render() {
_ensureListening();
bool isClosed = _position <= -_kWidth;
String inlineStyle = 'display: ${isClosed ? 'none' : ''}';
String maskInlineStyle = 'opacity: ${(_position / _kWidth + 1) * 0.25}';
String contentInlineStyle = 'transform: translateX(${_position}px)';
return new Container(
style: _style,
inlineStyle: inlineStyle,
onPointerDown: handlePointerDown,
onPointerMove: handlePointerMove,
onPointerUp: handlePointerUp,
onPointerCancel: handlePointerCancel,
children: [
new Container(
key: 'Mask',
style: _maskStyle,
inlineStyle: maskInlineStyle,
onGestureTap: handleMaskTap,
onFlingStart: handleMaskFling
),
new Container(
key: 'Content',
style: _contentStyle,
inlineStyle: contentInlineStyle,
children: children
)
]
);
}
}
part of widgets;
class DrawerHeader extends Component {
static Style _style = new Style('''
display: flex;
flex-direction: column;
height: 140px;
-webkit-user-select: none;
background-color: #E3ECF5;
border-bottom: 1px solid #D1D9E1;
padding-bottom: 7px;
margin-bottom: 8px;'''
);
static Style _spacerStyle = new Style('''
flex: 1'''
);
static Style _labelStyle = new Style('''
padding: 0 16px;
font-family: 'Roboto Medium', 'Helvetica';
color: #212121;'''
);
List<Node> children;
DrawerHeader({ Object key, this.children }) : super(key: key);
Node render() {
return new Container(
style: _style,
children: [
new Container(
key: 'Spacer',
style: _spacerStyle
),
new Container(
key: 'Label',
style: _labelStyle,
children: children
)
]
);
}
}
part of widgets;
abstract class FixedHeightScrollable extends Component {
static Style _style = new Style('''
overflow: hidden;
position: relative;
will-change: transform;'''
);
static Style _scrollAreaStyle = new Style('''
position:relative;
will-change: transform;'''
);
double itemHeight;
double height;
double minOffset;
double maxOffset;
double _scrollOffset = 0.0;
FlingCurve _flingCurve;
int _flingAnimationId;
FixedHeightScrollable({
Object key,
this.itemHeight,
this.height,
this.minOffset,
this.maxOffset
}) : super(key: key) {}
List<Node> renderItems(int start, int count);
Node render() {
int drawCount = (height / itemHeight).round() + 1;
double alignmentDelta = -_scrollOffset % itemHeight;
if (alignmentDelta != 0.0) {
alignmentDelta -= itemHeight;
}
double drawStart = _scrollOffset + alignmentDelta;
int itemNumber = (drawStart / itemHeight).floor();
var transformStyle =
'transform: translateY(${(alignmentDelta).toStringAsFixed(2)}px)';
var items = renderItems(itemNumber, drawCount);
return new Container(
style: _style,
onFlingStart: _handleFlingStart,
onFlingCancel: _handleFlingCancel,
onScrollUpdate: _handleScrollUpdate,
onWheel: _handleWheel,
children: [
new Container(
style: _scrollAreaStyle,
inlineStyle: transformStyle,
children: items
)
]
);
}
void willUnmount() {
_stopFling();
}
bool _scrollBy(double scrollDelta) {
var newScrollOffset = _scrollOffset + scrollDelta;
if (minOffset != null && newScrollOffset < minOffset) {
newScrollOffset = minOffset;
} else if (maxOffset != null && newScrollOffset > maxOffset) {
newScrollOffset = maxOffset;
}
if (newScrollOffset == _scrollOffset) {
return false;
}
setState(() {
_scrollOffset = newScrollOffset;
});
return true;
}
void _scheduleFlingUpdate() {
_flingAnimationId = sky.window.requestAnimationFrame(_updateFling);
}
void _stopFling() {
if (_flingAnimationId == null) {
return;
}
sky.window.cancelAnimationFrame(_flingAnimationId);
_flingCurve = null;
_flingAnimationId = null;
}
void _updateFling(double timeStamp) {
double scrollDelta = _flingCurve.update(timeStamp);
if (!_scrollBy(scrollDelta))
return _stopFling();
_scheduleFlingUpdate();
}
void _handleScrollUpdate(sky.Event event) {
_scrollBy(-event.dy);
}
void _handleFlingStart(sky.Event event) {
setState(() {
_flingCurve = new FlingCurve(-event.velocityY, event.timeStamp);
_scheduleFlingUpdate();
});
}
void _handleFlingCancel(sky.Event event) {
_stopFling();
}
void _handleWheel(sky.Event event) {
_scrollBy(-event.offsetY);
}
}
part of widgets;
// 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.
const double _kDefaultAlpha = -5707.62;
const double _kDefaultBeta = 172.0;
const double _kDefaultGamma = 3.7;
double _positionAtTime(double t) {
return _kDefaultAlpha * math.exp(-_kDefaultGamma * t)
- _kDefaultBeta * t
- _kDefaultAlpha;
}
double _velocityAtTime(double t) {
return -_kDefaultAlpha * _kDefaultGamma * math.exp(-_kDefaultGamma * t)
- _kDefaultBeta;
}
double _timeAtVelocity(double v) {
return -math.log((v + _kDefaultBeta) / (-_kDefaultAlpha * _kDefaultGamma))
/ _kDefaultGamma;
}
final double _kMaxVelocity = _velocityAtTime(0.0);
final double _kCurveDuration = _timeAtVelocity(0.0);
class FlingCurve {
double _timeOffset;
double _positionOffset;
double _startTime;
double _previousPosition;
double _direction;
FlingCurve(double velocity, double startTime) {
double startingVelocity = math.min(_kMaxVelocity, velocity.abs());
_timeOffset = _timeAtVelocity(startingVelocity);
_positionOffset = _positionAtTime(_timeOffset);
_startTime = startTime / 1000.0;
_previousPosition = 0.0;
_direction = velocity.sign;
}
double update(double timeStamp) {
double t = timeStamp / 1000.0 - _startTime + _timeOffset;
if (t >= _kCurveDuration)
return 0.0;
double position = _positionAtTime(t) - _positionOffset;
double positionDelta = position - _previousPosition;
_previousPosition = position;
return _direction * math.max(0.0, positionDelta);
}
}
part of widgets;
const String kAssetBase = '/sky/assets/material-design-icons';
class Icon extends Component {
Style style;
int size;
String type;
sky.EventListener onClick;
Icon({
String key,
this.style,
this.size,
this.type: '',
this.onClick
}) : super(key: key);
Node render() {
String category = '';
String subtype = '';
List<String> parts = type.split('/');
if (parts.length == 2) {
category = parts[0];
subtype = parts[1];
}
return new Image(
style: style,
onClick: onClick,
width: size,
height: size,
src: '${kAssetBase}/${category}/2x_web/ic_${subtype}_${size}dp.png'
);
}
}
part of widgets;
const double _kSplashSize = 400.0;
const double _kSplashDuration = 500.0;
class SplashAnimation {
AnimationGenerator _animation;
double _offsetX;
double _offsetY;
Stream<String> _styleChanged;
Stream<String> get onStyleChanged => _styleChanged;
void cancel() => _animation.cancel();
SplashAnimation(sky.ClientRect rect, double x, double y,
{ Function onDone })
: _offsetX = x - rect.left,
_offsetY = y - rect.top {
_animation = new AnimationGenerator(_kSplashDuration,
end: _kSplashSize, curve: easeOut, onDone: onDone);
_styleChanged = _animation.onTick.map((p) => '''
top: ${_offsetY - p/2}px;
left: ${_offsetX - p/2}px;
width: ${p}px;
height: ${p}px;
border-radius: ${p}px;
opacity: ${1.0 - (p / _kSplashSize)};
''');
}
}
class InkSplash extends Component {
Stream<String> onStyleChanged;
static Style _style = new Style('''
position: absolute;
pointer-events: none;
overflow: hidden;
top: 0;
left: 0;
bottom: 0;
right: 0;
''');
static Style _splashStyle = new Style('''
position: absolute;
background-color: rgba(0, 0, 0, 0.4);
border-radius: 0;
top: 0;
left: 0;
height: 0;
width: 0;
''');
double _offsetX;
double _offsetY;
String _inlineStyle;
InkSplash(Stream<String> onStyleChanged)
: onStyleChanged = onStyleChanged,
super(stateful: true, key: onStyleChanged.hashCode);
bool _listening = false;
void _ensureListening() {
if (_listening)
return;
_listening = true;
onStyleChanged.listen((style) {
setState(() {
_inlineStyle = style;
});
});
}
Node render() {
_ensureListening();
return new Container(
style: _style,
children: [
new Container(
inlineStyle: _inlineStyle,
style: _splashStyle
)
]
);
}
}
library item;
import 'dart:sky' as sky;
import 'fn.dart';
import 'widgets.dart';
enum Color { RED, GREEN }
class Item extends Component {
String label;
Color _color = Color.GREEN;
Item({ Object key, this.label }) : super(key: key);
Node render() {
return new Container(
children: [
new Radio(
onChanged: changed,
value: Color.GREEN,
groupValue: _color
),
new Radio(
onChanged: changed,
value: Color.RED,
groupValue: _color
),
new Text("$label: ${Color.values[_color.index]}")
]
);
}
void changed(Object value) {
setState(() {
_color = value;
});
}
}
part of widgets;
class MenuDivider extends Component {
static Style _style = new Style('''
margin: 8px 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.12);'''
);
MenuDivider({ Object key }) : super(key: key);
Node render() {
return new Container(
style: _style
);
}
}
part of widgets;
class MenuItem extends Component {
static Style _style = new Style('''
display: flex;
align-items: center;
height: 48px;
-webkit-user-select: none;'''
);
static Style _iconStyle = new Style('''
padding: 0px 16px;'''
);
static Style _labelStyle = new Style('''
font-family: 'Roboto Medium', 'Helvetica';
color: #212121;
padding: 0px 16px;
flex: 1;'''
);
List<Node> children;
String icon;
MenuItem({ Object key, this.icon, this.children }) : super(key: key) {
}
Node render() {
return new Container(
style: _style,
children: [
new Icon(
style: _iconStyle,
size: 24,
type: "${icon}_grey600"
),
new Container(
style: _labelStyle,
children: children
)
]
);
}
}
part of widgets;
class Radio extends ButtonBase {
Object value;
Object groupValue;
ValueChanged onChanged;
static Style _style = new Style('''
display: inline-block;
-webkit-user-select: none;
width: 14px;
height: 14px;
border-radius: 7px;
border: 1px solid blue;
margin: 0 5px;'''
);
static Style _highlightStyle = new Style('''
display: inline-block;
-webkit-user-select: none;
width: 14px;
height: 14px;
border-radius: 7px;
border: 1px solid blue;
margin: 0 5px;
background-color: orange;'''
);
static Style _dotStyle = new Style('''
-webkit-user-select: none;
width: 10px;
height: 10px;
border-radius: 5px;
background-color: black;
margin: 2px;'''
);
Radio({
Object key,
this.onChanged,
this.value,
this.groupValue
}) : super(key: key);
Node render() {
return new Container(
style: _highlight ? _highlightStyle : _style,
onClick: _handleClick,
onPointerDown: _handlePointerDown,
onPointerUp: _handlePointerUp,
onPointerCancel: _handlePointerCancel,
children: value == groupValue ?
[new Container( style : _dotStyle )] : null
);
}
void _handleClick(sky.Event e) {
onChanged(value);
}
}
part of widgets;
class Toolbar extends Component {
List<Node> children;
static Style _style = new Style('''
display: flex;
align-items: center;
height: 84px;
z-index: 1;
background-color: #3F51B5;
color: white;
box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);'''
);
Toolbar({String key, this.children}) : super(key: key);
Node render() {
return new Container(
style: _style,
children: children
);
}
}
library widgets;
import '../lib/fn.dart';
import 'dart:async';
import 'dart:math' as math;
import 'dart:sky' as sky;
part 'animationgenerator.dart';
part 'box.dart';
part 'button.dart';
part 'buttonbase.dart';
part 'checkbox.dart';
part 'drawer.dart';
part 'drawerheader.dart';
part 'fixedheightscrollable.dart';
part 'flingcurve.dart';
part 'icon.dart';
part 'inksplash.dart';
part 'menudivider.dart';
part 'menuitem.dart';
part 'radio.dart';
part 'toolbar.dart';
typedef void ValueChanged(value);
此差异已折叠。
part of stocksapp;
class StockArrow extends Component {
double percentChange;
static Style _style = new Style('''
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 40px;
margin-right: 16px;
border: 1px solid transparent;'''
);
static Style _upStyle = new Style('''
width: 0;
height: 0;
border-left: 9px solid transparent;
border-right: 9px solid transparent;
margin-bottom: 3px;
border-bottom: 9px solid white;'''
);
static Style _downStyle = new Style('''
width: 0;
height: 0;
border-left: 9px solid transparent;
border-right: 9px solid transparent;
margin-top: 3px;
border-top: 9px solid white'''
);
StockArrow({ Object key, this.percentChange }) : super(key: key);
final List<String> _kRedColors = [
'#E57373',
'#EF5350',
'#F44336',
'#E53935',
'#D32F2F',
'#C62828',
'#B71C1C',
];
final List<String> _kGreenColors = [
'#81C784',
'#66BB6A',
'#4CAF50',
'#43A047',
'#388E3C',
'#2E7D32',
'#1B5E20',
];
int _colorIndexForPercentChange(double percentChange) {
// Currently the max is 10%.
double maxPercent = 10.0;
return max(0, ((percentChange.abs() / maxPercent) * _kGreenColors.length).floor());
}
String _colorForPercentChange(double percentChange) {
if (percentChange > 0)
return _kGreenColors[_colorIndexForPercentChange(percentChange)];
return _kRedColors[_colorIndexForPercentChange(percentChange)];
}
Node render() {
String border = _colorForPercentChange(percentChange).toString();
bool up = percentChange > 0;
String type = up ? 'bottom' : 'top';
return new Container(
inlineStyle: 'border-color: $border',
style: _style,
children: [
new Container(
inlineStyle: 'border-$type-color: $border',
style: up ? _upStyle : _downStyle
)
]
);
}
}
part of stocksapp;
class Stocklist extends FixedHeightScrollable {
List<Stock> stocks;
Stocklist({
Object key,
this.stocks
}) : super(key: key, itemHeight: 80.0, height: 800.0, minOffset: 0.0);
List<Node> renderItems(int start, int count) {
var items = [];
for (var i = 0; i < count; i++) {
items.add(new StockRow(stock: stocks[start + i]));
}
return items;
}
}
part of stocksapp;
class StockRow extends Component {
Stock stock;
LinkedHashSet<SplashAnimation> _splashes;
static Style _style = new Style('''
transform: translateX(0);
max-height: 48px;
display: flex;
align-items: center;
border-bottom: 1px solid #F4F4F4;
padding-top: 16px;
padding-left: 16px;
padding-right: 16px;
padding-bottom: 20px;'''
);
static Style _tickerStyle = new Style('''
flex: 1;
font-family: 'Roboto Medium', 'Helvetica';'''
);
static Style _lastSaleStyle = new Style('''
text-align: right;
padding-right: 16px;'''
);
static Style _changeStyle = new Style('''
color: #8A8A8A;
text-align: right;'''
);
StockRow({Stock stock}) : super(key: stock.symbol) {
this.stock = stock;
}
Node render() {
String lastSale = "\$${stock.lastSale.toStringAsFixed(2)}";
String changeInPrice = "${stock.percentChange.toStringAsFixed(2)}%";
if (stock.percentChange > 0)
changeInPrice = "+" + changeInPrice;
List<Node> children = [
new StockArrow(
percentChange: stock.percentChange
),
new Container(
key: 'Ticker',
style: _tickerStyle,
children: [new Text(stock.symbol)]
),
new Container(
key: 'LastSale',
style: _lastSaleStyle,
children: [new Text(lastSale)]
),
new Container(
key: 'Change',
style: _changeStyle,
children: [new Text(changeInPrice)]
)
];
if (_splashes != null) {
children.addAll(_splashes.map((s) => new InkSplash(s.onStyleChanged)));
}
return new Container(
style: _style,
onScrollStart: _cancelSplashes,
onWheel: _cancelSplashes,
onPointerDown: _handlePointerDown,
children: children
);
}
sky.ClientRect _getBoundingRect() => getRoot().getBoundingClientRect();
void _handlePointerDown(sky.Event event) {
setState(() {
if (_splashes == null) {
_splashes = new LinkedHashSet<SplashAnimation>();
}
var splash;
splash = new SplashAnimation(_getBoundingRect(), event.x, event.y,
onDone: () { _splashDone(splash); });
_splashes.add(splash);
});
}
void _cancelSplashes(sky.Event event) {
if (_splashes == null) {
return;
}
setState(() {
var splashes = _splashes;
_splashes = null;
splashes.forEach((s) { s.cancel(); });
});
}
void willUnmount() {
_cancelSplashes(null);
}
void _splashDone(SplashAnimation splash) {
if (_splashes == null) {
return;
}
setState(() {
_splashes.remove(splash);
if (_splashes.length == 0) {
_splashes = null;
}
});
}
}
#!mojo mojo:sky_viewer
<sky>
<script>
import 'stocksapp.dart';
main() {
new StocksApp();
}
</script>
</sky>
library stocksapp;
import '../fn/lib/fn.dart';
import '../fn/widgets/widgets.dart';
import 'dart:collection';
import 'dart:math';
import 'dart:sky' as sky;
part 'companylist.dart';
part 'stockarrow.dart';
part 'stocklist.dart';
part 'stockrow.dart';
class StocksApp extends App {
DrawerAnimation _drawerAnimation = new DrawerAnimation();
static Style _style = new Style('''
display: flex;
flex-direction: column;
height: -webkit-fill-available;
font-family: 'Roboto Regular', 'Helvetica';
font-size: 16px;'''
);
static Style _iconStyle = new Style('''
padding: 8px;
margin: 0 4px;'''
);
static Style _titleStyle = new Style('''
flex: 1;
margin: 0 4px;'''
);
StocksApp() : super();
Node render() {
var drawer = new Drawer(
onPositionChanged: _drawerAnimation.onPositionChanged,
handleMaskFling: _drawerAnimation.handleFlingStart,
handleMaskTap: _drawerAnimation.handleMaskTap,
handlePointerCancel: _drawerAnimation.handlePointerCancel,
handlePointerDown: _drawerAnimation.handlePointerDown,
handlePointerMove: _drawerAnimation.handlePointerMove,
handlePointerUp: _drawerAnimation.handlePointerUp,
children: [
new DrawerHeader(
children: [new Text('Stocks')]
),
new MenuItem(
key: 'Inbox',
icon: 'content/inbox',
children: [new Text('Inbox')]
),
new MenuDivider(
),
new MenuItem(
key: 'Drafts',
icon: 'content/drafts',
children: [new Text('Drafts')]
),
new MenuItem(
key: 'Settings',
icon: 'action/settings',
children: [new Text('Settings')]
),
new MenuItem(
key: 'Help & Feedback',
icon: 'action/help',
children: [new Text('Help & Feedback')]
)
]
);
var toolbar = new Toolbar(
children: [
new Icon(key: 'menu', style: _iconStyle,
onClick: _drawerAnimation.toggle,
size: 24,
type: 'navigation/menu_white'),
new Container(
style: _titleStyle,
children: [new Text('I am a stocks app')]
),
new Icon(key: 'search', style: _iconStyle,
size: 24,
type: 'action/search_white'),
new Icon(key: 'more_white', style: _iconStyle,
size: 24,
type: 'navigation/more_vert_white')
]
);
return new Container(
key: 'StocksApp',
style: _style,
children: [drawer, toolbar, new Stocklist(stocks: oracle.stocks)]
);
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册