From fe77808d0886b5caffcfcefc24ab1a3e0d9dff91 Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Fri, 25 Sep 2015 10:06:59 -0700 Subject: [PATCH 1/2] Copy Input and EditableText into fn3 --- .../flutter/lib/src/fn3/editable_text.dart | 236 ++++++++++++++++++ packages/flutter/lib/src/fn3/input.dart | 131 ++++++++++ 2 files changed, 367 insertions(+) create mode 100644 packages/flutter/lib/src/fn3/editable_text.dart create mode 100644 packages/flutter/lib/src/fn3/input.dart diff --git a/packages/flutter/lib/src/fn3/editable_text.dart b/packages/flutter/lib/src/fn3/editable_text.dart new file mode 100644 index 00000000000..8b3ff14e86e --- /dev/null +++ b/packages/flutter/lib/src/fn3/editable_text.dart @@ -0,0 +1,236 @@ +// 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 'dart:async'; +import 'dart:sky' as sky; + +import 'package:mojo_services/keyboard/keyboard.mojom.dart'; +import 'package:sky/painting.dart'; +import 'package:sky/src/widgets/basic.dart'; +import 'package:sky/src/widgets/framework.dart'; + +const _kCursorBlinkPeriod = 500; // milliseconds +const _kCursorGap = 1.0; +const _kCursorHeightOffset = 2.0; +const _kCursorWidth = 1.0; + +typedef void StringUpdated(); + +class TextRange { + final int start; + final int end; + + TextRange({this.start, this.end}); + TextRange.collapsed(int position) + : start = position, + end = position; + const TextRange.empty() + : start = -1, + end = -1; + + bool get isValid => start >= 0 && end >= 0; + bool get isCollapsed => start == end; +} + +class EditableString implements KeyboardClient { + String text; + TextRange composing = const TextRange.empty(); + TextRange selection = const TextRange.empty(); + + final StringUpdated onUpdated; + + KeyboardClientStub stub; + + EditableString({this.text: '', this.onUpdated}) { + stub = new KeyboardClientStub.unbound()..impl = this; + } + + String textBefore(TextRange range) { + return text.substring(0, range.start); + } + + String textAfter(TextRange range) { + return text.substring(range.end); + } + + String textInside(TextRange range) { + return text.substring(range.start, range.end); + } + + void _delete(TextRange range) { + if (range.isCollapsed || !range.isValid) return; + text = textBefore(range) + textAfter(range); + } + + TextRange _append(String newText) { + int start = text.length; + text += newText; + return new TextRange(start: start, end: start + newText.length); + } + + TextRange _replace(TextRange range, String newText) { + assert(range.isValid); + + String before = textBefore(range); + String after = textAfter(range); + + text = before + newText + after; + return new TextRange( + start: before.length, end: before.length + newText.length); + } + + TextRange _replaceOrAppend(TextRange range, String newText) { + if (!range.isValid) return _append(newText); + return _replace(range, newText); + } + + void commitCompletion(CompletionData completion) { + // TODO(abarth): Not implemented. + } + + void commitCorrection(CorrectionData correction) { + // TODO(abarth): Not implemented. + } + + void commitText(String text, int newCursorPosition) { + // TODO(abarth): Why is |newCursorPosition| always 1? + TextRange committedRange = _replaceOrAppend(composing, text); + selection = new TextRange.collapsed(committedRange.end); + composing = const TextRange.empty(); + onUpdated(); + } + + void deleteSurroundingText(int beforeLength, int afterLength) { + TextRange beforeRange = new TextRange( + start: selection.start - beforeLength, end: selection.start); + TextRange afterRange = + new TextRange(start: selection.end, end: selection.end + afterLength); + _delete(afterRange); + _delete(beforeRange); + selection = new TextRange( + start: selection.start - beforeLength, + end: selection.end - beforeLength); + onUpdated(); + } + + void setComposingRegion(int start, int end) { + composing = new TextRange(start: start, end: end); + onUpdated(); + } + + void setComposingText(String text, int newCursorPosition) { + // TODO(abarth): Why is |newCursorPosition| always 1? + composing = _replaceOrAppend(composing, text); + selection = new TextRange.collapsed(composing.end); + onUpdated(); + } + + void setSelection(int start, int end) { + selection = new TextRange(start: start, end: end); + onUpdated(); + } +} + +class EditableText extends StatefulComponent { + + EditableText({ + Key key, + this.value, + this.focused: false, + this.style, + this.cursorColor}) : super(key: key); + + EditableString value; + bool focused; + TextStyle style; + Color cursorColor; + + void syncConstructorArguments(EditableText source) { + value = source.value; + focused = source.focused; + style = source.style; + cursorColor = source.cursorColor; + } + + Timer _cursorTimer; + bool _showCursor = false; + + /// Whether the blinking cursor is visible (exposed for testing). + bool get test_showCursor => _showCursor; + + /// The cursor blink interval (exposed for testing). + Duration get test_cursorBlinkPeriod => + new Duration(milliseconds: _kCursorBlinkPeriod); + + void _cursorTick(Timer timer) { + setState(() { + _showCursor = !_showCursor; + }); + } + + void _startCursorTimer() { + _showCursor = true; + _cursorTimer = new Timer.periodic( + new Duration(milliseconds: _kCursorBlinkPeriod), _cursorTick); + } + + void didUnmount() { + if (_cursorTimer != null) + _stopCursorTimer(); + super.didUnmount(); + } + + void _stopCursorTimer() { + _cursorTimer.cancel(); + _cursorTimer = null; + _showCursor = false; + } + + void _paintCursor(sky.Canvas canvas, Size size) { + if (!_showCursor) + return; + + double cursorHeight = style.fontSize + 2.0 * _kCursorHeightOffset; + Rect cursorRect = new Rect.fromLTWH( + _kCursorGap, + (size.height - cursorHeight) / 2.0, + _kCursorWidth, + cursorHeight + ); + canvas.drawRect(cursorRect, new Paint()..color = cursorColor); + } + + Widget build() { + assert(style != null); + assert(focused != null); + assert(cursorColor != null); + + if (focused && _cursorTimer == null) + _startCursorTimer(); + else if (!focused && _cursorTimer != null) + _stopCursorTimer(); + + Widget text; + if (value.composing.isValid) { + TextStyle composingStyle = style.merge(const TextStyle(decoration: underline)); + text = new StyledText(elements: [ + style, + value.textBefore(value.composing), + [composingStyle, value.textInside(value.composing)], + value.textAfter(value.composing) + ]); + } else { + // TODO(eseidel): This is the wrong height if empty! + text = new Text(value.text, style: style); + } + + Widget cursor = new Container( + height: style.fontSize * style.height, + width: _kCursorGap + _kCursorWidth, + child: new CustomPaint(callback: _paintCursor, token: _showCursor) + ); + + return new Row([text, cursor]); + } +} diff --git a/packages/flutter/lib/src/fn3/input.dart b/packages/flutter/lib/src/fn3/input.dart new file mode 100644 index 00000000000..67d4581d1a9 --- /dev/null +++ b/packages/flutter/lib/src/fn3/input.dart @@ -0,0 +1,131 @@ +// 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/services.dart'; +import 'package:sky/painting.dart'; +import 'package:sky/src/widgets/basic.dart'; +import 'package:sky/src/widgets/editable_text.dart'; +import 'package:sky/src/widgets/focus.dart'; +import 'package:sky/src/widgets/framework.dart'; +import 'package:sky/src/widgets/theme.dart'; + +export 'package:sky/services.dart' show KeyboardType; + +typedef void StringValueChanged(String value); + +// TODO(eseidel): This isn't right, it's 16px on the bottom: +// http://www.google.com/design/spec/components/text-fields.html#text-fields-single-line-text-field +const EdgeDims _kTextfieldPadding = const EdgeDims.symmetric(vertical: 8.0); + +class Input extends StatefulComponent { + + Input({ + GlobalKey key, + String initialValue: '', + this.placeholder, + this.onChanged, + this.keyboardType : KeyboardType.TEXT + }): _value = initialValue, super(key: key); + + KeyboardType keyboardType; + String placeholder; + StringValueChanged onChanged; + + String _value; + EditableString _editableValue; + KeyboardHandle _keyboardHandle = KeyboardHandle.unattached; + + void initState() { + _editableValue = new EditableString( + text: _value, + onUpdated: _handleTextUpdated + ); + super.initState(); + } + + void syncConstructorArguments(Input source) { + placeholder = source.placeholder; + onChanged = source.onChanged; + keyboardType = source.keyboardType; + } + + void _handleTextUpdated() { + if (_value != _editableValue.text) { + setState(() { + _value = _editableValue.text; + }); + if (onChanged != null) + onChanged(_value); + } + } + + Widget build() { + ThemeData themeData = Theme.of(this); + bool focused = Focus.at(this); + + if (focused && !_keyboardHandle.attached) { + _keyboardHandle = keyboard.show(_editableValue.stub, keyboardType); + } else if (!focused && _keyboardHandle.attached) { + _keyboardHandle.release(); + } + + TextStyle textStyle = themeData.text.subhead; + List textChildren = []; + + if (placeholder != null && _value.isEmpty) { + Widget child = new Opacity( + key: const ValueKey('placeholder'), + child: new Text(placeholder, style: textStyle), + opacity: themeData.hintOpacity + ); + textChildren.add(child); + } + + Color focusHighlightColor = themeData.accentColor; + Color cursorColor = themeData.accentColor; + if (themeData.primarySwatch != null) { + cursorColor = themeData.primarySwatch[200]; + focusHighlightColor = focused ? themeData.primarySwatch[400] : themeData.hintColor; + } + + textChildren.add(new EditableText( + value: _editableValue, + focused: focused, + style: textStyle, + cursorColor: cursorColor + )); + + Border focusHighlight = new Border(bottom: new BorderSide( + color: focusHighlightColor, + width: focused ? 2.0 : 1.0 + )); + + Container input = new Container( + child: new Stack(textChildren), + padding: _kTextfieldPadding, + decoration: new BoxDecoration(border: focusHighlight) + ); + + return new Listener( + child: input, + onPointerDown: focus + ); + } + + void focus(_) { + if (Focus.at(this)) { + assert(_keyboardHandle.attached); + _keyboardHandle.showByRequest(); + } else { + Focus.moveTo(this); + // we'll get told to rebuild and we'll take care of the keyboard then + } + } + + void didUnmount() { + if (_keyboardHandle.attached) + _keyboardHandle.release(); + super.didUnmount(); + } +} From 481b764f677ec9defb22c1f6ec748b87e372a979 Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Fri, 25 Sep 2015 10:08:29 -0700 Subject: [PATCH 2/2] Port Input and EditableText to fn3 --- packages/flutter/lib/src/fn3.dart | 2 + .../flutter/lib/src/fn3/editable_text.dart | 45 ++++++----- packages/flutter/lib/src/fn3/input.dart | 79 +++++++++---------- 3 files changed, 62 insertions(+), 64 deletions(-) diff --git a/packages/flutter/lib/src/fn3.dart b/packages/flutter/lib/src/fn3.dart index d6a14431be6..b844d9f170d 100644 --- a/packages/flutter/lib/src/fn3.dart +++ b/packages/flutter/lib/src/fn3.dart @@ -18,6 +18,7 @@ export 'fn3/drawer.dart'; export 'fn3/drawer_divider.dart'; export 'fn3/drawer_header.dart'; export 'fn3/drawer_item.dart'; +export 'fn3/editable_text.dart'; export 'fn3/flat_button.dart'; export 'fn3/floating_action_button.dart'; export 'fn3/focus.dart'; @@ -27,6 +28,7 @@ export 'fn3/homogeneous_viewport.dart'; export 'fn3/icon.dart'; export 'fn3/icon_button.dart'; export 'fn3/ink_well.dart'; +export 'fn3/input.dart'; export 'fn3/material.dart'; export 'fn3/material_button.dart'; export 'fn3/mixed_viewport.dart'; diff --git a/packages/flutter/lib/src/fn3/editable_text.dart b/packages/flutter/lib/src/fn3/editable_text.dart index 8b3ff14e86e..ed5d57390f9 100644 --- a/packages/flutter/lib/src/fn3/editable_text.dart +++ b/packages/flutter/lib/src/fn3/editable_text.dart @@ -7,8 +7,8 @@ import 'dart:sky' as sky; import 'package:mojo_services/keyboard/keyboard.mojom.dart'; import 'package:sky/painting.dart'; -import 'package:sky/src/widgets/basic.dart'; -import 'package:sky/src/widgets/framework.dart'; +import 'package:sky/src/fn3/basic.dart'; +import 'package:sky/src/fn3/framework.dart'; const _kCursorBlinkPeriod = 500; // milliseconds const _kCursorGap = 1.0; @@ -133,7 +133,6 @@ class EditableString implements KeyboardClient { } class EditableText extends StatefulComponent { - EditableText({ Key key, this.value, @@ -141,18 +140,15 @@ class EditableText extends StatefulComponent { this.style, this.cursorColor}) : super(key: key); - EditableString value; - bool focused; - TextStyle style; - Color cursorColor; + final EditableString value; + final bool focused; + final TextStyle style; + final Color cursorColor; - void syncConstructorArguments(EditableText source) { - value = source.value; - focused = source.focused; - style = source.style; - cursorColor = source.cursorColor; - } + EditableTextState createState() => new EditableTextState(); +} +class EditableTextState extends State { Timer _cursorTimer; bool _showCursor = false; @@ -175,10 +171,10 @@ class EditableText extends StatefulComponent { new Duration(milliseconds: _kCursorBlinkPeriod), _cursorTick); } - void didUnmount() { + void dispose() { if (_cursorTimer != null) _stopCursorTimer(); - super.didUnmount(); + super.dispose(); } void _stopCursorTimer() { @@ -191,26 +187,29 @@ class EditableText extends StatefulComponent { if (!_showCursor) return; - double cursorHeight = style.fontSize + 2.0 * _kCursorHeightOffset; + double cursorHeight = config.style.fontSize + 2.0 * _kCursorHeightOffset; Rect cursorRect = new Rect.fromLTWH( _kCursorGap, (size.height - cursorHeight) / 2.0, _kCursorWidth, cursorHeight ); - canvas.drawRect(cursorRect, new Paint()..color = cursorColor); + canvas.drawRect(cursorRect, new Paint()..color = config.cursorColor); } - Widget build() { - assert(style != null); - assert(focused != null); - assert(cursorColor != null); + Widget build(BuildContext context) { + assert(config.style != null); + assert(config.focused != null); + assert(config.cursorColor != null); - if (focused && _cursorTimer == null) + if (config.focused && _cursorTimer == null) _startCursorTimer(); - else if (!focused && _cursorTimer != null) + else if (!config.focused && _cursorTimer != null) _stopCursorTimer(); + final EditableString value = config.value; + final TextStyle style = config.style; + Widget text; if (value.composing.isValid) { TextStyle composingStyle = style.merge(const TextStyle(decoration: underline)); diff --git a/packages/flutter/lib/src/fn3/input.dart b/packages/flutter/lib/src/fn3/input.dart index 67d4581d1a9..05fe77ec72a 100644 --- a/packages/flutter/lib/src/fn3/input.dart +++ b/packages/flutter/lib/src/fn3/input.dart @@ -4,11 +4,11 @@ import 'package:sky/services.dart'; import 'package:sky/painting.dart'; -import 'package:sky/src/widgets/basic.dart'; -import 'package:sky/src/widgets/editable_text.dart'; -import 'package:sky/src/widgets/focus.dart'; -import 'package:sky/src/widgets/framework.dart'; -import 'package:sky/src/widgets/theme.dart'; +import 'package:sky/src/fn3/basic.dart'; +import 'package:sky/src/fn3/editable_text.dart'; +import 'package:sky/src/fn3/focus.dart'; +import 'package:sky/src/fn3/framework.dart'; +import 'package:sky/src/fn3/theme.dart'; export 'package:sky/services.dart' show KeyboardType; @@ -19,35 +19,34 @@ typedef void StringValueChanged(String value); const EdgeDims _kTextfieldPadding = const EdgeDims.symmetric(vertical: 8.0); class Input extends StatefulComponent { - Input({ GlobalKey key, - String initialValue: '', + this.initialValue: '', this.placeholder, this.onChanged, - this.keyboardType : KeyboardType.TEXT - }): _value = initialValue, super(key: key); + this.keyboardType: KeyboardType.TEXT + }): super(key: key); - KeyboardType keyboardType; - String placeholder; - StringValueChanged onChanged; + final String initialValue; + final KeyboardType keyboardType; + final String placeholder; + final StringValueChanged onChanged; + InputState createState() => new InputState(); +} + +class InputState extends State { String _value; EditableString _editableValue; KeyboardHandle _keyboardHandle = KeyboardHandle.unattached; - void initState() { + void initState(BuildContext context) { + super.initState(context); + _value = config.initialValue; _editableValue = new EditableString( text: _value, onUpdated: _handleTextUpdated ); - super.initState(); - } - - void syncConstructorArguments(Input source) { - placeholder = source.placeholder; - onChanged = source.onChanged; - keyboardType = source.keyboardType; } void _handleTextUpdated() { @@ -55,17 +54,17 @@ class Input extends StatefulComponent { setState(() { _value = _editableValue.text; }); - if (onChanged != null) - onChanged(_value); + if (config.onChanged != null) + config.onChanged(_value); } } - Widget build() { - ThemeData themeData = Theme.of(this); - bool focused = Focus.at(this); + Widget build(BuildContext context) { + ThemeData themeData = Theme.of(context); + bool focused = FocusState.at(context, config); if (focused && !_keyboardHandle.attached) { - _keyboardHandle = keyboard.show(_editableValue.stub, keyboardType); + _keyboardHandle = keyboard.show(_editableValue.stub, config.keyboardType); } else if (!focused && _keyboardHandle.attached) { _keyboardHandle.release(); } @@ -73,10 +72,10 @@ class Input extends StatefulComponent { TextStyle textStyle = themeData.text.subhead; List textChildren = []; - if (placeholder != null && _value.isEmpty) { + if (config.placeholder != null && _value.isEmpty) { Widget child = new Opacity( key: const ValueKey('placeholder'), - child: new Text(placeholder, style: textStyle), + child: new Text(config.placeholder, style: textStyle), opacity: themeData.hintOpacity ); textChildren.add(child); @@ -109,23 +108,21 @@ class Input extends StatefulComponent { return new Listener( child: input, - onPointerDown: focus + onPointerDown: (_) { + if (FocusState.at(context, config)) { + assert(_keyboardHandle.attached); + _keyboardHandle.showByRequest(); + } else { + FocusState.moveTo(context, config); + // we'll get told to rebuild and we'll take care of the keyboard then + } + } ); } - void focus(_) { - if (Focus.at(this)) { - assert(_keyboardHandle.attached); - _keyboardHandle.showByRequest(); - } else { - Focus.moveTo(this); - // we'll get told to rebuild and we'll take care of the keyboard then - } - } - - void didUnmount() { + void dispose() { if (_keyboardHandle.attached) _keyboardHandle.release(); - super.didUnmount(); + super.dispose(); } }