From fe77808d0886b5caffcfcefc24ab1a3e0d9dff91 Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Fri, 25 Sep 2015 10:06:59 -0700 Subject: [PATCH] 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 0000000000..8b3ff14e86 --- /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 0000000000..67d4581d1a --- /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(); + } +}