Unverified Commit 2338576a authored by chunhtai's avatar chunhtai Committed by GitHub

implement selectable text (#34019)

parent 41bc10fa
......@@ -93,6 +93,7 @@ export 'src/material/reorderable_list.dart';
export 'src/material/scaffold.dart';
export 'src/material/scrollbar.dart';
export 'src/material/search.dart';
export 'src/material/selectable_text.dart';
export 'src/material/shadows.dart';
export 'src/material/slider.dart';
export 'src/material/slider_theme.dart';
......
This diff is collapsed.
......@@ -17,6 +17,7 @@ import 'ink_well.dart' show InteractiveInkFeature;
import 'input_decorator.dart';
import 'material.dart';
import 'material_localizations.dart';
import 'selectable_text.dart' show iOSHorizontalOffset;
import 'text_selection.dart';
import 'theme.dart';
......@@ -932,14 +933,7 @@ class _TextFieldState extends State<TextField> with AutomaticKeepAliveClientMixi
cursorOpacityAnimates = true;
cursorColor ??= CupertinoTheme.of(context).primaryColor;
cursorRadius ??= const Radius.circular(2.0);
// An eyeballed value that moves the cursor slightly left of where it is
// rendered for text on Android so its positioning more accurately matches the
// native iOS text cursor positioning.
//
// This value is in device pixels, not logical pixels as is typically used
// throughout the codebase.
const int _iOSHorizontalOffset = -2;
cursorOffset = Offset(_iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0);
cursorOffset = Offset(iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0);
break;
case TargetPlatform.android:
......
......@@ -652,7 +652,7 @@ class TextPainter {
final double caretEnd = box.end;
final double dx = box.direction == TextDirection.rtl ? caretEnd - caretPrototype.width : caretEnd;
return Rect.fromLTRB(min(dx, width), box.top, min(dx, width), box.bottom);
return Rect.fromLTRB(min(dx, _paragraph.width), box.top, min(dx, _paragraph.width), box.bottom);
}
return null;
}
......@@ -694,7 +694,7 @@ class TextPainter {
final TextBox box = boxes.last;
final double caretStart = box.start;
final double dx = box.direction == TextDirection.rtl ? caretStart - caretPrototype.width : caretStart;
return Rect.fromLTRB(min(dx, width), box.top, min(dx, width), box.bottom);
return Rect.fromLTRB(min(dx, _paragraph.width), box.top, min(dx, _paragraph.width), box.bottom);
}
return null;
}
......
......@@ -157,6 +157,9 @@ class RenderEditable extends RenderBox {
this.onSelectionChanged,
this.onCaretChanged,
this.ignorePointer = false,
bool readOnly = false,
bool forceLine = true,
TextWidthBasis textWidthBasis = TextWidthBasis.parent,
bool obscureText = false,
Locale locale,
double cursorWidth = 1.0,
......@@ -185,11 +188,14 @@ class RenderEditable extends RenderBox {
assert(textScaleFactor != null),
assert(offset != null),
assert(ignorePointer != null),
assert(textWidthBasis != null),
assert(paintCursorAboveText != null),
assert(obscureText != null),
assert(textSelectionDelegate != null),
assert(cursorWidth != null && cursorWidth >= 0.0),
assert(devicePixelRatio != null),
assert(readOnly != null),
assert(forceLine != null),
assert(devicePixelRatio != null),
_textPainter = TextPainter(
text: text,
textAlign: textAlign,
......@@ -197,6 +203,7 @@ class RenderEditable extends RenderBox {
textScaleFactor: textScaleFactor,
locale: locale,
strutStyle: strutStyle,
textWidthBasis: textWidthBasis,
),
_cursorColor = cursorColor,
_backgroundCursorColor = backgroundCursorColor,
......@@ -216,7 +223,9 @@ class RenderEditable extends RenderBox {
_devicePixelRatio = devicePixelRatio,
_startHandleLayerLink = startHandleLayerLink,
_endHandleLayerLink = endHandleLayerLink,
_obscureText = obscureText {
_obscureText = obscureText,
_readOnly = readOnly,
_forceLine = forceLine {
assert(_showCursor != null);
assert(!_showCursor.value || cursorColor != null);
this.hasFocus = hasFocus ?? false;
......@@ -245,12 +254,15 @@ class RenderEditable extends RenderBox {
/// The default value of this property is false.
bool ignorePointer;
/// Whether text is composed.
///
/// Text is composed when user selects it for editing. The [TextSpan] will have
/// children with composing effect and leave text property to be null.
@visibleForTesting
bool get isComposingText => text.text == null;
/// {@macro flutter.widgets.text.DefaultTextStyle.textWidthBasis}
TextWidthBasis get textWidthBasis => _textPainter.textWidthBasis;
set textWidthBasis(TextWidthBasis value) {
assert(value != null);
if (_textPainter.textWidthBasis == value)
return;
_textPainter.textWidthBasis = value;
markNeedsTextLayout();
}
/// The pixel ratio of the current device.
///
......@@ -444,7 +456,7 @@ class RenderEditable extends RenderBox {
if (leftArrow && _extentOffset > 2) {
final TextSelection textSelection = _selectWordAtOffset(TextPosition(offset: _extentOffset - 2));
newOffset = textSelection.baseOffset + 1;
} else if (rightArrow && _extentOffset < text.text.length - 2) {
} else if (rightArrow && _extentOffset < text.toPlainText().length - 2) {
final TextSelection textSelection = _selectWordAtOffset(TextPosition(offset: _extentOffset + 1));
newOffset = textSelection.extentOffset - 1;
}
......@@ -487,7 +499,7 @@ class RenderEditable extends RenderBox {
// case that the user wants to unhighlight some text.
if (position.offset == _extentOffset) {
if (downArrow)
newOffset = text.text.length;
newOffset = text.toPlainText().length;
else if (upArrow)
newOffset = 0;
_resetCursor = shift;
......@@ -554,16 +566,16 @@ class RenderEditable extends RenderBox {
case _kCKeyCode:
if (!selection.isCollapsed) {
Clipboard.setData(
ClipboardData(text: selection.textInside(text.text)));
ClipboardData(text: selection.textInside(text.toPlainText())));
}
break;
case _kXKeyCode:
if (!selection.isCollapsed) {
Clipboard.setData(
ClipboardData(text: selection.textInside(text.text)));
ClipboardData(text: selection.textInside(text.toPlainText())));
textSelectionDelegate.textEditingValue = TextEditingValue(
text: selection.textBefore(text.text)
+ selection.textAfter(text.text),
text: selection.textBefore(text.toPlainText())
+ selection.textAfter(text.toPlainText()),
selection: TextSelection.collapsed(offset: selection.start),
);
}
......@@ -601,15 +613,15 @@ class RenderEditable extends RenderBox {
}
void _handleDelete() {
if (selection.textAfter(text.text).isNotEmpty) {
if (selection.textAfter(text.toPlainText()).isNotEmpty) {
textSelectionDelegate.textEditingValue = TextEditingValue(
text: selection.textBefore(text.text)
+ selection.textAfter(text.text).substring(1),
text: selection.textBefore(text.toPlainText())
+ selection.textAfter(text.toPlainText()).substring(1),
selection: TextSelection.collapsed(offset: selection.start),
);
} else {
textSelectionDelegate.textEditingValue = TextEditingValue(
text: selection.textBefore(text.text),
text: selection.textBefore(text.toPlainText()),
selection: TextSelection.collapsed(offset: selection.start),
);
}
......@@ -758,6 +770,28 @@ class RenderEditable extends RenderBox {
markNeedsSemanticsUpdate();
}
/// Whether this rendering object will take a full line regardless the text width.
bool get forceLine => _forceLine;
bool _forceLine = false;
set forceLine(bool value) {
assert(value != null);
if (_forceLine == value)
return;
_forceLine = value;
markNeedsLayout();
}
/// Whether this rendering object is read only.
bool get readOnly => _readOnly;
bool _readOnly = false;
set readOnly(bool value) {
assert(value != null);
if (_readOnly == value)
return;
_readOnly = value;
markNeedsSemanticsUpdate();
}
/// 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 extend
......@@ -983,6 +1017,8 @@ class RenderEditable extends RenderBox {
return enableInteractiveSelection ?? !obscureText;
}
double get _caretMargin => _kCaretGap + cursorWidth;
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
......@@ -995,7 +1031,8 @@ class RenderEditable extends RenderBox {
..isMultiline = _isMultiline
..textDirection = textDirection
..isFocused = hasFocus
..isTextField = true;
..isTextField = true
..isReadOnly = readOnly;
if (hasFocus && selectionEnabled)
config.onSetSelection = _handleSetSelection;
......@@ -1526,10 +1563,12 @@ class RenderEditable extends RenderBox {
assert(constraintWidth != null);
if (_textLayoutLastWidth == constraintWidth)
return;
final double caretMargin = _kCaretGap + cursorWidth;
final double availableWidth = math.max(0.0, constraintWidth - caretMargin);
final double availableWidth = math.max(0.0, constraintWidth - _caretMargin);
final double maxWidth = _isMultiline ? availableWidth : double.infinity;
_textPainter.layout(minWidth: availableWidth, maxWidth: maxWidth);
_textPainter.layout(
minWidth: forceLine ? availableWidth : 0,
maxWidth: maxWidth,
);
_textLayoutLastWidth = constraintWidth;
}
......@@ -1566,8 +1605,10 @@ class RenderEditable extends RenderBox {
// though we currently don't use those here.
// See also RenderParagraph which has a similar issue.
final Size textPainterSize = _textPainter.size;
size = Size(constraints.maxWidth, constraints.constrainHeight(_preferredHeight(constraints.maxWidth)));
final Size contentSize = Size(textPainterSize.width + _kCaretGap + cursorWidth, textPainterSize.height);
final double width = forceLine ? constraints.maxWidth : constraints
.constrainWidth(_textPainter.size.width + _caretMargin);
size = Size(width, constraints.constrainHeight(_preferredHeight(constraints.maxWidth)));
final Size contentSize = Size(textPainterSize.width + _caretMargin, textPainterSize.height);
_maxScrollExtent = _getMaxScrollExtent(contentSize);
offset.applyViewportDimension(_viewportExtent);
offset.applyContentDimensions(0.0, _maxScrollExtent);
......
......@@ -150,6 +150,29 @@ class TextEditingController extends ValueNotifier<TextEditingValue> {
);
}
/// Builds [TextSpan] from current editing value.
///
/// By default makes text in composing range appear as underlined.
/// Descendants can override this method to customize appearance of text.
TextSpan buildTextSpan({TextStyle style , bool withComposing}) {
if (!value.composing.isValid || !withComposing) {
return TextSpan(style: style, text: text);
}
final TextStyle composingStyle = style.merge(
const TextStyle(decoration: TextDecoration.underline),
);
return TextSpan(
style: style,
children: <TextSpan>[
TextSpan(text: value.composing.textBefore(value.text)),
TextSpan(
style: composingStyle,
text: value.composing.textInside(value.text),
),
TextSpan(text: value.composing.textAfter(value.text)),
]);
}
/// The currently selected [text].
///
/// If the selection is collapsed, then this property gives the offset of the
......@@ -288,6 +311,8 @@ class EditableText extends StatefulWidget {
this.maxLines = 1,
this.minLines,
this.expands = false,
this.forceLine = true,
this.textWidthBasis = TextWidthBasis.parent,
this.autofocus = false,
bool showCursor,
this.showSelectionHandles = false,
......@@ -320,6 +345,7 @@ class EditableText extends StatefulWidget {
assert(autocorrect != null),
assert(showSelectionHandles != null),
assert(readOnly != null),
assert(forceLine != null),
assert(style != null),
assert(cursorColor != null),
assert(cursorOpacityAnimates != null),
......@@ -368,6 +394,9 @@ class EditableText extends StatefulWidget {
/// {@endtemplate}
final bool obscureText;
/// {@macro flutter.widgets.text.DefaultTextStyle.textWidthBasis}
final TextWidthBasis textWidthBasis;
/// {@template flutter.widgets.editableText.readOnly}
/// Whether the text can be changed.
///
......@@ -378,6 +407,18 @@ class EditableText extends StatefulWidget {
/// {@endtemplate}
final bool readOnly;
/// Whether the text will take the full width regardless of the text width.
///
/// When this is set to false, the width will be based on text width, which
/// will also be affected by [textWidthBasis].
///
/// Defaults to true. Must not be null.
///
/// See also:
///
/// * [textWidthBasis], which controls the calculation of text width.
final bool forceLine;
/// Whether to show selection handles.
///
/// When a selection is active, there will be two handles at each side of
......@@ -396,7 +437,7 @@ class EditableText extends StatefulWidget {
///
/// See also:
///
/// * [showSelectionHandles], which controls the visibility of the selection handles..
/// * [showSelectionHandles], which controls the visibility of the selection handles.
/// {@endtemplate}
final bool showCursor;
......@@ -1622,6 +1663,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
showCursor: EditableText.debugDeterministicCursor
? ValueNotifier<bool>(widget.showCursor)
: _cursorVisibilityNotifier,
forceLine: widget.forceLine,
readOnly: widget.readOnly,
hasFocus: _hasFocus,
maxLines: widget.maxLines,
minLines: widget.minLines,
......@@ -1632,6 +1675,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
textAlign: widget.textAlign,
textDirection: _textDirection,
locale: widget.locale,
textWidthBasis: widget.textWidthBasis,
obscureText: widget.obscureText,
autocorrect: widget.autocorrect,
offset: offset,
......@@ -1657,32 +1701,20 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
/// By default makes text in composing range appear as underlined.
/// Descendants can override this method to customize appearance of text.
TextSpan buildTextSpan() {
// Read only mode should not paint text composing.
if (!widget.obscureText && _value.composing.isValid && !widget.readOnly) {
final TextStyle composingStyle = widget.style.merge(
const TextStyle(decoration: TextDecoration.underline),
);
return TextSpan(
style: widget.style,
children: <TextSpan>[
TextSpan(text: _value.composing.textBefore(_value.text)),
TextSpan(
style: composingStyle,
text: _value.composing.textInside(_value.text),
),
TextSpan(text: _value.composing.textAfter(_value.text)),
]);
}
String text = _value.text;
if (widget.obscureText) {
String text = _value.text;
text = RenderEditable.obscuringCharacter * text.length;
final int o =
_obscureShowCharTicksPending > 0 ? _obscureLatestCharIndex : null;
if (o != null && o >= 0 && o < text.length)
text = text.replaceRange(o, o + 1, _value.text.substring(o, o + 1));
return TextSpan(style: widget.style, text: text);
}
return TextSpan(style: widget.style, text: text);
// Read only mode should not paint text composing.
return widget.controller.buildTextSpan(
style: widget.style,
withComposing: !widget.readOnly,
);
}
}
......@@ -1696,6 +1728,9 @@ class _Editable extends LeafRenderObjectWidget {
this.cursorColor,
this.backgroundCursorColor,
this.showCursor,
this.forceLine,
this.readOnly,
this.textWidthBasis,
this.hasFocus,
this.maxLines,
this.minLines,
......@@ -1730,6 +1765,8 @@ class _Editable extends LeafRenderObjectWidget {
final LayerLink endHandleLayerLink;
final Color backgroundCursorColor;
final ValueNotifier<bool> showCursor;
final bool forceLine;
final bool readOnly;
final bool hasFocus;
final int maxLines;
final int minLines;
......@@ -1741,6 +1778,7 @@ class _Editable extends LeafRenderObjectWidget {
final TextDirection textDirection;
final Locale locale;
final bool obscureText;
final TextWidthBasis textWidthBasis;
final bool autocorrect;
final ViewportOffset offset;
final SelectionChangedHandler onSelectionChanged;
......@@ -1763,6 +1801,8 @@ class _Editable extends LeafRenderObjectWidget {
endHandleLayerLink: endHandleLayerLink,
backgroundCursorColor: backgroundCursorColor,
showCursor: showCursor,
forceLine: forceLine,
readOnly: readOnly,
hasFocus: hasFocus,
maxLines: maxLines,
minLines: minLines,
......@@ -1779,6 +1819,7 @@ class _Editable extends LeafRenderObjectWidget {
onCaretChanged: onCaretChanged,
ignorePointer: rendererIgnoresPointer,
obscureText: obscureText,
textWidthBasis: textWidthBasis,
cursorWidth: cursorWidth,
cursorRadius: cursorRadius,
cursorOffset: cursorOffset,
......@@ -1797,6 +1838,8 @@ class _Editable extends LeafRenderObjectWidget {
..startHandleLayerLink = startHandleLayerLink
..endHandleLayerLink = endHandleLayerLink
..showCursor = showCursor
..forceLine = forceLine
..readOnly = readOnly
..hasFocus = hasFocus
..maxLines = maxLines
..minLines = minLines
......@@ -1812,6 +1855,7 @@ class _Editable extends LeafRenderObjectWidget {
..onSelectionChanged = onSelectionChanged
..onCaretChanged = onCaretChanged
..ignorePointer = rendererIgnoresPointer
..textWidthBasis = textWidthBasis
..obscureText = obscureText
..cursorWidth = cursorWidth
..cursorRadius = cursorRadius
......
......@@ -973,7 +973,7 @@ void main() {
final RenderEditable renderEditable = findRenderEditable(tester);
// There should be no composing.
expect(renderEditable.isComposingText, false);
expect(renderEditable.text, TextSpan(text:'readonly', style: renderEditable.text.style));
});
testWidgets('Dynamically switching between read only and not read only should hide or show collapse cursor', (WidgetTester tester) async {
......@@ -3231,6 +3231,30 @@ void main() {
semantics.dispose();
});
testWidgets('Read only TextField identifies as read only text field in semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
const MaterialApp(
home: Material(
child: Center(
child: TextField(
maxLength: 10,
readOnly: true,
),
),
),
),
);
expect(
semantics,
includesNodeWith(flags: <SemanticsFlag>[SemanticsFlag.isTextField, SemanticsFlag.isReadOnly])
);
semantics.dispose();
});
void sendFakeKeyEvent(Map<String, dynamic> data) {
defaultBinaryMessenger.handlePlatformMessage(
SystemChannels.keyEvent.name,
......
This source diff could not be displayed because it is too large. You can view the blob instead.
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