Make a material design Input component

We don't yet have a focus controller, which means once this control becomes
focused there's no way for it to lose focus.

R=eseidel@chromium.org

Review URL: https://codereview.chromium.org/999553002
diff --git a/sky/examples/editor/editable_string.dart b/sky/examples/editor/editable_string.dart
index aa60b40..79ff081 100644
--- a/sky/examples/editor/editable_string.dart
+++ b/sky/examples/editor/editable_string.dart
@@ -4,7 +4,7 @@
 
 import 'package:sky/services/keyboard/keyboard.mojom.dart';
 
-typedef void StringChangedCallback(EditableString updated);
+typedef void StringUpdated();
 
 class TextRange {
   final int start;
@@ -23,11 +23,11 @@
   TextRange composing = const TextRange.empty();
   TextRange selection = const TextRange.empty();
 
-  final StringChangedCallback onChanged;
+  final StringUpdated onUpdated;
 
   KeyboardClientStub stub;
 
-  EditableString({this.text: '', this.onChanged}) {
+  EditableString({this.text: '', this.onUpdated}) {
     stub = new KeyboardClientStub.unbound()..impl = this;
   }
 
@@ -85,7 +85,7 @@
     TextRange committedRange = _replaceOrAppend(composing, text);
     selection = new TextRange.collapsed(committedRange.end);
     composing = const TextRange.empty();
-    onChanged(this);
+    onUpdated();
   }
 
   void deleteSurroundingText(int beforeLength, int afterLength) {
@@ -97,23 +97,23 @@
     _delete(beforeRange);
     selection = new TextRange(start: selection.start - beforeLength,
                               end: selection.end - beforeLength);
-    onChanged(this);
+    onUpdated();
   }
 
   void setComposingRegion(int start, int end) {
     composing = new TextRange(start: start, end: end);
-    onChanged(this);
+    onUpdated();
   }
 
   void setComposingText(String text, int newCursorPosition) {
     // TODO(abarth): Why is |newCursorPosition| always 1?
     composing = _replaceOrAppend(composing, text);
     selection = new TextRange.collapsed(composing.end);
-    onChanged(this);
+    onUpdated();
   }
 
   void setSelection(int start, int end) {
     selection = new TextRange(start: start, end: end);
-    onChanged(this);
+    onUpdated();
   }
 }
diff --git a/sky/examples/editor/editable_text.dart b/sky/examples/editor/editable_text.dart
index 6e2c89d..d752972 100644
--- a/sky/examples/editor/editable_text.dart
+++ b/sky/examples/editor/editable_text.dart
@@ -3,73 +3,35 @@
 // found in the LICENSE file.
 
 import '../../framework/fn.dart';
-import '../../framework/shell.dart' as shell;
+import '../../framework/theme/colors.dart';
 import 'dart:async';
-import 'dart:math';
 import 'editable_string.dart';
-import 'package:sky/services/keyboard/keyboard.mojom.dart';
 
 class EditableText extends Component {
-  static Style _style = new Style('''
-    display: paragraph;
-    white-space: pre-wrap;
-    padding: 10px;
-    height: 200px;
-    background-color: lightblue;'''
+  static final Style _style = new Style('''
+    display: inline;'''
   );
 
-  static Style _cusorStyle = new Style('''
+  static final Style _cusorStyle = new Style('''
     display: inline-block;
     width: 2px;
     height: 1.2em;
     vertical-align: top;
-    background-color: blue;'''
+    background-color: ${Blue[500]};'''
   );
 
-  static Style _composingStyle = new Style('''
+  static final Style _composingStyle = new Style('''
     display: inline;
     text-decoration: underline;'''
   );
 
-  KeyboardServiceProxy _service;
-
-  EditableString _string;
+  EditableString value;
+  bool focused;
   Timer _cursorTimer;
   bool _showCursor = false;
 
-  EditableText({Object key}) : super(key: key, stateful: true) {
-    _string = new EditableString(text: '', onChanged: _handleTextChanged);
-    events.listen('click', _handleClick);
-    events.listen('focus', _handleFocus);
-    events.listen('blur', _handleBlur);
-  }
-
-  void _handleTextChanged(EditableString string) {
-    setState(() {
-      _string = string;
-      _showCursor = true;
-      _restartCursorTimer();
-    });
-  }
-
-  void _handleClick(_) {
-    if (_service != null)
-      return;
-    _service = new KeyboardServiceProxy.unbound();
-    shell.requestService(_service);
-    _service.ptr.show(_string.stub);
-    _restartCursorTimer();
-    setState(() {
-      _showCursor = true;
-    });
-  }
-
-  void _handleFocus(_) {
-    print("_handleFocus");
-  }
-
-  void _handleBlur(_) {
-    print("_handleBlur");
+  EditableText({Object key, this.value, this.focused})
+      : super(key: key, stateful: true) {
   }
 
   void _cursorTick(Timer timer) {
@@ -78,28 +40,39 @@
     });
   }
 
-  void _restartCursorTimer() {
-    if (_cursorTimer != null)
-      _cursorTimer.cancel();
+  void _startCursorTimer() {
+    _showCursor = true;
     _cursorTimer = new Timer.periodic(
         new Duration(milliseconds: 500), _cursorTick);
   }
 
-  void didUnmount() {
+  void _stopCursorTimer() {
     _cursorTimer.cancel();
+    _cursorTimer = null;
+    _showCursor = false;
+  }
+
+  void didUnmount() {
+    if (_cursorTimer != null)
+      _stopCursorTimer();
   }
 
   Node build() {
+    if (focused && _cursorTimer == null)
+      _startCursorTimer();
+    else if (!focused && _cursorTimer != null)
+      _stopCursorTimer();
+
     List<Node> children = new List<Node>();
 
-    if (!_string.composing.isValid) {
-      children.add(new Text(_string.text));
+    if (!value.composing.isValid) {
+      children.add(new Text(value.text));
     } else {
-      String beforeComposing = _string.textBefore(_string.composing);
+      String beforeComposing = value.textBefore(value.composing);
       if (!beforeComposing.isEmpty)
         children.add(new Text(beforeComposing));
 
-      String composing = _string.textInside(_string.composing);
+      String composing = value.textInside(value.composing);
       if (!composing.isEmpty) {
         children.add(new Container(
           key: 'composing', 
@@ -108,7 +81,7 @@
         ));
       }
 
-      String afterComposing = _string.textAfter(_string.composing);
+      String afterComposing = value.textAfter(value.composing);
       if (!afterComposing.isEmpty)
         children.add(new Text(afterComposing));
     }
diff --git a/sky/examples/editor/editor_app.dart b/sky/examples/editor/editor_app.dart
index 376aa43..58239c9 100644
--- a/sky/examples/editor/editor_app.dart
+++ b/sky/examples/editor/editor_app.dart
@@ -3,10 +3,10 @@
 // found in the LICENSE file.
 
 import '../../framework/fn.dart';
-import 'editable_text.dart';
+import 'input.dart';
 
 class EditorApp extends App {
   Node build() {
-    return new EditableText();
+    return new Input();
   }
 }
diff --git a/sky/examples/editor/input.dart b/sky/examples/editor/input.dart
new file mode 100644
index 0000000..0c5b499
--- /dev/null
+++ b/sky/examples/editor/input.dart
@@ -0,0 +1,66 @@
+// 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 '../../framework/fn.dart';
+import '../../framework/theme/colors.dart';
+import 'editable_string.dart';
+import 'editable_text.dart';
+import 'keyboard.dart';
+
+typedef void ValueChanged(value);
+
+class Input extends Component {
+  static final Style _style = new Style('''
+    display: paragraph;
+    margin: 8px;
+    padding: 8px;
+    border-bottom: 1px solid ${Grey[200]};
+    align-self: center;
+    height: 1.2em;
+    white-space: pre;
+    overflow: hidden;'''
+  );
+
+  static final String _focusedInlineStyle = '''
+    padding: 7px;
+    border-bottom: 2px solid ${Blue[500]};''';
+
+  ValueChanged onChanged;
+  String value;
+
+  bool _focused = false;
+  EditableString _editableValue;
+
+  Input({Object key, this.value: ''}) : super(key: key, stateful: true) {
+    _editableValue = new EditableString(text: value,
+                                        onUpdated: _handleTextUpdated);
+    events.listen('click', _handleClick);
+  }
+
+  void _handleClick(_) {
+    keyboard.show(_editableValue.stub);
+    setState(() {
+      _focused = true;
+    });
+  }
+
+  void _handleTextUpdated() {
+    setState(() {});
+    if (value != _editableValue.text) {
+      value = _editableValue.text;
+      if (onChanged != null)
+        onChanged(value);
+    }
+  }
+
+  Node build() {
+    return new Container(
+      style: _style,
+      inlineStyle: _focused ? _focusedInlineStyle : null,
+      children: [
+        new EditableText(value: _editableValue, focused: _focused),
+      ]
+    );
+  }
+}
diff --git a/sky/examples/editor/keyboard.dart b/sky/examples/editor/keyboard.dart
new file mode 100644
index 0000000..c958e48
--- /dev/null
+++ b/sky/examples/editor/keyboard.dart
@@ -0,0 +1,20 @@
+// 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 '../../framework/shell.dart' as shell;
+import 'package:sky/services/keyboard/keyboard.mojom.dart';
+
+class _KeyboardConnection {
+  KeyboardServiceProxy proxy;
+
+  _KeyboardConnection() {
+    proxy = new KeyboardServiceProxy.unbound();
+    shell.requestService(proxy);
+  }
+
+  KeyboardService get keyboard => proxy.ptr;
+}
+
+final _KeyboardConnection _connection = new _KeyboardConnection();
+final KeyboardService keyboard = _connection.keyboard;