Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add WidgetSpan support in TextFields and "Replacements" API for TextEditingController #80185

Closed
wants to merge 22 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/flutter/lib/src/painting/placeholder_span.dart
Expand Up @@ -38,6 +38,7 @@ abstract class PlaceholderSpan extends InlineSpan {
const PlaceholderSpan({
this.alignment = ui.PlaceholderAlignment.bottom,
this.baseline,
this.range,
TextStyle? style,
}) : super(style: style);

Expand All @@ -52,6 +53,11 @@ abstract class PlaceholderSpan extends InlineSpan {
/// This is ignored when using other alignment modes.
final TextBaseline? baseline;

/// The [TextRange] that this span replaces.
///
/// This can be null if not used as part of an editable text field.
final TextRange? range;

/// [PlaceholderSpan]s are flattened to a `0xFFFC` object replacement character in the
/// plain text representation when `includePlaceholders` is true.
@override
Expand Down
114 changes: 106 additions & 8 deletions packages/flutter/lib/src/painting/text_painter.dart
Expand Up @@ -3,7 +3,7 @@
// found in the LICENSE file.

import 'dart:math' show min, max;
import 'dart:ui' as ui show Paragraph, ParagraphBuilder, ParagraphConstraints, ParagraphStyle, PlaceholderAlignment, LineMetrics, TextHeightBehavior, BoxHeightStyle, BoxWidthStyle;
import 'dart:ui' as ui show Paragraph, ParagraphBuilder, ParagraphConstraints, ParagraphStyle, PlaceholderAlignment, LineMetrics, TextHeightBehavior, TextAffinity, BoxHeightStyle, BoxWidthStyle;

import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
Expand Down Expand Up @@ -64,6 +64,7 @@ class PlaceholderDimensions {
required this.alignment,
this.baseline,
this.baselineOffset,
this.range = TextRange.empty,
}) : assert(size != null),
assert(alignment != null);

Expand Down Expand Up @@ -98,9 +99,15 @@ class PlaceholderDimensions {
/// * [ui.PlaceholderAlignment.middle]
final TextBaseline? baseline;

/// The range of text that this placeholder replaces.
///
/// This value is only relevant when used in TextFields/Editable widgets
/// as replacements for strings of regular text.
final TextRange range;

@override
String toString() {
return 'PlaceholderDimensions($size, $baseline)';
return 'PlaceholderDimensions($size, $baseline, $range)';
GaryQian marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand Down Expand Up @@ -426,6 +433,94 @@ class TextPainter {
}
List<PlaceholderDimensions>? _placeholderDimensions;

// Placeholders when used in TextFields are frequently inserted as replacements
// for strings of text. However, in text layout, the placeholder is treated as
// a single object replacement character codepoint. This means that the TextField
// treats text editing positions/offsets with the full un-replaced string's length
// while the layout and rendering treats placeholders as a single offset.
//
// This method converts a un-replaced offset to the layout engine's single codepoint
// placeholder offset by accounting for the replaced string's length and subtracting
// it from the offset.
//
// This conversion should be applied before calling any _paragraph methods, and any
// offsets returned by _paragraph should call _getRawOffset before using the value.
//
// See also:
//
// * _getRawOffset
// * _getPlaceholderAdjustedPosition
int _getPlaceholderAdjustedOffset(int offset, [TextAffinity? affinity]) {
if (_placeholderDimensions == null) {
return offset;
}
int adjustment = 0;
for (final PlaceholderDimensions dims in _placeholderDimensions!) {
if (!dims.range.isValid) {
continue;
}
if (dims.range.end <= offset) {
// placeholders are represented as a single replacement character,
// so we subtract 1 from the length to account for it.
adjustment += dims.range.end - dims.range.start - 1;
} else if (dims.range.end > offset && offset >= dims.range.start) {
// Within the range.
// place offset at beginning or end of placeholder depending on
// which half it is in.
adjustment += offset - dims.range.start;
print(affinity);
if (affinity == null && offset > dims.range.start + dims.range.end / 2 ||
affinity == TextAffinity.upstream) {
adjustment--;
}
} else {
break;
}
}
return offset - adjustment;
}

// This method performs the opposite conversion of _getPlaceholderAdjustedOffset,
// taking a layout-space offset and adding placeholder replaced string lengths
// as appropriate.
int _getRawOffset(int offset) {
if (_placeholderDimensions == null) {
return offset;
}
for (final PlaceholderDimensions dims in _placeholderDimensions!) {
if (!dims.range.isValid) {
continue;
}
if (offset > dims.range.end || offset > dims.range.start && offset <= dims.range.end) {
offset += dims.range.end - dims.range.start - 1;
} else {
break;
}
}
return offset;
}

TextPosition _getPlaceholderAdjustedPosition(TextPosition position) {
return TextPosition(
offset: _getPlaceholderAdjustedOffset(position.offset, position.affinity),
affinity: position.affinity
);
}

TextPosition _getRawPosition(TextPosition position) {
return TextPosition(
offset: _getRawOffset(position.offset),
affinity: position.affinity
);
}

TextRange _getRawRange(TextRange range) {
return TextRange(
start: _getRawOffset(range.start),
end: _getRawOffset(range.end),
);
}

ui.ParagraphStyle _createParagraphStyle([ TextDirection? defaultTextDirection ]) {
// The defaultTextDirection argument is used for preferredLineHeight in case
// textDirection hasn't yet been set.
Expand Down Expand Up @@ -662,6 +757,7 @@ class TextPainter {
/// Returns the closest offset after `offset` at which the input cursor can be
/// positioned.
int? getOffsetAfter(int offset) {
offset = _getPlaceholderAdjustedOffset(offset, TextAffinity.downstream);
final int? nextCodeUnit = _text!.codeUnitAt(offset);
if (nextCodeUnit == null)
return null;
Expand All @@ -672,6 +768,7 @@ class TextPainter {
/// Returns the closest offset before `offset` at which the input cursor can
/// be positioned.
int? getOffsetBefore(int offset) {
offset = _getPlaceholderAdjustedOffset(offset, TextAffinity.upstream);
final int? prevCodeUnit = _text!.codeUnitAt(offset - 1);
if (prevCodeUnit == null)
return null;
Expand Down Expand Up @@ -838,7 +935,7 @@ class TextPainter {
assert(!_needsLayout);
if (position == _previousCaretPosition && caretPrototype == _previousCaretPrototype)
return;
final int offset = position.offset;
final int offset = _getPlaceholderAdjustedOffset(position.offset);
assert(position.affinity != null);
Rect? rect;
switch (position.affinity) {
Expand Down Expand Up @@ -887,8 +984,8 @@ class TextPainter {
assert(boxHeightStyle != null);
assert(boxWidthStyle != null);
return _paragraph!.getBoxesForRange(
selection.start,
selection.end,
_getPlaceholderAdjustedOffset(selection.start, TextAffinity.upstream),
_getPlaceholderAdjustedOffset(selection.end, TextAffinity.downstream),
boxHeightStyle: boxHeightStyle,
boxWidthStyle: boxWidthStyle,
);
Expand All @@ -897,7 +994,7 @@ class TextPainter {
/// Returns the position within the text for the given pixel offset.
TextPosition getPositionForOffset(Offset offset) {
assert(!_needsLayout);
return _paragraph!.getPositionForOffset(offset);
return _getRawPosition(_paragraph!.getPositionForOffset(offset));
}

/// Returns the text range of the word at the given offset. Characters not
Expand All @@ -909,15 +1006,16 @@ class TextPainter {
/// <http://www.unicode.org/reports/tr29/#Word_Boundaries>.
TextRange getWordBoundary(TextPosition position) {
assert(!_needsLayout);
return _paragraph!.getWordBoundary(position);
return _getRawRange(_paragraph!.getWordBoundary(_getPlaceholderAdjustedPosition(position)));
}

/// Returns the text range of the line at the given offset.
///
/// The newline, if any, is included in the range.
TextRange getLineBoundary(TextPosition position) {
assert(!_needsLayout);
return _paragraph!.getLineBoundary(position);
position = _getPlaceholderAdjustedPosition(position);
return _getRawRange(_paragraph!.getLineBoundary(_getPlaceholderAdjustedPosition(position)));
}

/// Returns the full list of [LineMetrics] that describe in detail the various
Expand Down