Merge "baku/todos: adds basic CRUD features"
diff --git a/examples/todos/flutter.yaml b/examples/todos/flutter.yaml
index 5f2e40e..fe54c87 100644
--- a/examples/todos/flutter.yaml
+++ b/examples/todos/flutter.yaml
@@ -1,3 +1,8 @@
 name: todos
 material-design-icons:
+  - name: action/check_circle
+  - name: action/delete
   - name: content/add
+  - name: content/clear
+  - name: navigation/arrow_back
+  - name: navigation/close
diff --git a/examples/todos/lib/components/todo_collection.dart b/examples/todos/lib/components/todo_collection.dart
new file mode 100644
index 0000000..f3baf81
--- /dev/null
+++ b/examples/todos/lib/components/todo_collection.dart
@@ -0,0 +1,69 @@
+// Copyright 2015 The Vanadium 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:flutter/material.dart';
+
+import 'todo_item.dart';
+import 'todo_dialog.dart';
+import '../todo_model.dart';
+
+/// [TodoCollection] is a [StatefulComponent] component.
+///
+/// This component is responsible for holding a list of TodoModel instances
+/// and rendering the list appropriately. A Todo list fills the whole screen
+/// and is composed of a tool bar, a list view, and the floating action button
+/// for adding a new Todo item.
+class TodoCollection extends StatefulComponent {
+  TodoCollectionState createState() => new TodoCollectionState();
+}
+
+class TodoCollectionState extends State<TodoCollection> {
+  List<TodoModel> todos = new List<TodoModel>();
+
+  void removeTodo(TodoModel todo) {
+    setState(() {
+      todos.remove(todo);
+    });
+  }
+
+  void addTodo(String title) {
+    // Use `setState(fn)` to signal to the framework that some internal state
+    // has been changed and a needs to be rebuilt. If the state update is not
+    // wrapped in a `setState` call the change will not be displayed to the
+    // screen.
+    setState(() {
+      todos.add(new TodoModel(title: title));
+    });
+  }
+
+  Widget buildTodo(BuildContext context, int index) {
+    if (index >= todos.length) {
+      return null;
+    }
+
+    TodoModel todo = todos[index];
+
+    return new TodoItem(
+        // Key is required here to support a requirement of child
+        // ScrollableMixedWidgetList.
+        key: todo.key,
+        todo: todo,
+        onDelete: removeTodo);
+  }
+
+  Widget build(BuildContext context) {
+    return new Scaffold(
+        toolBar: new ToolBar(center: new Text('TODO List')),
+        // Avoiding MaterialList/ScrollableList here since children
+        // would be required to have a pre-defined height via the contstructor
+        // property itemExtent.
+        // SEE: http://goo.gl/cB5bOE
+        body: new ScrollableMixedWidgetList(
+            builder: buildTodo, token: todos.length),
+        floatingActionButton: new FloatingActionButton(
+            child: new Icon(icon: 'content/add'),
+            onPressed: () => showDialog(
+                context: context, child: new TodoDialog(onSave: addTodo))));
+  }
+}
diff --git a/examples/todos/lib/components/todo_dialog.dart b/examples/todos/lib/components/todo_dialog.dart
new file mode 100644
index 0000000..3c542b7
--- /dev/null
+++ b/examples/todos/lib/components/todo_dialog.dart
@@ -0,0 +1,81 @@
+// Copyright 2015 The Vanadium 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:flutter/material.dart';
+
+typedef void TodoHandleSave(Todo);
+
+class TodoDialog extends StatefulComponent {
+  TodoDialog({Key key, this.value: "", this.title: "Add Todo", this.onSave})
+      : super(key: key);
+
+  final String title;
+  final TodoHandleSave onSave;
+  final String value;
+
+  TodoDialogState createState() => new TodoDialogState();
+}
+
+class TodoDialogState extends State<TodoDialog> {
+  String value;
+
+  @override
+  void initState() {
+    super.initState();
+    value = config.value;
+  }
+
+  void handleInputChange(String update) {
+    value = update;
+  }
+
+  // The Input widget gets different values on change versus on submit, be
+  // sure to update the todo with the most recent value.
+  void handleInputSubmit(String value) {
+    handleInputChange(value);
+    save();
+  }
+
+  void close() {
+    Navigator.pop(context);
+  }
+
+  void save() {
+    close();
+    config.onSave(value);
+  }
+
+  static final GlobalKey inputKey = new GlobalKey(debugLabel: 'todo input');
+
+  Widget build(BuildContext context) {
+    Text label = new Text("What needs to get done?");
+    Input input = new Input(
+        key: inputKey,
+        initialValue: value,
+        onChanged: handleInputChange,
+        onSubmitted: handleInputSubmit);
+    List children = <Widget>[label, input];
+
+    return new Scaffold(
+        toolBar: buildToolbar(),
+        body: new Container(
+            margin: new EdgeDims.all(24.0), child: new Block(children)));
+  }
+
+  Widget buildToolbar() {
+    return new ToolBar(
+        left: new IconButton(icon: "navigation/close", onPressed: close),
+        center: new Text(config.title),
+        right: <Widget>[
+          // NOTE: The right spacing is off here.
+          new FlatButton(
+              child: new Text('SAVE'),
+              // NOTE: without this the button text color is black instead of
+              // white. Is there a correct/canonical way to reach into the
+              // theme and grab the toolbar text color to use here?
+              textColor: Colors.white,
+              onPressed: save)
+        ]);
+  }
+}
diff --git a/examples/todos/lib/components/todo_item.dart b/examples/todos/lib/components/todo_item.dart
new file mode 100644
index 0000000..51f4ce6
--- /dev/null
+++ b/examples/todos/lib/components/todo_item.dart
@@ -0,0 +1,252 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+import 'dart:ui' as ui;
+import 'package:flutter/material.dart';
+import 'package:flutter/painting.dart';
+import 'package:flutter/rendering.dart';
+import 'package:flutter/gestures.dart';
+import 'package:flutter/animation.dart';
+
+import "../todo_model.dart";
+
+const Duration _todoSnapDuration = const Duration(milliseconds: 200);
+
+typedef void TodoHandleDelete(TodoModel);
+
+enum TodoStatus { idle, completing, deleting, }
+
+class TodoItem extends StatefulComponent {
+  TodoItem({Key key, this.todo, this.onDelete}) : super(key: key) {
+    assert(todo != null);
+    assert(onDelete != null);
+  }
+
+  final TodoModel todo;
+  final TodoHandleDelete onDelete;
+
+  TodoItemState createState() => new TodoItemState();
+}
+
+// TODO(jasoncampbell): Create a throttle animation so that dragging
+// appears to slow down as the item gets closer to the threshold.
+class TodoItemState extends State<TodoItem> {
+  HorizontalDragGestureRecognizer drag;
+  ValuePerformance<double> snapPerformance;
+
+  TodoItemState() {
+    drag = new HorizontalDragGestureRecognizer(
+        router: Gesturer.instance.pointerRouter,
+        gestureArena: Gesturer.instance.gestureArena)
+      ..onStart = handleDragStart
+      ..onUpdate = handleDragUpdate
+      ..onEnd = handleDragEnd;
+
+    snapPerformance = new ValuePerformance<double>(
+        variable: snapValue, duration: _todoSnapDuration)
+      ..addStatusListener(handleSnapAnimationStatus);
+  }
+
+  TodoModel todo;
+  TodoStatus status;
+  Size size = new Size(ui.window.size.width, 0.0);
+  double position = 0.0;
+  double lastDelta;
+
+  final double dragThreshold = ui.window.size.width / 2 * 0.75;
+
+  final AnimatedValue<double> snapValue =
+      new AnimatedValue<double>(1.0, end: 0.0, curve: Curves.easeOut);
+
+  /// Override `initState()`.
+  @override
+  void initState() {
+    super.initState();
+
+    todo = config.todo;
+  }
+
+  void dispose() {
+    snapPerformance?.stop();
+    super.dispose();
+  }
+
+  void handleSnapAnimationStatus(PerformanceStatus update) {
+    if (update == PerformanceStatus.completed) {
+      setState(() {
+        snapPerformance.progress = 0.0;
+        position = 0.0;
+
+        // Make sure the right thing happens after the snap animation.
+        switch (status) {
+          case TodoStatus.completing:
+            todo.completed = true;
+            status = TodoStatus.idle;
+            break;
+          case TodoStatus.deleting:
+            config.onDelete(todo);
+            break;
+          default:
+            status = TodoStatus.idle;
+        }
+      });
+    }
+  }
+
+  void handlePointerDown(PointerDownEvent event) {
+    drag.addPointer(event);
+  }
+
+  void handleDragStart(Point globalPosition) {
+    position = 0.0;
+  }
+
+  void handleDragUpdate(double delta) {
+    double update = position + delta;
+
+    // NOTE: My eyes might be playing tricks on me but I think this prevents
+    // some jitter while dragging a Todo horizonatlly.
+    if (position != update) {
+      bool pastThreshold = update.abs() > dragThreshold;
+
+      // Prevent the drag from moving past the treshold.
+      if (!pastThreshold) {
+        setState(() {
+          lastDelta = delta;
+          position = update;
+
+          double statusThreshold = dragThreshold / 2;
+
+          // Update status based on the current position.
+          if (position.abs() > statusThreshold) {
+            if (position.isNegative) {
+              // Dragging to the left (revealing the delete icon).
+              status = TodoStatus.deleting;
+            } else {
+              // Dragging to the right (revealing the complete icon).
+              status = TodoStatus.completing;
+            }
+          } else {
+            // When dragging back and forth between the original position and
+            // the status threshold return the item to the idle state.
+            status = TodoStatus.idle;
+          }
+        });
+      }
+    }
+  }
+
+  void handleDragEnd(Offset velocity) {
+    snapPerformance.play();
+  }
+
+  void handleResize(Size update) {
+    setState(() {
+      size = update;
+    });
+  }
+
+  Widget build(BuildContext context) {
+    Widget overlay = new BuilderTransition(
+        variables: <AnimatedValue<double>>[snapPerformance.variable],
+        performance: snapPerformance.view, builder: (BuildContext context) {
+      double left = snapValue.value.clamp(0.0, 1.0) * position;
+
+      return new Positioned(
+          width: size.width,
+          left: left,
+          child: new TodoItemBody(todo: todo, status: status));
+    });
+
+    List<Widget> children = [
+      // NOTE: This instance of TodoItemBody will never be seen, it is used
+      // entirely for sizing purposes and appears behind the action icons
+      // below. The visible instance is inside of a positioned element on top
+      // of the stack and as a result will lack a hight constraint.
+      new TodoItemBody(todo: todo, onResize: handleResize),
+    ];
+
+    Widget background = new Container(
+        decoration: new BoxDecoration(
+            backgroundColor: Theme.of(context).canvasColor,
+            border: new Border(
+                bottom: new BorderSide(
+                    color: Theme.of(context).dividerColor, width: 1.0))),
+        height: size.height,
+        child: new Row(<Widget>[
+          new Flexible(
+              child: new Container(
+                  child: new Align(
+                      child: new Icon(
+                          icon: "action/check_circle",
+                          color: IconThemeColor.white),
+                      alignment: const FractionalOffset(0.0, 0.5)),
+                  decoration: new BoxDecoration(
+                      backgroundColor: Colors.greenAccent[100]),
+                  padding: const EdgeDims.all(24.0))),
+          new Flexible(
+              child: new Container(
+                  child: new Align(
+                      child: new Icon(
+                          icon: "action/delete", color: IconThemeColor.white),
+                      alignment: const FractionalOffset(1.0, 0.5)),
+                  decoration: new BoxDecoration(
+                      backgroundColor: Colors.redAccent[100]),
+                  padding: const EdgeDims.all(24.0)))
+        ]));
+
+    children.add(background);
+    children.add(overlay);
+
+    return new Listener(
+        onPointerDown: handlePointerDown,
+        behavior: HitTestBehavior.translucent,
+        child: new Stack(children));
+  }
+}
+
+typedef void TodoHandleResize(Size);
+
+class TodoItemBody extends StatelessComponent {
+  TodoItemBody({Key key, this.todo, this.status, this.onResize});
+
+  final TodoModel todo;
+  final TodoHandleResize onResize;
+  final TodoStatus status;
+
+  void maybeCallResize(Size size) {
+    if (onResize != null) {
+      onResize(size);
+    }
+  }
+
+  Widget build(BuildContext context) {
+    Color backgroundColor;
+
+    switch (status) {
+      case TodoStatus.completing:
+        backgroundColor = Colors.greenAccent[100];
+        break;
+      case TodoStatus.deleting:
+        backgroundColor = Colors.redAccent[100];
+        break;
+      default:
+        backgroundColor = Theme.of(context).canvasColor;
+    }
+
+    String message = todo.title;
+
+    if (todo.completed) {
+      message = "COMPLETED: ${todo.title}";
+    }
+
+    return new SizeObserver(
+        onSizeChanged: maybeCallResize,
+        child: new Container(
+            decoration: new BoxDecoration(backgroundColor: backgroundColor),
+            child: new Padding(
+                padding: const EdgeDims.all(24.0),
+                child: new Text(message))));
+  }
+}
diff --git a/examples/todos/lib/main.dart b/examples/todos/lib/main.dart
index e0cf583..6e40cfa 100644
--- a/examples/todos/lib/main.dart
+++ b/examples/todos/lib/main.dart
@@ -4,6 +4,8 @@
 
 import 'package:flutter/material.dart';
 
+import './components/todo_collection.dart';
+
 /// Start the Todo application.
 ///
 /// Use the Flutter `runApp` function render this application's widget tree
@@ -21,10 +23,9 @@
 /// composition of the widget tree it can inherit from `StatelessComponent`.
 class App extends StatelessComponent {
   final ThemeData theme = new ThemeData(
-    brightness: ThemeBrightness.light,
-    primarySwatch: Colors.indigo,
-    accentColor: Colors.pinkAccent[200]
-  );
+      brightness: ThemeBrightness.light,
+      primarySwatch: Colors.indigo,
+      accentColor: Colors.pinkAccent[200]);
 
   /// Build a `MaterialApp` widget.
   ///
@@ -32,86 +33,8 @@
   /// only an empty Todo List is rendered as the single, default route.
   Widget build(BuildContext context) {
     return new MaterialApp(
-      title: 'TODO',
-      theme: theme,
-      routes: {'/': (RouteArguments args) => new TodoList()}
-    );
-  }
-}
-
-/// `TodoList` is a `StatefulComponent` component.
-///
-/// This component is responsible for holding a list of `Todo` items and
-/// rendering the list appropriately. A Todo list fills the whole screen
-/// and is composed of a tool bar, a list view, and the floating
-/// action button for adding a new Todo item.
-class TodoList extends StatefulComponent {
-  TodoListState createState() => new TodoListState();
-}
-
-class TodoListState extends State<TodoList> {
-  List<Todo> todos = new List<Todo>();
-
-  void addTodo() {
-    // Use `setState(fn)` to signal to the framework that some internal state
-    // has been changed and a needs to be rebuilt. If the state update is not
-    // wrapped in a `setState` call the change will not be displayed to the
-    // screen.
-    setState(() {
-      todos.add(new Todo(title: 'FAB Item #${todos.length}.'));
-    });
-  }
-
-  Widget build(BuildContext context) {
-    return new Scaffold(
-      toolBar: new ToolBar(
-        center: new Text('TODO List')
-      ),
-      body: new MaterialList(
-        type: MaterialListType.oneLine,
-        children: todos
-      ),
-      floatingActionButton: new FloatingActionButton(
-          child: new Icon(icon: 'content/add'),
-          onPressed: addTodo
-      )
-    );
-  }
-}
-
-class Todo extends StatefulComponent {
-  Todo({Key key, this.title, this.completed: false}) : super(key: key);
-
-  final String title;
-  final bool completed;
-
-  TodoState createState() => new TodoState();
-}
-
-class TodoState extends State<Todo> {
-  bool completed;
-
-  /// Override `initState()`.
-  ///
-  /// Initialize internal state value `completed` with the value passed into
-  /// the parent `Todo` constructor. The `config` property allows the state
-  /// instance to access the properties of the `Todo` component.
-  @override
-  void initState() {
-    super.initState();
-    completed = config.completed;
-  }
-
-  void toggle() {
-    setState(() {
-      completed = !completed;
-    });
-  }
-
-  Widget build(BuildContext context) {
-    return new ListItem(
-      center: new Text('${config.title} - done: $completed'),
-      onTap: toggle
-    );
+        title: 'TODO',
+        theme: theme,
+        routes: {'/': (RouteArguments args) => new TodoCollection()});
   }
 }
diff --git a/examples/todos/lib/todo_model.dart b/examples/todos/lib/todo_model.dart
new file mode 100644
index 0000000..37aecb4
--- /dev/null
+++ b/examples/todos/lib/todo_model.dart
@@ -0,0 +1,17 @@
+// Copyright 2015 The Vanadium 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:flutter/material.dart';
+import 'package:uuid/uuid.dart';
+
+class TodoModel {
+  TodoModel({this.id, this.title, this.completed: false}) {
+    id ??= new Uuid().v4();
+  }
+
+  String id;
+  String title;
+  bool completed;
+  Key get key => new ObjectKey(this);
+}
diff --git a/examples/todos/pubspec.yaml b/examples/todos/pubspec.yaml
index dc298bd..855c158 100644
--- a/examples/todos/pubspec.yaml
+++ b/examples/todos/pubspec.yaml
@@ -1,5 +1,6 @@
 name: todos
 description: A minimal Flutter project.
 dependencies:
+  uuid: 0.5.0
   flutter:
     path: ../../../../../../flutter/packages/flutter