Commit d081dc67 authored by Adam Barth's avatar Adam Barth

Merge pull request #1347 from abarth/editable_line

Input widget shrinks when typing a space
parents 4357d087 bdef1038
...@@ -12,7 +12,7 @@ export 'src/rendering/block.dart'; ...@@ -12,7 +12,7 @@ export 'src/rendering/block.dart';
export 'src/rendering/box.dart'; export 'src/rendering/box.dart';
export 'src/rendering/custom_layout.dart'; export 'src/rendering/custom_layout.dart';
export 'src/rendering/debug.dart'; export 'src/rendering/debug.dart';
export 'src/rendering/editable_paragraph.dart'; export 'src/rendering/editable_line.dart';
export 'src/rendering/error.dart'; export 'src/rendering/error.dart';
export 'src/rendering/flex.dart'; export 'src/rendering/flex.dart';
export 'src/rendering/grid.dart'; export 'src/rendering/grid.dart';
......
...@@ -61,18 +61,18 @@ class Input extends Scrollable { ...@@ -61,18 +61,18 @@ class Input extends Scrollable {
class InputState extends ScrollableState<Input> { class InputState extends ScrollableState<Input> {
String _value; String _value;
EditableString _editableValue; EditableString _editableString;
KeyboardHandle _keyboardHandle = KeyboardHandle.unattached; KeyboardHandle _keyboardHandle = KeyboardHandle.unattached;
double _contentWidth = 0.0; double _contentWidth = 0.0;
double _containerWidth = 0.0; double _containerWidth = 0.0;
EditableString get editableValue => _editableValue; EditableString get editableValue => _editableString;
void initState() { void initState() {
super.initState(); super.initState();
_value = config.initialValue; _value = config.initialValue;
_editableValue = new EditableString( _editableString = new EditableString(
text: _value, text: _value,
onUpdated: _handleTextUpdated, onUpdated: _handleTextUpdated,
onSubmitted: _handleTextSubmitted onSubmitted: _handleTextSubmitted
...@@ -80,9 +80,9 @@ class InputState extends ScrollableState<Input> { ...@@ -80,9 +80,9 @@ class InputState extends ScrollableState<Input> {
} }
void _handleTextUpdated() { void _handleTextUpdated() {
if (_value != _editableValue.text) { if (_value != _editableString.text) {
setState(() { setState(() {
_value = _editableValue.text; _value = _editableString.text;
}); });
if (config.onChanged != null) if (config.onChanged != null)
config.onChanged(_value); config.onChanged(_value);
...@@ -100,10 +100,10 @@ class InputState extends ScrollableState<Input> { ...@@ -100,10 +100,10 @@ class InputState extends ScrollableState<Input> {
bool focused = Focus.at(context, autofocus: config.autofocus); bool focused = Focus.at(context, autofocus: config.autofocus);
if (focused && !_keyboardHandle.attached) { if (focused && !_keyboardHandle.attached) {
_keyboardHandle = keyboard.show(_editableValue.stub, config.keyboardType); _keyboardHandle = keyboard.show(_editableString.stub, config.keyboardType);
_keyboardHandle.setText(_editableValue.text); _keyboardHandle.setText(_editableString.text);
_keyboardHandle.setSelection(_editableValue.selection.start, _keyboardHandle.setSelection(_editableString.selection.start,
_editableValue.selection.end); _editableString.selection.end);
} else if (!focused && _keyboardHandle.attached) { } else if (!focused && _keyboardHandle.attached) {
_keyboardHandle.release(); _keyboardHandle.release();
} }
...@@ -127,8 +127,8 @@ class InputState extends ScrollableState<Input> { ...@@ -127,8 +127,8 @@ class InputState extends ScrollableState<Input> {
focusHighlightColor = focused ? themeData.primarySwatch[400] : themeData.hintColor; focusHighlightColor = focused ? themeData.primarySwatch[400] : themeData.hintColor;
} }
textChildren.add(new EditableText( textChildren.add(new RawEditableLine(
value: _editableValue, value: _editableString,
focused: focused, focused: focused,
style: textStyle, style: textStyle,
hideText: config.hideText, hideText: config.hideText,
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:ui' as ui;
import 'package:flutter/painting.dart'; import 'package:flutter/painting.dart';
import 'box.dart'; import 'box.dart';
...@@ -13,24 +15,43 @@ const _kCursorGap = 1.0; // pixels ...@@ -13,24 +15,43 @@ const _kCursorGap = 1.0; // pixels
const _kCursorHeightOffset = 2.0; // pixels const _kCursorHeightOffset = 2.0; // pixels
const _kCursorWidth = 1.0; // pixels const _kCursorWidth = 1.0; // pixels
/// A render object used by EditableText widgets. This is similar to final String _kZeroWidthSpace = new String.fromCharCode(0x200B);
/// RenderParagraph but also renders a cursor and provides support for
/// scrolling.
class RenderEditableParagraph extends RenderParagraph {
RenderEditableParagraph({ /// A single line of editable text.
TextSpan text, class RenderEditableLine extends RenderBox {
RenderEditableLine({
StyledTextSpan text,
Color cursorColor, Color cursorColor,
bool showCursor, bool showCursor,
this.onContentSizeChanged, this.onContentSizeChanged,
Offset scrollOffset Offset scrollOffset
}) : _cursorColor = cursorColor, }) : _textPainter = new TextPainter(text),
_cursorColor = cursorColor,
_showCursor = showCursor, _showCursor = showCursor,
_scrollOffset = scrollOffset, _scrollOffset = scrollOffset {
super(text); // TODO(abarth): These min/max values should be the default for TextPainter.
_textPainter
..minWidth = 0.0
..maxWidth = double.INFINITY
..minHeight = 0.0
..maxHeight = double.INFINITY;
}
SizeChangedCallback onContentSizeChanged; SizeChangedCallback onContentSizeChanged;
Size _contentSize;
/// The text to display
StyledTextSpan get text => _textPainter.text;
final TextPainter _textPainter;
void set text(StyledTextSpan value) {
if (_textPainter.text == value)
return;
StyledTextSpan oldStyledText = _textPainter.text;
if (oldStyledText.style != value.style)
_layoutTemplate = null;
_textPainter.text = value;
_constraintsForCurrentLayout = null;
markNeedsLayout();
}
Color get cursorColor => _cursorColor; Color get cursorColor => _cursorColor;
Color _cursorColor; Color _cursorColor;
...@@ -59,39 +80,74 @@ class RenderEditableParagraph extends RenderParagraph { ...@@ -59,39 +80,74 @@ class RenderEditableParagraph extends RenderParagraph {
markNeedsPaint(); markNeedsPaint();
} }
BoxConstraints _getTextContraints(BoxConstraints constraints) { Size _contentSize;
assert(constraints.isNormalized);
return new BoxConstraints(
minWidth: 0.0,
maxWidth: double.INFINITY,
minHeight: constraints.minHeight,
maxHeight: constraints.maxHeight
);
}
double _getIntrinsicWidth(BoxConstraints constraints) { ui.Paragraph _layoutTemplate;
// There should be no difference between the minimum and maximum width double get _preferredHeight {
// because we only support single-line text. if (_layoutTemplate == null) {
layoutText(_getTextContraints(constraints)); ui.ParagraphBuilder builder = new ui.ParagraphBuilder()
return constraints.constrainWidth( ..pushStyle(text.style.textStyle)
textPainter.width + _kCursorGap + _kCursorWidth ..addText(_kZeroWidthSpace);
); // TODO(abarth): ParagraphBuilder#build's argument should be optional.
// TODO(abarth): These min/max values should be the default for ui.Paragraph.
_layoutTemplate = builder.build(new ui.ParagraphStyle())
..minWidth = 0.0
..maxWidth = double.INFINITY
..minHeight = 0.0
..maxHeight = double.INFINITY
..layout();
}
return _layoutTemplate.height;
} }
double getMinIntrinsicWidth(BoxConstraints constraints) { double getMinIntrinsicWidth(BoxConstraints constraints) {
return _getIntrinsicWidth(constraints); assert(constraints.isNormalized);
return constraints.constrainWidth(0.0);
} }
double getMaxIntrinsicWidth(BoxConstraints constraints) { double getMaxIntrinsicWidth(BoxConstraints constraints) {
return _getIntrinsicWidth(constraints); assert(constraints.isNormalized);
return constraints.constrainWidth(0.0);
} }
void performLayout() { double getMinIntrinsicHeight(BoxConstraints constraints) {
layoutText(_getTextContraints(constraints)); assert(constraints.isNormalized);
Size contentSize = new Size(textPainter.width + _kCursorGap + _kCursorWidth, textPainter.height); return constraints.constrainHeight(_preferredHeight);
size = constraints.constrain(contentSize); }
double getMaxIntrinsicHeight(BoxConstraints constraints) {
assert(constraints.isNormalized);
return constraints.constrainHeight(_preferredHeight);
}
bool hitTestSelf(Point position) => true;
if (_contentSize == null || _contentSize != contentSize) { BoxConstraints _constraintsForCurrentLayout; // when null, we don't have a current layout
// TODO(abarth): This logic should live in TextPainter and be shared with RenderParagraph.
void _layoutText(BoxConstraints constraints) {
assert(constraints != null);
assert(constraints.isNormalized);
if (_constraintsForCurrentLayout == constraints)
return; // already cached this layout
_textPainter.maxWidth = constraints.maxWidth;
_textPainter.minWidth = constraints.minWidth;
_textPainter.minHeight = constraints.minHeight;
_textPainter.maxHeight = constraints.maxHeight;
_textPainter.layout();
// By default, we shrinkwrap to the intrinsic width.
double width = constraints.constrainWidth(_textPainter.maxIntrinsicWidth);
_textPainter.minWidth = width;
_textPainter.maxWidth = width;
_textPainter.layout();
_constraintsForCurrentLayout = constraints;
}
void performLayout() {
size = new Size(constraints.maxWidth, constraints.constrainHeight(_preferredHeight));
_layoutText(new BoxConstraints(minHeight: constraints.minHeight, maxHeight: constraints.maxHeight));
Size contentSize = new Size(_textPainter.width + _kCursorGap + _kCursorWidth, _textPainter.height);
if (_contentSize != contentSize) {
_contentSize = contentSize; _contentSize = contentSize;
if (onContentSizeChanged != null) if (onContentSizeChanged != null)
onContentSizeChanged(_contentSize); onContentSizeChanged(_contentSize);
...@@ -99,7 +155,7 @@ class RenderEditableParagraph extends RenderParagraph { ...@@ -99,7 +155,7 @@ class RenderEditableParagraph extends RenderParagraph {
} }
void _paintContents(PaintingContext context, Offset offset) { void _paintContents(PaintingContext context, Offset offset) {
textPainter.paint(context.canvas, offset - _scrollOffset); _textPainter.paint(context.canvas, offset - _scrollOffset);
if (_showCursor) { if (_showCursor) {
Rect cursorRect = new Rect.fromLTWH( Rect cursorRect = new Rect.fromLTWH(
...@@ -113,12 +169,10 @@ class RenderEditableParagraph extends RenderParagraph { ...@@ -113,12 +169,10 @@ class RenderEditableParagraph extends RenderParagraph {
} }
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
layoutText(_getTextContraints(constraints));
final bool hasVisualOverflow = (_contentSize.width > size.width); final bool hasVisualOverflow = (_contentSize.width > size.width);
if (hasVisualOverflow) if (hasVisualOverflow)
context.pushClipRect(needsCompositing, offset, Point.origin & size, _paintContents); context.pushClipRect(needsCompositing, offset, Point.origin & size, _paintContents);
else else
_paintContents(context, offset); _paintContents(context, offset);
} }
} }
...@@ -24,55 +24,56 @@ class RenderParagraph extends RenderBox { ...@@ -24,55 +24,56 @@ class RenderParagraph extends RenderBox {
RenderParagraph( RenderParagraph(
TextSpan text TextSpan text
) : textPainter = new TextPainter(text) { ) : _textPainter = new TextPainter(text) {
assert(text != null); assert(text != null);
} }
final TextPainter textPainter; final TextPainter _textPainter;
BoxConstraints _constraintsForCurrentLayout; // when null, we don't have a current layout BoxConstraints _constraintsForCurrentLayout; // when null, we don't have a current layout
/// The text to display /// The text to display
TextSpan get text => textPainter.text; TextSpan get text => _textPainter.text;
void set text(TextSpan value) { void set text(TextSpan value) {
if (textPainter.text == value) if (_textPainter.text == value)
return; return;
textPainter.text = value; _textPainter.text = value;
_constraintsForCurrentLayout = null; _constraintsForCurrentLayout = null;
markNeedsLayout(); markNeedsLayout();
} }
void layoutText(BoxConstraints constraints) { // TODO(abarth): This logic should live in TextPainter and be shared with RenderEditableLine.
void _layoutText(BoxConstraints constraints) {
assert(constraints != null); assert(constraints != null);
assert(constraints.isNormalized); assert(constraints.isNormalized);
if (_constraintsForCurrentLayout == constraints) if (_constraintsForCurrentLayout == constraints)
return; // already cached this layout return; // already cached this layout
textPainter.maxWidth = constraints.maxWidth; _textPainter.maxWidth = constraints.maxWidth;
textPainter.minWidth = constraints.minWidth; _textPainter.minWidth = constraints.minWidth;
textPainter.minHeight = constraints.minHeight; _textPainter.minHeight = constraints.minHeight;
textPainter.maxHeight = constraints.maxHeight; _textPainter.maxHeight = constraints.maxHeight;
textPainter.layout(); _textPainter.layout();
// By default, we shrinkwrap to the intrinsic width. // By default, we shrinkwrap to the intrinsic width.
double width = constraints.constrainWidth(textPainter.maxIntrinsicWidth); double width = constraints.constrainWidth(_textPainter.maxIntrinsicWidth);
textPainter.minWidth = width; _textPainter.minWidth = width;
textPainter.maxWidth = width; _textPainter.maxWidth = width;
textPainter.layout(); _textPainter.layout();
_constraintsForCurrentLayout = constraints; _constraintsForCurrentLayout = constraints;
} }
double getMinIntrinsicWidth(BoxConstraints constraints) { double getMinIntrinsicWidth(BoxConstraints constraints) {
layoutText(constraints); _layoutText(constraints);
return constraints.constrainWidth(textPainter.minIntrinsicWidth); return constraints.constrainWidth(_textPainter.minIntrinsicWidth);
} }
double getMaxIntrinsicWidth(BoxConstraints constraints) { double getMaxIntrinsicWidth(BoxConstraints constraints) {
layoutText(constraints); _layoutText(constraints);
return constraints.constrainWidth(textPainter.maxIntrinsicWidth); return constraints.constrainWidth(_textPainter.maxIntrinsicWidth);
} }
double _getIntrinsicHeight(BoxConstraints constraints) { double _getIntrinsicHeight(BoxConstraints constraints) {
layoutText(constraints); _layoutText(constraints);
return constraints.constrainHeight(textPainter.size.height); return constraints.constrainHeight(_textPainter.size.height);
} }
double getMinIntrinsicHeight(BoxConstraints constraints) { double getMinIntrinsicHeight(BoxConstraints constraints) {
...@@ -87,15 +88,15 @@ class RenderParagraph extends RenderBox { ...@@ -87,15 +88,15 @@ class RenderParagraph extends RenderBox {
double computeDistanceToActualBaseline(TextBaseline baseline) { double computeDistanceToActualBaseline(TextBaseline baseline) {
assert(!needsLayout); assert(!needsLayout);
layoutText(constraints); _layoutText(constraints);
return textPainter.computeDistanceToActualBaseline(baseline); return _textPainter.computeDistanceToActualBaseline(baseline);
} }
bool hitTestSelf(Point position) => true; bool hitTestSelf(Point position) => true;
void performLayout() { void performLayout() {
layoutText(constraints); _layoutText(constraints);
size = constraints.constrain(textPainter.size); size = constraints.constrain(_textPainter.size);
} }
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
...@@ -106,8 +107,8 @@ class RenderParagraph extends RenderBox { ...@@ -106,8 +107,8 @@ class RenderParagraph extends RenderBox {
// //
// TODO(abarth): Make computing the min/max intrinsic width/height // TODO(abarth): Make computing the min/max intrinsic width/height
// a non-destructive operation. // a non-destructive operation.
layoutText(constraints); _layoutText(constraints);
textPainter.paint(context.canvas, offset); _textPainter.paint(context.canvas, offset);
} }
// we should probably expose a way to do precise (inter-glpyh) hit testing // we should probably expose a way to do precise (inter-glpyh) hit testing
......
...@@ -140,8 +140,8 @@ class EditableString implements KeyboardClient { ...@@ -140,8 +140,8 @@ class EditableString implements KeyboardClient {
} }
} }
class EditableText extends StatefulComponent { class RawEditableLine extends StatefulComponent {
EditableText({ RawEditableLine({
Key key, Key key,
this.value, this.value,
this.focused: false, this.focused: false,
...@@ -160,10 +160,12 @@ class EditableText extends StatefulComponent { ...@@ -160,10 +160,12 @@ class EditableText extends StatefulComponent {
final SizeChangedCallback onContentSizeChanged; final SizeChangedCallback onContentSizeChanged;
final Offset scrollOffset; final Offset scrollOffset;
EditableTextState createState() => new EditableTextState(); RawEditableTextState createState() => new RawEditableTextState();
} }
class EditableTextState extends State<EditableText> { class RawEditableTextState extends State<RawEditableLine> {
// TODO(abarth): Move the cursor timer into RenderEditableLine so we can
// remove this extra widget.
Timer _cursorTimer; Timer _cursorTimer;
bool _showCursor = false; bool _showCursor = false;
...@@ -209,25 +211,20 @@ class EditableTextState extends State<EditableText> { ...@@ -209,25 +211,20 @@ class EditableTextState extends State<EditableText> {
else if (!config.focused && _cursorTimer != null) else if (!config.focused && _cursorTimer != null)
_stopCursorTimer(); _stopCursorTimer();
return new SizedBox( return new _EditableLineWidget(
width: double.INFINITY, value: config.value,
child: new _EditableTextWidget( style: config.style,
value: config.value, cursorColor: config.cursorColor,
style: config.style, showCursor: _showCursor,
cursorColor: config.cursorColor, hideText: config.hideText,
showCursor: _showCursor, onContentSizeChanged: config.onContentSizeChanged,
hideText: config.hideText, scrollOffset: config.scrollOffset
onContentSizeChanged: config.onContentSizeChanged,
scrollOffset: config.scrollOffset
)
); );
} }
} }
final String _kZeroWidthSpace = new String.fromCharCode(0x200B); class _EditableLineWidget extends LeafRenderObjectWidget {
_EditableLineWidget({
class _EditableTextWidget extends LeafRenderObjectWidget {
_EditableTextWidget({
Key key, Key key,
this.value, this.value,
this.style, this.style,
...@@ -246,9 +243,9 @@ class _EditableTextWidget extends LeafRenderObjectWidget { ...@@ -246,9 +243,9 @@ class _EditableTextWidget extends LeafRenderObjectWidget {
final SizeChangedCallback onContentSizeChanged; final SizeChangedCallback onContentSizeChanged;
final Offset scrollOffset; final Offset scrollOffset;
RenderEditableParagraph createRenderObject() { RenderEditableLine createRenderObject() {
return new RenderEditableParagraph( return new RenderEditableLine(
text: _buildTextSpan(), text: _styledTextSpan,
cursorColor: cursorColor, cursorColor: cursorColor,
showCursor: showCursor, showCursor: showCursor,
onContentSizeChanged: onContentSizeChanged, onContentSizeChanged: onContentSizeChanged,
...@@ -256,17 +253,16 @@ class _EditableTextWidget extends LeafRenderObjectWidget { ...@@ -256,17 +253,16 @@ class _EditableTextWidget extends LeafRenderObjectWidget {
); );
} }
void updateRenderObject(RenderEditableParagraph renderObject, void updateRenderObject(RenderEditableLine renderObject,
_EditableTextWidget oldWidget) { _EditableLineWidget oldWidget) {
renderObject.text = _buildTextSpan(); renderObject.text = _styledTextSpan;
renderObject.cursorColor = cursorColor; renderObject.cursorColor = cursorColor;
renderObject.showCursor = showCursor; renderObject.showCursor = showCursor;
renderObject.onContentSizeChanged = onContentSizeChanged; renderObject.onContentSizeChanged = onContentSizeChanged;
renderObject.scrollOffset = scrollOffset; renderObject.scrollOffset = scrollOffset;
} }
// Construct a TextSpan that renders the EditableString using the chosen style. StyledTextSpan get _styledTextSpan {
TextSpan _buildTextSpan() {
if (!hideText && value.composing.isValid) { if (!hideText && value.composing.isValid) {
TextStyle composingStyle = style.merge( TextStyle composingStyle = style.merge(
const TextStyle(decoration: TextDecoration.underline) const TextStyle(decoration: TextDecoration.underline)
...@@ -284,8 +280,6 @@ class _EditableTextWidget extends LeafRenderObjectWidget { ...@@ -284,8 +280,6 @@ class _EditableTextWidget extends LeafRenderObjectWidget {
String text = value.text; String text = value.text;
if (hideText) if (hideText)
text = new String.fromCharCodes(new List<int>.filled(text.length, 0x2022)); text = new String.fromCharCodes(new List<int>.filled(text.length, 0x2022));
return new StyledTextSpan(style, <TextSpan>[ return new StyledTextSpan(style, <TextSpan>[ new PlainTextSpan(text) ]);
new PlainTextSpan(text.isEmpty ? _kZeroWidthSpace : text)
]);
} }
} }
...@@ -9,7 +9,7 @@ export 'src/widgets/basic.dart'; ...@@ -9,7 +9,7 @@ export 'src/widgets/basic.dart';
export 'src/widgets/binding.dart'; export 'src/widgets/binding.dart';
export 'src/widgets/dismissable.dart'; export 'src/widgets/dismissable.dart';
export 'src/widgets/drag_target.dart'; export 'src/widgets/drag_target.dart';
export 'src/widgets/editable_text.dart'; export 'src/widgets/editable.dart';
export 'src/widgets/enter_exit_transition.dart'; export 'src/widgets/enter_exit_transition.dart';
export 'src/widgets/focus.dart'; export 'src/widgets/focus.dart';
export 'src/widgets/framework.dart'; export 'src/widgets/framework.dart';
......
...@@ -31,7 +31,7 @@ void main() { ...@@ -31,7 +31,7 @@ void main() {
MockKeyboard mockKeyboard = new MockKeyboard(); MockKeyboard mockKeyboard = new MockKeyboard();
serviceMocker.registerMockService(KeyboardService.serviceName, mockKeyboard); serviceMocker.registerMockService(KeyboardService.serviceName, mockKeyboard);
test('Editable text has consistent width', () { test('Editable text has consistent size', () {
testWidgets((WidgetTester tester) { testWidgets((WidgetTester tester) {
GlobalKey inputKey = new GlobalKey(); GlobalKey inputKey = new GlobalKey();
String inputValue; String inputValue;
...@@ -53,17 +53,21 @@ void main() { ...@@ -53,17 +53,21 @@ void main() {
Element input = tester.findElementByKey(inputKey); Element input = tester.findElementByKey(inputKey);
Size emptyInputSize = (input.renderObject as RenderBox).size; Size emptyInputSize = (input.renderObject as RenderBox).size;
// Simulate entry of text through the keyboard. void enterText(String testValue) {
expect(mockKeyboard.client, isNotNull); // Simulate entry of text through the keyboard.
const String testValue = 'Test'; expect(mockKeyboard.client, isNotNull);
mockKeyboard.client.setComposingText(testValue, testValue.length); mockKeyboard.client.setComposingText(testValue, testValue.length);
// Check that the onChanged event handler fired. // Check that the onChanged event handler fired.
expect(inputValue, equals(testValue)); expect(inputValue, equals(testValue));
tester.pumpWidget(builder()); tester.pumpWidget(builder());
}
enterText(' ');
expect((input.renderObject as RenderBox).size, equals(emptyInputSize));
// Check that the Input with text has the same size as the empty Input. enterText('Test');
expect((input.renderObject as RenderBox).size, equals(emptyInputSize)); expect((input.renderObject as RenderBox).size, equals(emptyInputSize));
}); });
}); });
...@@ -85,7 +89,7 @@ void main() { ...@@ -85,7 +89,7 @@ void main() {
tester.pumpWidget(builder()); tester.pumpWidget(builder());
EditableTextState editableText = tester.findStateOfType(EditableTextState); RawEditableTextState editableText = tester.findStateOfType(RawEditableTextState);
// Check that the cursor visibility toggles after each blink interval. // Check that the cursor visibility toggles after each blink interval.
void checkCursorToggle() { void checkCursorToggle() {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment