blob: 7bfd910d5aa084327ac99be9906f715368424161 [file] [log] [blame]
// 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 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'automatic_keep_alive.dart';
import 'basic.dart';
import 'focus_manager.dart';
import 'focus_scope.dart';
import 'framework.dart';
import 'media_query.dart';
import 'scroll_controller.dart';
import 'scroll_physics.dart';
import 'scrollable.dart';
import 'text_selection.dart';
export 'package:flutter/services.dart' show TextEditingValue, TextSelection, TextInputType;
export 'package:flutter/rendering.dart' show SelectionChangedCause;
/// Signature for the callback that reports when the user changes the selection
/// (including the cursor location).
typedef void SelectionChangedCallback(TextSelection selection, SelectionChangedCause cause);
const Duration _kCursorBlinkHalfPeriod = const Duration(milliseconds: 500);
// Number of cursor ticks during which the most recently entered character
// is shown in an obscured text field.
const int _kObscureShowLatestCharCursorTicks = 3;
/// A controller for an editable text field.
///
/// Whenever the user modifies a text field with an associated
/// [TextEditingController], the text field updates [value] and the controller
/// notifies its listeners. Listeners can then read the [text] and [selection]
/// properties to learn what the user has typed or how the selection has been
/// updated.
///
/// Similarly, if you modify the [text] or [selection] properties, the text
/// field will be notified and will update itself appropriately.
///
/// A [TextEditingController] can also be used to provide an initial value for a
/// text field. If you build a text field with a controller that already has
/// [text], the text field will use that text as its initial value.
///
/// See also:
///
/// * [TextField], which is a Material Design text field that can be controlled
/// with a [TextEditingController].
/// * [EditableText], which is a raw region of editable text that can be
/// controlled with a [TextEditingController].
class TextEditingController extends ValueNotifier<TextEditingValue> {
/// Creates a controller for an editable text field.
///
/// This constructor treats a null [text] argument as if it were the empty
/// string.
TextEditingController({ String text })
: super(text == null ? TextEditingValue.empty : new TextEditingValue(text: text));
/// Creates a controller for an editable text field from an initial [TextEditingValue].
///
/// This constructor treats a null [value] argument as if it were
/// [TextEditingValue.empty].
TextEditingController.fromValue(TextEditingValue value)
: super(value ?? TextEditingValue.empty);
/// The current string the user is editing.
String get text => value.text;
/// Setting this will notify all the listeners of this [TextEditingController]
/// that they need to update (it calls [notifyListeners]). For this reason,
/// this value should only be set between frames, e.g. in response to user
/// actions, not during the build, layout, or paint phases.
set text(String newText) {
value = value.copyWith(text: newText,
selection: const TextSelection.collapsed(offset: -1),
composing: TextRange.empty);
}
/// The currently selected [text].
///
/// If the selection is collapsed, then this property gives the offset of the
/// cursor within the text.
TextSelection get selection => value.selection;
/// Setting this will notify all the listeners of this [TextEditingController]
/// that they need to update (it calls [notifyListeners]). For this reason,
/// this value should only be set between frames, e.g. in response to user
/// actions, not during the build, layout, or paint phases.
set selection(TextSelection newSelection) {
if (newSelection.start > text.length || newSelection.end > text.length)
throw new FlutterError('invalid text selection: $newSelection');
value = value.copyWith(selection: newSelection, composing: TextRange.empty);
}
/// Set the [value] to empty.
///
/// After calling this function, [text] will be the empty string and the
/// selection will be invalid.
///
/// Calling this will notify all the listeners of this [TextEditingController]
/// that they need to update (it calls [notifyListeners]). For this reason,
/// this method should only be called between frames, e.g. in response to user
/// actions, not during the build, layout, or paint phases.
void clear() {
value = TextEditingValue.empty;
}
/// Set the composing region to an empty range.
///
/// The composing region is the range of text that is still being composed.
/// Calling this function indicates that the user is done composing that
/// region.
///
/// Calling this will notify all the listeners of this [TextEditingController]
/// that they need to update (it calls [notifyListeners]). For this reason,
/// this method should only be called between frames, e.g. in response to user
/// actions, not during the build, layout, or paint phases.
void clearComposing() {
value = value.copyWith(composing: TextRange.empty);
}
}
/// A basic text input field.
///
/// This widget interacts with the [TextInput] service to let the user edit the
/// text it contains. It also provides scrolling, selection, and cursor
/// movement. This widget does not provide any focus management (e.g.,
/// tap-to-focus).
///
/// Rather than using this widget directly, consider using [TextField], which
/// is a full-featured, material-design text input field with placeholder text,
/// labels, and [Form] integration.
///
/// See also:
///
/// * [TextField], which is a full-featured, material-design text input field
/// with placeholder text, labels, and [Form] integration.
class EditableText extends StatefulWidget {
/// Creates a basic text input control.
///
/// The [maxLines] property can be set to null to remove the restriction on
/// the number of lines. By default, it is one, meaning this is a single-line
/// text field. [maxLines] must be null or greater than zero.
///
/// If [keyboardType] is not set or is null, it will default to
/// [TextInputType.text] unless [maxLines] is greater than one, when it will
/// default to [TextInputType.multiline].
///
/// The [controller], [focusNode], [style], [cursorColor], [textAlign],
/// and [rendererIgnoresPointer], arguments must not be null.
EditableText({
Key key,
@required this.controller,
@required this.focusNode,
this.obscureText: false,
this.autocorrect: true,
@required this.style,
@required this.cursorColor,
this.textAlign: TextAlign.start,
this.textDirection,
this.textScaleFactor,
this.maxLines: 1,
this.autofocus: false,
this.selectionColor,
this.selectionControls,
TextInputType keyboardType,
this.onChanged,
this.onSubmitted,
this.onSelectionChanged,
List<TextInputFormatter> inputFormatters,
this.rendererIgnoresPointer: false,
}) : assert(controller != null),
assert(focusNode != null),
assert(obscureText != null),
assert(autocorrect != null),
assert(style != null),
assert(cursorColor != null),
assert(textAlign != null),
assert(maxLines == null || maxLines > 0),
assert(autofocus != null),
assert(rendererIgnoresPointer != null),
keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline),
inputFormatters = maxLines == 1
? (
<TextInputFormatter>[BlacklistingTextInputFormatter.singleLineFormatter]
..addAll(inputFormatters ?? const Iterable<TextInputFormatter>.empty())
)
: inputFormatters,
super(key: key);
/// Controls the text being edited.
final TextEditingController controller;
/// Controls whether this widget has keyboard focus.
final FocusNode focusNode;
/// Whether to hide the text being edited (e.g., for passwords).
///
/// Defaults to false.
final bool obscureText;
/// Whether to enable autocorrection.
///
/// Defaults to true.
final bool autocorrect;
/// The text style to use for the editable text.
final TextStyle style;
/// How the text should be aligned horizontally.
///
/// Defaults to [TextAlign.start].
final TextAlign textAlign;
/// The directionality of the text.
///
/// This decides how [textAlign] values like [TextAlign.start] and
/// [TextAlign.end] are interpreted.
///
/// This is also used to disambiguate how to render bidirectional text. For
/// example, if the text is an English phrase followed by a Hebrew phrase,
/// in a [TextDirection.ltr] context the English phrase will be on the left
/// and the Hebrew phrase to its right, while in a [TextDirection.rtl]
/// context, the English phrase will be on the right and the Hebrew phrase on
/// its left.
///
/// Defaults to the ambient [Directionality], if any.
final TextDirection textDirection;
/// The number of font pixels for each logical pixel.
///
/// For example, if the text scale factor is 1.5, text will be 50% larger than
/// the specified font size.
///
/// Defaults to the [MediaQueryData.textScaleFactor] obtained from the ambient
/// [MediaQuery], or 1.0 if there is no [MediaQuery] in scope.
final double textScaleFactor;
/// The color to use when painting the cursor.
final Color cursorColor;
/// The maximum number of lines for the text to span, wrapping if necessary.
///
/// If this is 1 (the default), the text will not wrap, but will scroll
/// horizontally instead.
///
/// If this is null, there is no limit to the number of lines. If it is not
/// null, the value must be greater than zero.
final int maxLines;
/// Whether this input field should focus itself if nothing else is already focused.
/// If true, the keyboard will open as soon as this input obtains focus. Otherwise,
/// the keyboard is only shown after the user taps the text field.
///
/// Defaults to false.
final bool autofocus;
/// The color to use when painting the selection.
final Color selectionColor;
/// Optional delegate for building the text selection handles and toolbar.
final TextSelectionControls selectionControls;
/// The type of keyboard to use for editing the text.
final TextInputType keyboardType;
/// Called when the text being edited changes.
final ValueChanged<String> onChanged;
/// Called when the user indicates that they are done editing the text in the field.
final ValueChanged<String> onSubmitted;
/// Called when the user changes the selection of text (including the cursor
/// location).
final SelectionChangedCallback onSelectionChanged;
/// Optional input validation and formatting overrides. Formatters are run
/// in the provided order when the text input changes.
final List<TextInputFormatter> inputFormatters;
/// If true, the [RenderEditable] created by this widget will not handle
/// pointer events, see [renderEditable] and [RenderEditable.ignorePointer].
///
/// This property is false by default.
final bool rendererIgnoresPointer;
@override
EditableTextState createState() => new EditableTextState();
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(new DiagnosticsProperty<TextEditingController>('controller', controller));
properties.add(new DiagnosticsProperty<FocusNode>('focusNode', focusNode));
properties.add(new DiagnosticsProperty<bool>('obscureText', obscureText, defaultValue: false));
properties.add(new DiagnosticsProperty<bool>('autocorrect', autocorrect, defaultValue: true));
style?.debugFillProperties(properties);
properties.add(new EnumProperty<TextAlign>('textAlign', textAlign, defaultValue: null));
properties.add(new EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
properties.add(new DoubleProperty('textScaleFactor', textScaleFactor, defaultValue: null));
properties.add(new IntProperty('maxLines', maxLines, defaultValue: 1));
properties.add(new DiagnosticsProperty<bool>('autofocus', autofocus, defaultValue: false));
properties.add(new DiagnosticsProperty<TextInputType>('keyboardType', keyboardType, defaultValue: null));
}
}
/// State for a [EditableText].
class EditableTextState extends State<EditableText> with AutomaticKeepAliveClientMixin implements TextInputClient, TextSelectionDelegate {
Timer _cursorTimer;
final ValueNotifier<bool> _showCursor = new ValueNotifier<bool>(false);
final GlobalKey _editableKey = new GlobalKey();
TextInputConnection _textInputConnection;
TextSelectionOverlay _selectionOverlay;
final ScrollController _scrollController = new ScrollController();
final LayerLink _layerLink = new LayerLink();
bool _didAutoFocus = false;
@override
bool get wantKeepAlive => widget.focusNode.hasFocus;
// State lifecycle:
@override
void initState() {
super.initState();
widget.controller.addListener(_didChangeTextEditingValue);
widget.focusNode.addListener(_handleFocusChanged);
_scrollController.addListener(() { _selectionOverlay?.updateForScroll(); });
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!_didAutoFocus && widget.autofocus) {
FocusScope.of(context).autofocus(widget.focusNode);
_didAutoFocus = true;
}
}
@override
void didUpdateWidget(EditableText oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.controller != oldWidget.controller) {
oldWidget.controller.removeListener(_didChangeTextEditingValue);
widget.controller.addListener(_didChangeTextEditingValue);
_updateRemoteEditingValueIfNeeded();
}
if (widget.focusNode != oldWidget.focusNode) {
oldWidget.focusNode.removeListener(_handleFocusChanged);
widget.focusNode.addListener(_handleFocusChanged);
updateKeepAlive();
}
}
@override
void dispose() {
widget.controller.removeListener(_didChangeTextEditingValue);
_closeInputConnectionIfNeeded();
assert(!_hasInputConnection);
_stopCursorTimer();
assert(_cursorTimer == null);
_selectionOverlay?.dispose();
_selectionOverlay = null;
widget.focusNode.removeListener(_handleFocusChanged);
super.dispose();
}
// TextInputClient implementation:
TextEditingValue _lastKnownRemoteTextEditingValue;
@override
void updateEditingValue(TextEditingValue value) {
if (value.text != _value.text) {
_hideSelectionOverlayIfNeeded();
if (widget.obscureText && value.text.length == _value.text.length + 1) {
_obscureShowCharTicksPending = _kObscureShowLatestCharCursorTicks;
_obscureLatestCharIndex = _value.selection.baseOffset;
}
}
_lastKnownRemoteTextEditingValue = value;
_formatAndSetValue(value);
}
@override
void performAction(TextInputAction action) {
switch (action) {
case TextInputAction.done:
widget.controller.clearComposing();
widget.focusNode.unfocus();
if (widget.onSubmitted != null)
widget.onSubmitted(_value.text);
break;
case TextInputAction.newline:
// Do nothing for a "newline" action: the newline is already inserted.
break;
}
}
void _updateRemoteEditingValueIfNeeded() {
if (!_hasInputConnection)
return;
final TextEditingValue localValue = _value;
if (localValue == _lastKnownRemoteTextEditingValue)
return;
_lastKnownRemoteTextEditingValue = localValue;
_textInputConnection.setEditingState(localValue);
}
TextEditingValue get _value => widget.controller.value;
set _value(TextEditingValue value) {
widget.controller.value = value;
}
bool get _hasFocus => widget.focusNode.hasFocus;
bool get _isMultiline => widget.maxLines != 1;
// Calculate the new scroll offset so the cursor remains visible.
double _getScrollOffsetForCaret(Rect caretRect) {
final double caretStart = _isMultiline ? caretRect.top : caretRect.left;
final double caretEnd = _isMultiline ? caretRect.bottom : caretRect.right;
double scrollOffset = _scrollController.offset;
final double viewportExtent = _scrollController.position.viewportDimension;
if (caretStart < 0.0) // cursor before start of bounds
scrollOffset += caretStart;
else if (caretEnd >= viewportExtent) // cursor after end of bounds
scrollOffset += caretEnd - viewportExtent;
return scrollOffset;
}
bool get _hasInputConnection => _textInputConnection != null && _textInputConnection.attached;
void _openInputConnection() {
if (!_hasInputConnection) {
final TextEditingValue localValue = _value;
_lastKnownRemoteTextEditingValue = localValue;
_textInputConnection = TextInput.attach(this,
new TextInputConfiguration(
inputType: widget.keyboardType,
obscureText: widget.obscureText,
autocorrect: widget.autocorrect,
inputAction: widget.keyboardType == TextInputType.multiline
? TextInputAction.newline
: TextInputAction.done
)
)..setEditingState(localValue);
}
_textInputConnection.show();
}
void _closeInputConnectionIfNeeded() {
if (_hasInputConnection) {
_textInputConnection.close();
_textInputConnection = null;
_lastKnownRemoteTextEditingValue = null;
}
}
void _openOrCloseInputConnectionIfNeeded() {
if (_hasFocus && widget.focusNode.consumeKeyboardToken()) {
_openInputConnection();
} else if (!_hasFocus) {
_closeInputConnectionIfNeeded();
widget.controller.clearComposing();
}
}
/// Express interest in interacting with the keyboard.
///
/// If this control is already attached to the keyboard, this function will
/// request that the keyboard become visible. Otherwise, this function will
/// ask the focus system that it become focused. If successful in acquiring
/// focus, the control will then attach to the keyboard and request that the
/// keyboard become visible.
void requestKeyboard() {
if (_hasFocus)
_openInputConnection();
else
FocusScope.of(context).requestFocus(widget.focusNode);
}
void _hideSelectionOverlayIfNeeded() {
_selectionOverlay?.hide();
_selectionOverlay = null;
}
void _updateOrDisposeSelectionOverlayIfNeeded() {
if (_selectionOverlay != null) {
if (_hasFocus) {
_selectionOverlay.update(_value);
} else {
_selectionOverlay.dispose();
_selectionOverlay = null;
}
}
}
void _handleSelectionChanged(TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {
widget.controller.selection = selection;
// This will show the keyboard for all selection changes on the
// EditableWidget, not just changes triggered by user gestures.
requestKeyboard();
_hideSelectionOverlayIfNeeded();
if (widget.selectionControls != null) {
_selectionOverlay = new TextSelectionOverlay(
context: context,
value: _value,
debugRequiredFor: widget,
layerLink: _layerLink,
renderObject: renderObject,
selectionControls: widget.selectionControls,
selectionDelegate: this,
);
final bool longPress = cause == SelectionChangedCause.longPress;
if (cause != SelectionChangedCause.keyboard && (_value.text.isNotEmpty || longPress))
_selectionOverlay.showHandles();
if (longPress)
_selectionOverlay.showToolbar();
if (widget.onSelectionChanged != null)
widget.onSelectionChanged(selection, cause);
}
}
bool _textChangedSinceLastCaretUpdate = false;
void _handleCaretChanged(Rect caretRect) {
// If the caret location has changed due to an update to the text or
// selection, then scroll the caret into view.
if (_textChangedSinceLastCaretUpdate) {
_textChangedSinceLastCaretUpdate = false;
scheduleMicrotask(() {
_scrollController.animateTo(
_getScrollOffsetForCaret(caretRect),
curve: Curves.fastOutSlowIn,
duration: const Duration(milliseconds: 50),
);
});
}
}
void _formatAndSetValue(TextEditingValue value) {
final bool textChanged = _value?.text != value?.text;
if (widget.inputFormatters != null && widget.inputFormatters.isNotEmpty) {
for (TextInputFormatter formatter in widget.inputFormatters)
value = formatter.formatEditUpdate(_value, value);
_value = value;
_updateRemoteEditingValueIfNeeded();
} else {
_value = value;
}
if (textChanged && widget.onChanged != null)
widget.onChanged(value.text);
}
/// Whether the blinking cursor is actually visible at this precise moment
/// (it's hidden half the time, since it blinks).
@visibleForTesting
bool get cursorCurrentlyVisible => _showCursor.value;
/// The cursor blink interval (the amount of time the cursor is in the "on"
/// state or the "off" state). A complete cursor blink period is twice this
/// value (half on, half off).
@visibleForTesting
Duration get cursorBlinkInterval => _kCursorBlinkHalfPeriod;
/// The current status of the text selection handles.
@visibleForTesting
TextSelectionOverlay get selectionOverlay => _selectionOverlay;
int _obscureShowCharTicksPending = 0;
int _obscureLatestCharIndex;
void _cursorTick(Timer timer) {
_showCursor.value = !_showCursor.value;
if (_obscureShowCharTicksPending > 0) {
setState(() { _obscureShowCharTicksPending--; });
}
}
void _startCursorTimer() {
_showCursor.value = true;
_cursorTimer = new Timer.periodic(_kCursorBlinkHalfPeriod, _cursorTick);
}
void _stopCursorTimer() {
_cursorTimer?.cancel();
_cursorTimer = null;
_showCursor.value = false;
_obscureShowCharTicksPending = 0;
}
void _startOrStopCursorTimerIfNeeded() {
if (_cursorTimer == null && _hasFocus && _value.selection.isCollapsed)
_startCursorTimer();
else if (_cursorTimer != null && (!_hasFocus || !_value.selection.isCollapsed))
_stopCursorTimer();
}
void _didChangeTextEditingValue() {
_updateRemoteEditingValueIfNeeded();
_startOrStopCursorTimerIfNeeded();
_updateOrDisposeSelectionOverlayIfNeeded();
_textChangedSinceLastCaretUpdate = true;
// TODO(abarth): Teach RenderEditable about ValueNotifier<TextEditingValue>
// to avoid this setState().
setState(() { /* We use widget.controller.value in build(). */ });
}
void _handleFocusChanged() {
_openOrCloseInputConnectionIfNeeded();
_startOrStopCursorTimerIfNeeded();
_updateOrDisposeSelectionOverlayIfNeeded();
if (!_hasFocus) {
// Clear the selection and composition state if this widget lost focus.
_value = new TextEditingValue(text: _value.text);
}
updateKeepAlive();
}
TextDirection get _textDirection {
final TextDirection result = widget.textDirection ?? Directionality.of(context);
assert(result != null, '$runtimeType created without a textDirection and with no ambient Directionality.');
return result;
}
/// The renderer for this widget's [Editable] descendant.
///
/// This property is typically used to notify the renderer of input gestures
/// when [ignorePointer] is true. See [RenderEditable.ignorePointer].
RenderEditable get renderEditable => _editableKey.currentContext.findRenderObject();
@override
TextEditingValue get textEditingValue => _value;
@override
set textEditingValue(TextEditingValue value) {
_selectionOverlay?.update(value);
_formatAndSetValue(value);
}
@override
void bringIntoView(TextPosition position) {
_scrollController.jumpTo(_getScrollOffsetForCaret(renderEditable.getLocalRectForCaret(position)));
}
@override
void hideToolbar() {
_selectionOverlay?.hide();
}
@override
Widget build(BuildContext context) {
FocusScope.of(context).reparentIfNeeded(widget.focusNode);
super.build(context); // See AutomaticKeepAliveClientMixin.
final TextSelectionControls controls = widget.selectionControls;
return new Scrollable(
excludeFromSemantics: true,
axisDirection: _isMultiline ? AxisDirection.down : AxisDirection.right,
controller: _scrollController,
physics: const ClampingScrollPhysics(),
viewportBuilder: (BuildContext context, ViewportOffset offset) {
return new CompositedTransformTarget(
link: _layerLink,
child: new Semantics(
onCopy: _hasFocus && controls?.canCopy(this) == true ? () => controls.handleCopy(this) : null,
onCut: _hasFocus && controls?.canCut(this) == true ? () => controls.handleCut(this) : null,
onPaste: _hasFocus && controls?.canPaste(this) == true ? () => controls.handlePaste(this) : null,
child: new _Editable(
key: _editableKey,
value: _value,
style: widget.style,
cursorColor: widget.cursorColor,
showCursor: _showCursor,
hasFocus: _hasFocus,
maxLines: widget.maxLines,
selectionColor: widget.selectionColor,
textScaleFactor: widget.textScaleFactor ?? MediaQuery.of(context, nullOk: true)?.textScaleFactor ?? 1.0,
textAlign: widget.textAlign,
textDirection: _textDirection,
obscureText: widget.obscureText,
obscureShowCharacterAtIndex: _obscureShowCharTicksPending > 0 ? _obscureLatestCharIndex : null,
autocorrect: widget.autocorrect,
offset: offset,
onSelectionChanged: _handleSelectionChanged,
onCaretChanged: _handleCaretChanged,
rendererIgnoresPointer: widget.rendererIgnoresPointer,
),
),
);
},
);
}
}
class _Editable extends LeafRenderObjectWidget {
const _Editable({
Key key,
this.value,
this.style,
this.cursorColor,
this.showCursor,
this.hasFocus,
this.maxLines,
this.selectionColor,
this.textScaleFactor,
this.textAlign,
@required this.textDirection,
this.obscureText,
this.obscureShowCharacterAtIndex,
this.autocorrect,
this.offset,
this.onSelectionChanged,
this.onCaretChanged,
this.rendererIgnoresPointer: false,
}) : assert(textDirection != null),
assert(rendererIgnoresPointer != null),
super(key: key);
final TextEditingValue value;
final TextStyle style;
final Color cursorColor;
final ValueNotifier<bool> showCursor;
final bool hasFocus;
final int maxLines;
final Color selectionColor;
final double textScaleFactor;
final TextAlign textAlign;
final TextDirection textDirection;
final bool obscureText;
final int obscureShowCharacterAtIndex;
final bool autocorrect;
final ViewportOffset offset;
final SelectionChangedHandler onSelectionChanged;
final CaretChangedHandler onCaretChanged;
final bool rendererIgnoresPointer;
@override
RenderEditable createRenderObject(BuildContext context) {
return new RenderEditable(
text: _styledTextSpan,
cursorColor: cursorColor,
showCursor: showCursor,
hasFocus: hasFocus,
maxLines: maxLines,
selectionColor: selectionColor,
textScaleFactor: textScaleFactor,
textAlign: textAlign,
textDirection: textDirection,
selection: value.selection,
offset: offset,
onSelectionChanged: onSelectionChanged,
onCaretChanged: onCaretChanged,
ignorePointer: rendererIgnoresPointer,
obscureText: obscureText,
);
}
@override
void updateRenderObject(BuildContext context, RenderEditable renderObject) {
renderObject
..text = _styledTextSpan
..cursorColor = cursorColor
..showCursor = showCursor
..hasFocus = hasFocus
..maxLines = maxLines
..selectionColor = selectionColor
..textScaleFactor = textScaleFactor
..textAlign = textAlign
..textDirection = textDirection
..selection = value.selection
..offset = offset
..onSelectionChanged = onSelectionChanged
..onCaretChanged = onCaretChanged
..ignorePointer = rendererIgnoresPointer
..obscureText = obscureText;
}
TextSpan get _styledTextSpan {
if (!obscureText && value.composing.isValid) {
final TextStyle composingStyle = style.merge(
const TextStyle(decoration: TextDecoration.underline)
);
return new TextSpan(
style: style,
children: <TextSpan>[
new TextSpan(text: value.composing.textBefore(value.text)),
new TextSpan(
style: composingStyle,
text: value.composing.textInside(value.text)
),
new TextSpan(text: value.composing.textAfter(value.text))
]);
}
String text = value.text;
if (obscureText) {
text = RenderEditable.obscuringCharacter * text.length;
final int o = obscureShowCharacterAtIndex;
if (o != null && o >= 0 && o < text.length)
text = text.replaceRange(o, o + 1, value.text.substring(o, o + 1));
}
return new TextSpan(style: style, text: text);
}
}