From b19dbcdea4d833e91fe498cca9fe2ce1d33cd7a8 Mon Sep 17 00:00:00 2001 From: Adam Barth Date: Tue, 10 Mar 2015 12:31:55 -0700 Subject: [PATCH] Make Sky's EditableText mostly work This CL factors EditableString out of EditableText and implements more of the InputConnection functions. As a result, EditableText now basically works. This CL also paves the way to make EditableText stateless by using EditableString as its state object. However, there's still a bit more work to do to make that a reality (e.g., factoring out the cursor blink timer and the connection to the keyboard). R=eseidel@chromium.org Review URL: https://codereview.chromium.org/995073002 --- .../examples/editor/editable_string.dart | 119 +++++++++++++++++ .../examples/editor/editable_text.dart | 124 ++++++++++++++++++ .../flutter/examples/editor/editor_app.dart | 4 +- engine/src/flutter/examples/editor/input.dart | 111 ---------------- 4 files changed, 245 insertions(+), 113 deletions(-) create mode 100644 engine/src/flutter/examples/editor/editable_string.dart create mode 100644 engine/src/flutter/examples/editor/editable_text.dart delete mode 100644 engine/src/flutter/examples/editor/input.dart diff --git a/engine/src/flutter/examples/editor/editable_string.dart b/engine/src/flutter/examples/editor/editable_string.dart new file mode 100644 index 00000000000..aa60b4007fa --- /dev/null +++ b/engine/src/flutter/examples/editor/editable_string.dart @@ -0,0 +1,119 @@ +// 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/keyboard/keyboard.mojom.dart'; + +typedef void StringChangedCallback(EditableString updated); + +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 StringChangedCallback onChanged; + + KeyboardClientStub stub; + + EditableString({this.text: '', this.onChanged}) { + 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(); + onChanged(this); + } + + 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); + onChanged(this); + } + + void setComposingRegion(int start, int end) { + composing = new TextRange(start: start, end: end); + onChanged(this); + } + + 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); + } + + void setSelection(int start, int end) { + selection = new TextRange(start: start, end: end); + onChanged(this); + } +} diff --git a/engine/src/flutter/examples/editor/editable_text.dart b/engine/src/flutter/examples/editor/editable_text.dart new file mode 100644 index 00000000000..6e2c89dff9e --- /dev/null +++ b/engine/src/flutter/examples/editor/editable_text.dart @@ -0,0 +1,124 @@ +// 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/shell.dart' as shell; +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 Style _cusorStyle = new Style(''' + display: inline-block; + width: 2px; + height: 1.2em; + vertical-align: top; + background-color: blue;''' + ); + + static Style _composingStyle = new Style(''' + display: inline; + text-decoration: underline;''' + ); + + KeyboardServiceProxy _service; + + EditableString _string; + 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"); + } + + void _cursorTick(Timer timer) { + setState(() { + _showCursor = !_showCursor; + }); + } + + void _restartCursorTimer() { + if (_cursorTimer != null) + _cursorTimer.cancel(); + _cursorTimer = new Timer.periodic( + new Duration(milliseconds: 500), _cursorTick); + } + + void didUnmount() { + _cursorTimer.cancel(); + } + + Node build() { + List children = new List(); + + if (!_string.composing.isValid) { + children.add(new Text(_string.text)); + } else { + String beforeComposing = _string.textBefore(_string.composing); + if (!beforeComposing.isEmpty) + children.add(new Text(beforeComposing)); + + String composing = _string.textInside(_string.composing); + if (!composing.isEmpty) { + children.add(new Container( + key: 'composing', + style: _composingStyle, + children: [new Text(composing)] + )); + } + + String afterComposing = _string.textAfter(_string.composing); + if (!afterComposing.isEmpty) + children.add(new Text(afterComposing)); + } + + if (_showCursor) + children.add(new Container(key: 'cursor', style: _cusorStyle)); + + return new Container( + style: _style, + children: children + ); + } +} diff --git a/engine/src/flutter/examples/editor/editor_app.dart b/engine/src/flutter/examples/editor/editor_app.dart index 58239c96c37..376aa434d2d 100644 --- a/engine/src/flutter/examples/editor/editor_app.dart +++ b/engine/src/flutter/examples/editor/editor_app.dart @@ -3,10 +3,10 @@ // found in the LICENSE file. import '../../framework/fn.dart'; -import 'input.dart'; +import 'editable_text.dart'; class EditorApp extends App { Node build() { - return new Input(); + return new EditableText(); } } diff --git a/engine/src/flutter/examples/editor/input.dart b/engine/src/flutter/examples/editor/input.dart deleted file mode 100644 index 8508a7ce2fa..00000000000 --- a/engine/src/flutter/examples/editor/input.dart +++ /dev/null @@ -1,111 +0,0 @@ -// 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/shell.dart' as shell; -import 'package:sky/services/keyboard/keyboard.mojom.dart'; -import 'dart:math'; - -class Input extends Component implements KeyboardClient{ - static Style _style = new Style(''' - display: paragraph; - padding: 10px; - height: 200px; - background-color: lightblue;''' - ); - - static Style _composingStyle = new Style(''' - display: inline; - text-decoration: underline;''' - ); - - KeyboardServiceProxy _service; - KeyboardClientStub _stub; - - String _text = ""; - int _composingStart = -1; - int _composingEnd = -1; - - Input({Object key}) : super(key: key, stateful: true) { - events.listen('click', _handleClick); - _stub = new KeyboardClientStub.unbound()..impl = this; - } - - bool get _hasComposingRegion => _composingStart != -1 && _composingEnd != -1; - - void _handleClick(_) { - if (_service != null) - return; - _service = new KeyboardServiceProxy.unbound(); - shell.requestService(_service); - _service.ptr.show(_stub); - } - - void _replaceComposing(String text) { - if (!_hasComposingRegion) { - _composingStart = _text.length; - _composingEnd = _composingStart + text.length; - _text += text; - return; - } - - _text = _text.substring(0, _composingStart) - + text + _text.substring(_composingEnd); - _composingEnd = _composingStart + text.length; - } - - void _clearComposingRegion() { - _composingStart = -1; - _composingEnd = -1; - } - - void commitText(String text, int newCursorPosition) { - setState(() { - _replaceComposing(text); - _clearComposingRegion(); - }); - } - - void setComposingText(String text, int newCursorPosition) { - setState(() { - _replaceComposing(text); - }); - } - - void setComposingRegion(int start, int end) { - setState(() { - _composingStart = start; - _composingEnd = end; - }); - } - - Node build() { - List children = new List(); - - if (!_hasComposingRegion) { - children.add(new Text(_text)); - } else { - String run = _text.substring(0, _composingStart); - if (!run.isEmpty) - children.add(new Text(run)); - - run = _text.substring(_composingStart, _composingEnd); - if (!run.isEmpty) { - children.add(new Container( - style: _composingStyle, - children: [new Text(_text.substring(_composingStart, _composingEnd))] - )); - } - - run = _text.substring(_composingEnd); - if (!run.isEmpty) - children.add(new Text(_text.substring(_composingEnd))); - } - - return new Container( - style: _style, - children: children - ); - } -}