opt text selection

Signed-off-by: dom <githubaccount56556@proton.me>
This commit is contained in:
dom
2026-07-25 13:00:02 +08:00
parent 56ca0cade4
commit 10b723fa64
11 changed files with 648 additions and 390 deletions

View File

@@ -45,7 +45,7 @@ $TextFieldPatch = "lib/scripts/text_field.patch"
$ScrollPositionPatch = "lib/scripts/scroll_position.patch"
$SelectionPlaceholderPatch = "lib/scripts/selection_placeholder.patch"
$ScrollablePatch = "lib/scripts/scrollable.patch"
# TODO: remove
# https://github.com/flutter/flutter/pull/183261
@@ -80,7 +80,7 @@ $patches = @($ModalBarrierPatch, $TextSelectionPatch, $MouseCursorPatch,
$ImageAnimPatch, $LayoutBuilderPatch, $NavigationDrawerPatch,
$PopupMenuPatch, $FABPatch, $SelectableRegionPatch, $SelectableRegionSelectionPatch,
$EditableTextPatch, $TextFieldPatch, $ScrollPositionPatch,
$SelectionPlaceholderPatch)
$ScrollablePatch)
switch ($platform.ToLower()) {
"android" {

View File

@@ -0,0 +1,12 @@
diff --git a/packages/flutter/lib/src/widgets/scrollable.dart b/packages/flutter/lib/src/widgets/scrollable.dart
index 9df2d633f16..f39e6760df9 100644
--- a/packages/flutter/lib/src/widgets/scrollable.dart
+++ b/packages/flutter/lib/src/widgets/scrollable.dart
@@ -766,6 +766,7 @@ class ScrollableState extends State<Scrollable>
Map<Type, GestureRecognizerFactory> _gestureRecognizers =
const <Type, GestureRecognizerFactory>{};
bool _shouldIgnorePointer = false;
+ bool get shouldIgnorePointer => _shouldIgnorePointer;
bool? _lastCanDrag;
Axis? _lastAxisDirection;

View File

@@ -1,21 +1,326 @@
diff --git a/packages/flutter/lib/src/painting/inline_span.dart b/packages/flutter/lib/src/painting/inline_span.dart
index 0c4a0be6177..77baa589699 100644
--- a/packages/flutter/lib/src/painting/inline_span.dart
+++ b/packages/flutter/lib/src/painting/inline_span.dart
@@ -315,6 +315,21 @@ abstract class InlineSpan extends DiagnosticableTree {
return buffer.toString();
}
+ (String, Map<int, String>) toPlainTextV2({
+ bool includeSemanticsLabels = true,
+ bool includePlaceholders = true,
+ }) {
+ final buffer = StringBuffer();
+ final map = <int, String>{};
+ computeToPlainTextV2(
+ buffer,
+ map,
+ includeSemanticsLabels: includeSemanticsLabels,
+ includePlaceholders: includePlaceholders,
+ );
+ return (buffer.toString(), map);
+ }
+
/// Flattens the [InlineSpan] tree to a list of
/// [InlineSpanSemanticsInformation] objects.
///
@@ -358,6 +373,14 @@ abstract class InlineSpan extends DiagnosticableTree {
bool includePlaceholders = true,
});
+ @protected
+ void computeToPlainTextV2(
+ StringBuffer buffer,
+ Map<int, String> map, {
+ bool includeSemanticsLabels = true,
+ bool includePlaceholders = true,
+ });
+
/// Returns the UTF-16 code unit at the given `index` in the flattened string.
///
/// This only accounts for the [TextSpan.text] values and ignores [PlaceholderSpan]s.
diff --git a/packages/flutter/lib/src/painting/placeholder_span.dart b/packages/flutter/lib/src/painting/placeholder_span.dart
index 110ff860310..7ad808d05bc 100644
--- a/packages/flutter/lib/src/painting/placeholder_span.dart
+++ b/packages/flutter/lib/src/painting/placeholder_span.dart
@@ -45,6 +45,7 @@ abstract class PlaceholderSpan extends InlineSpan {
this.alignment = ui.PlaceholderAlignment.bottom,
this.baseline,
super.style,
+ this.rawText,
});
/// The unicode character to represent a placeholder.
@@ -61,6 +62,8 @@ abstract class PlaceholderSpan extends InlineSpan {
/// This is ignored when using other alignment modes.
final TextBaseline? baseline;
+ final String? rawText;
+
/// [PlaceholderSpan]s are flattened to a `0xFFFC` object replacement character in the
/// plain text representation when `includePlaceholders` is true.
@override
@@ -74,6 +77,21 @@ abstract class PlaceholderSpan extends InlineSpan {
}
}
+ @override
+ void computeToPlainTextV2(
+ StringBuffer buffer,
+ Map<int, String> map, {
+ bool includeSemanticsLabels = true,
+ bool includePlaceholders = true,
+ }) {
+ if (includePlaceholders) {
+ if (rawText != null) {
+ map[buffer.length] = rawText!;
+ }
+ buffer.writeCharCode(placeholderCodeUnit);
+ }
+ }
+
@override
void computeSemanticsInformation(List<InlineSpanSemanticsInformation> collector) {
collector.add(InlineSpanSemanticsInformation.placeholder);
diff --git a/packages/flutter/lib/src/painting/text_span.dart b/packages/flutter/lib/src/painting/text_span.dart
index 09fba7fc4fb..57ee469358e 100644
--- a/packages/flutter/lib/src/painting/text_span.dart
+++ b/packages/flutter/lib/src/painting/text_span.dart
@@ -398,6 +398,31 @@ class TextSpan extends InlineSpan implements HitTestTarget, MouseTrackerAnnotati
}
}
+ @override
+ void computeToPlainTextV2(
+ StringBuffer buffer,
+ Map<int, String> map, {
+ bool includeSemanticsLabels = true,
+ bool includePlaceholders = true,
+ }) {
+ assert(debugAssertIsValid());
+ if (semanticsLabel != null && includeSemanticsLabels) {
+ buffer.write(semanticsLabel);
+ } else if (text != null) {
+ buffer.write(text);
+ }
+ if (children != null) {
+ for (final InlineSpan child in children!) {
+ child.computeToPlainTextV2(
+ buffer,
+ map,
+ includeSemanticsLabels: includeSemanticsLabels,
+ includePlaceholders: includePlaceholders,
+ );
+ }
+ }
+ }
+
@override
void computeSemanticsInformation(
List<InlineSpanSemanticsInformation> collector, {
diff --git a/packages/flutter/lib/src/rendering/paragraph.dart b/packages/flutter/lib/src/rendering/paragraph.dart
index 97a41dafb8d..0be1766148e 100644
index 97a41dafb8d..266dd6f0283 100644
--- a/packages/flutter/lib/src/rendering/paragraph.dart
+++ b/packages/flutter/lib/src/rendering/paragraph.dart
@@ -1925,9 +1925,9 @@ class _SelectableFragment
final Matrix4 transform = paragraph.getTransformTo(null);
transform.invert();
final Offset localPosition = MatrixUtils.transformPoint(transform, globalPosition);
- if (_rect.isEmpty) {
- return SelectionUtils.getResultBasedOnRect(_rect, localPosition);
@@ -502,27 +502,17 @@ class RenderParagraph extends RenderBox
}
List<_SelectableFragment> _getSelectableFragments() {
- final String plainText = text.toPlainText(includeSemanticsLabels: false);
- final result = <_SelectableFragment>[];
- var start = 0;
- while (start < plainText.length) {
- int end = plainText.indexOf(_placeholderCharacter, start);
- if (start != end) {
- if (end == -1) {
- end = plainText.length;
- }
- result.add(
- _SelectableFragment(
- paragraph: this,
- range: TextRange(start: start, end: end),
- fullText: plainText,
- ),
- );
- start = end;
- }
- start += 1;
- }
+ // if (_rect.isEmpty) {
+ // return SelectionUtils.getResultBasedOnRect(_rect, localPosition);
+ // }
final Offset adjustedOffset = SelectionUtils.adjustDragOffset(
- return result;
+ final (String plainText, Map<int, String> placeHolder) = text.toPlainTextV2(
+ includeSemanticsLabels: false,
+ );
+ return [
+ _SelectableFragment(
+ paragraph: this,
+ range: TextRange(start: 0, end: plainText.length),
+ fullText: plainText,
+ placeholder: placeHolder,
+ ),
+ ];
}
/// Determines whether the given [Selectable] was created by this
@@ -1467,7 +1457,7 @@ class RenderParagraph extends RenderBox
class _SelectableFragment
with Selectable, Diagnosticable, ChangeNotifier
implements TextLayoutMetrics {
- _SelectableFragment({required this.paragraph, required this.fullText, required this.range})
+ _SelectableFragment({required this.paragraph, required this.fullText, required this.range, required this.placeholder})
: assert(range.isValid && !range.isCollapsed && range.isNormalized) {
if (kFlutterMemoryAllocationsEnabled) {
ChangeNotifier.maybeDispatchObjectCreation(this);
@@ -1478,6 +1468,7 @@ class _SelectableFragment
final TextRange range;
final RenderParagraph paragraph;
final String fullText;
+ final Map<int,String> placeholder;
TextPosition? _textSelectionStart;
TextPosition? _textSelectionEnd;
@@ -1509,18 +1500,36 @@ class _SelectableFragment
final int selectionStart = _textSelectionStart!.offset;
final int selectionEnd = _textSelectionEnd!.offset;
final bool isReversed = selectionStart > selectionEnd;
- final Offset startOffsetInParagraphCoordinates = paragraph._getOffsetForPosition(
- TextPosition(offset: selectionStart),
- );
- final Offset endOffsetInParagraphCoordinates = selectionStart == selectionEnd
- ? startOffsetInParagraphCoordinates
- : paragraph._getOffsetForPosition(TextPosition(offset: selectionEnd));
+
final flipHandles = isReversed != (TextDirection.rtl == paragraph.textDirection);
final selection = TextSelection(baseOffset: selectionStart, extentOffset: selectionEnd);
+ final List<ui.TextBox> boxes = paragraph.getBoxesForSelection(selection, boxHeightStyle: .max);
final selectionRects = <Rect>[];
- for (final TextBox textBox in paragraph.getBoxesForSelection(selection)) {
+ for (final textBox in boxes) {
selectionRects.add(textBox.toRect());
}
+
+ final Offset startOffsetInParagraphCoordinates;
+ final Offset endOffsetInParagraphCoordinates;
+ if (boxes.isEmpty) {
+ final Offset offset =
+ paragraph._textPainter.getOffsetForCaret(selection.extent, Rect.zero) +
+ Offset(0, paragraph._textPainter.getFullHeightForCaret(selection.extent, Rect.zero));
+ startOffsetInParagraphCoordinates = endOffsetInParagraphCoordinates = Offset(
+ clampDouble(offset.dx, 0, paragraph._textPainter.width),
+ clampDouble(offset.dy, 0, paragraph._textPainter.height),
+ );
+ } else {
+ startOffsetInParagraphCoordinates = Offset(
+ clampDouble(boxes.first.start, 0, paragraph._textPainter.width),
+ clampDouble(boxes.first.bottom, 0, paragraph._textPainter.height),
+ );
+ endOffsetInParagraphCoordinates = Offset(
+ clampDouble(boxes.last.end, 0, paragraph._textPainter.width),
+ clampDouble(boxes.last.bottom, 0, paragraph._textPainter.height),
+ );
+ }
+
final selectionCollapsed = selectionStart == selectionEnd;
final (
TextSelectionHandleType startSelectionHandleType,
@@ -1564,12 +1573,14 @@ class _SelectableFragment
result = _updateSelectionEdge(
edgeUpdate.globalPosition,
isEnd: edgeUpdate.type == SelectionEventType.endEdgeUpdate,
+ isSingle: edgeUpdate.isSingle,
);
case TextGranularity.word:
result = _updateSelectionEdgeByTextBoundary(
edgeUpdate.globalPosition,
isEnd: edgeUpdate.type == SelectionEventType.endEdgeUpdate,
getTextBoundary: _getWordBoundaryAtPosition,
+ isSingle: edgeUpdate.isSingle,
);
case TextGranularity.paragraph:
result = _updateSelectionEdgeByMultiSelectableTextBoundary(
@@ -1626,9 +1637,33 @@ class _SelectableFragment
if (_textSelectionStart == null || _textSelectionEnd == null) {
return null;
}
- final int start = math.min(_textSelectionStart!.offset, _textSelectionEnd!.offset);
+ int start = math.min(_textSelectionStart!.offset, _textSelectionEnd!.offset);
final int end = math.max(_textSelectionStart!.offset, _textSelectionEnd!.offset);
- return SelectedContent(plainText: fullText.substring(start, end));
+ final String selectedText;
+ if (placeholder.isEmpty) {
+ selectedText = fullText.substring(start, end);
+ } else {
+ final buffer = StringBuffer();
+ for (final e in placeholder.entries) {
+ final i = e.key;
+ if (i < start) {
+ continue;
+ }
+ if (i >= end) {
+ break;
+ }
+ if (i != start) {
+ buffer.write(fullText.substring(start, i));
+ }
+ buffer.write(e.value);
+ start = i + 1;
+ }
+ if (start != end) {
+ buffer.write(fullText.substring(start, end));
+ }
+ selectedText = buffer.toString();
+ }
+ return SelectedContent(plainText: selectedText);
}
@override
@@ -1848,6 +1883,7 @@ class _SelectableFragment
Offset globalPosition, {
required bool isEnd,
required _TextBoundaryAtPosition getTextBoundary,
+ bool isSingle = false,
}) {
// When the start/end edges are swapped, i.e. the start is after the end, and
// the scrollable synthesizes an event for the opposite edge, this will potentially
@@ -1866,6 +1902,7 @@ class _SelectableFragment
_rect,
localPosition,
@@ -3568,7 +3568,10 @@ class _SelectableFragment
direction: paragraph.textDirection,
+ isSingle: isSingle,
);
final TextPosition position = paragraph.getPositionForOffset(adjustedOffset);
@@ -1920,7 +1957,7 @@ class _SelectableFragment
return SelectionUtils.getResultBasedOnRect(_rect, localPosition);
}
- SelectionResult _updateSelectionEdge(Offset globalPosition, {required bool isEnd}) {
+ SelectionResult _updateSelectionEdge(Offset globalPosition, {required bool isEnd, bool isSingle = false}) {
_setSelectionPosition(null, isEnd: isEnd);
final Matrix4 transform = paragraph.getTransformTo(null);
transform.invert();
@@ -1932,11 +1969,21 @@ class _SelectableFragment
_rect,
localPosition,
direction: paragraph.textDirection,
+ isSingle: isSingle,
);
final TextPosition position = _clampTextPosition(
paragraph.getPositionForOffset(adjustedOffset),
);
+ // if (rawText != null) {
+ // position = _clampTextPosition(
+ // adjustedOffset.dx * 2 > _rect.left + _rect.right
+ // ? TextPosition(offset: range.end)
+ // : TextPosition(offset: range.start),
+ // );
+ // } else {
+ // position = _clampTextPosition(paragraph.getPositionForOffset(adjustedOffset));
+ // }
_setSelectionPosition(position, isEnd: isEnd);
if (position.offset == range.end) {
return SelectionResult.next;
@@ -3568,7 +3615,10 @@ class _SelectableFragment
final selectionPaint = Paint()
..style = PaintingStyle.fill
..color = paragraph.selectionColor!;
@@ -28,10 +333,19 @@ index 97a41dafb8d..0be1766148e 100644
}
}
diff --git a/packages/flutter/lib/src/rendering/selection.dart b/packages/flutter/lib/src/rendering/selection.dart
index a813e141dc4..78b9bb84c0e 100644
index a813e141dc4..e5ef2103645 100644
--- a/packages/flutter/lib/src/rendering/selection.dart
+++ b/packages/flutter/lib/src/rendering/selection.dart
@@ -114,6 +114,8 @@ abstract class SelectionHandler implements ValueListenable<SelectionGeometry> {
@@ -6,6 +6,8 @@
/// @docImport 'package:flutter/material.dart';
library;
+import 'dart:math' as math;
+
import 'package:flutter/foundation.dart';
import 'package:vector_math/vector_math_64.dart';
@@ -114,6 +116,8 @@ abstract class SelectionHandler implements ValueListenable<SelectionGeometry> {
/// The length of the content in this object.
int get contentLength;
@@ -40,7 +354,7 @@ index a813e141dc4..78b9bb84c0e 100644
}
/// This class stores the range information of the selection under a [Selectable]
@@ -236,6 +238,8 @@ class SelectedContent with Diagnosticable {
@@ -236,6 +240,8 @@ class SelectedContent with Diagnosticable {
/// See also:
/// * [SelectableRegion], which provides an overview of selection system.
mixin Selectable implements SelectionHandler {
@@ -49,6 +363,53 @@ index a813e141dc4..78b9bb84c0e 100644
/// {@macro flutter.rendering.RenderObject.getTransformTo}
Matrix4 getTransformTo(RenderObject? ancestor);
@@ -356,10 +362,19 @@ abstract final class SelectionUtils {
Rect targetRect,
Offset point, {
TextDirection direction = TextDirection.ltr,
+ bool isSingle = false,
}) {
if (targetRect.contains(point)) {
return point;
}
+
+ if (isSingle || (point.dy >= targetRect.top && point.dy <= targetRect.bottom)) {
+ return Offset(
+ math.min(point.dx, targetRect.right),
+ math.min(point.dy, targetRect.bottom - .1),
+ );
+ }
+
if (point.dy <= targetRect.top ||
point.dy <= targetRect.bottom && point.dx <= targetRect.left) {
// Area 1
@@ -520,7 +535,7 @@ class SelectionEdgeUpdateEvent extends SelectionEvent {
///
/// The [granularity] contains the granularity which the selection edge should move by.
/// This value defaults to [TextGranularity.character].
- const SelectionEdgeUpdateEvent.forStart({
+ SelectionEdgeUpdateEvent.forStart({
required this.globalPosition,
TextGranularity? granularity,
}) : granularity = granularity ?? TextGranularity.character,
@@ -532,7 +547,7 @@ class SelectionEdgeUpdateEvent extends SelectionEvent {
///
/// The [granularity] contains the granularity which the selection edge should move by.
/// This value defaults to [TextGranularity.character].
- const SelectionEdgeUpdateEvent.forEnd({
+ SelectionEdgeUpdateEvent.forEnd({
required this.globalPosition,
TextGranularity? granularity,
}) : granularity = granularity ?? TextGranularity.character,
@@ -547,6 +562,8 @@ class SelectionEdgeUpdateEvent extends SelectionEvent {
///
/// Defaults to [TextGranularity.character].
final TextGranularity granularity;
+
+ bool isSingle = false;
}
/// Extends the start or end of the selection by a given [TextGranularity].
diff --git a/packages/flutter/lib/src/widgets/scrollable.dart b/packages/flutter/lib/src/widgets/scrollable.dart
index 9df2d633f16..54d30883245 100644
--- a/packages/flutter/lib/src/widgets/scrollable.dart
@@ -64,7 +425,7 @@ index 9df2d633f16..54d30883245 100644
static const double _kDefaultDragTargetSize = 0;
diff --git a/packages/flutter/lib/src/widgets/selectable_region.dart b/packages/flutter/lib/src/widgets/selectable_region.dart
index 59de8bae20b..39bfeb2b4f0 100644
index 59de8bae20b..9ce4810aa68 100644
--- a/packages/flutter/lib/src/widgets/selectable_region.dart
+++ b/packages/flutter/lib/src/widgets/selectable_region.dart
@@ -401,9 +401,11 @@ class SelectableRegionState extends State<SelectableRegion>
@@ -209,7 +570,35 @@ index 59de8bae20b..39bfeb2b4f0 100644
/// The set of [Selectable]s that have received start events.
final Set<Selectable> _hasReceivedStartEvent = <Selectable>{};
@@ -2822,8 +2801,17 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai
@@ -2271,6 +2250,18 @@ class StaticSelectionContainerDelegate extends MultiSelectableSelectionContainer
super.dispose();
}
+ void handleSelectionEdgeUpdateEvent(SelectionEdgeUpdateEvent event) {
+ event.isSingle = isSingle;
+ }
+
+ @override
+ SelectionResult dispatchSelectionEvent(SelectionEvent event) {
+ if (event is SelectionEdgeUpdateEvent) {
+ handleSelectionEdgeUpdateEvent(event);
+ }
+ return super.dispatchSelectionEvent(event);
+ }
+
@override
SelectionResult dispatchSelectionEventToChild(Selectable selectable, SelectionEvent event) {
switch (event.type) {
@@ -2367,6 +2358,8 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai
/// Gets the list of [Selectable]s this delegate is managing.
List<Selectable> selectables = <Selectable>[];
+ bool get isSingle => selectables.length == 1;
+
/// The number of additional pixels added to the selection handle drawable
/// area.
///
@@ -2822,8 +2815,17 @@ abstract class MultiSelectableSelectionContainerDelegate extends SelectionContai
return null;
}
final buffer = StringBuffer();
@@ -242,3 +631,30 @@ index 5d5a2386a7a..92e7d1eb70a 100644
BuildContext? _selectionContainerContext;
/// Gets the paint transform from the [Selectable] child to
diff --git a/packages/flutter/lib/src/widgets/text.dart b/packages/flutter/lib/src/widgets/text.dart
index a3aa5fde862..8fbf6a1b16d 100644
--- a/packages/flutter/lib/src/widgets/text.dart
+++ b/packages/flutter/lib/src/widgets/text.dart
@@ -992,6 +992,9 @@ class _SelectableTextContainerDelegate extends StaticSelectionContainerDelegate
final GlobalKey _textKey;
RenderParagraph get paragraph => _textKey.currentContext!.findRenderObject()! as RenderParagraph;
+ @override
+ void handleSelectionEdgeUpdateEvent(SelectionEdgeUpdateEvent event) {}
+
@override
SelectionResult handleSelectParagraph(SelectParagraphSelectionEvent event) {
final SelectionResult result = _handleSelectParagraph(event);
diff --git a/packages/flutter/lib/src/widgets/widget_span.dart b/packages/flutter/lib/src/widgets/widget_span.dart
index 09da3b76525..4a83efb0392 100644
--- a/packages/flutter/lib/src/widgets/widget_span.dart
+++ b/packages/flutter/lib/src/widgets/widget_span.dart
@@ -78,7 +78,7 @@ class WidgetSpan extends PlaceholderSpan {
///
/// A [TextStyle] may be provided with the [style] property, but only the
/// decoration, foreground, background, and spacing options will be used.
- const WidgetSpan({required this.child, super.alignment, super.baseline, super.style})
+ const WidgetSpan({required this.child, super.alignment, super.baseline, super.style, super.rawText})
: assert(
baseline != null ||
!(identical(alignment, ui.PlaceholderAlignment.aboveBaseline) ||

View File

@@ -1,222 +0,0 @@
diff --git a/packages/flutter/lib/src/painting/inline_span.dart b/packages/flutter/lib/src/painting/inline_span.dart
index 0c4a0be6177..77baa589699 100644
--- a/packages/flutter/lib/src/painting/inline_span.dart
+++ b/packages/flutter/lib/src/painting/inline_span.dart
@@ -315,6 +315,21 @@ abstract class InlineSpan extends DiagnosticableTree {
return buffer.toString();
}
+ (String, Map<int, String>) toPlainTextV2({
+ bool includeSemanticsLabels = true,
+ bool includePlaceholders = true,
+ }) {
+ final buffer = StringBuffer();
+ final map = <int, String>{};
+ computeToPlainTextV2(
+ buffer,
+ map,
+ includeSemanticsLabels: includeSemanticsLabels,
+ includePlaceholders: includePlaceholders,
+ );
+ return (buffer.toString(), map);
+ }
+
/// Flattens the [InlineSpan] tree to a list of
/// [InlineSpanSemanticsInformation] objects.
///
@@ -358,6 +373,14 @@ abstract class InlineSpan extends DiagnosticableTree {
bool includePlaceholders = true,
});
+ @protected
+ void computeToPlainTextV2(
+ StringBuffer buffer,
+ Map<int, String> map, {
+ bool includeSemanticsLabels = true,
+ bool includePlaceholders = true,
+ });
+
/// Returns the UTF-16 code unit at the given `index` in the flattened string.
///
/// This only accounts for the [TextSpan.text] values and ignores [PlaceholderSpan]s.
diff --git a/packages/flutter/lib/src/painting/placeholder_span.dart b/packages/flutter/lib/src/painting/placeholder_span.dart
index 110ff860310..7ad808d05bc 100644
--- a/packages/flutter/lib/src/painting/placeholder_span.dart
+++ b/packages/flutter/lib/src/painting/placeholder_span.dart
@@ -45,6 +45,7 @@ abstract class PlaceholderSpan extends InlineSpan {
this.alignment = ui.PlaceholderAlignment.bottom,
this.baseline,
super.style,
+ this.rawText,
});
/// The unicode character to represent a placeholder.
@@ -61,6 +62,8 @@ abstract class PlaceholderSpan extends InlineSpan {
/// This is ignored when using other alignment modes.
final TextBaseline? baseline;
+ final String? rawText;
+
/// [PlaceholderSpan]s are flattened to a `0xFFFC` object replacement character in the
/// plain text representation when `includePlaceholders` is true.
@override
@@ -74,6 +77,21 @@ abstract class PlaceholderSpan extends InlineSpan {
}
}
+ @override
+ void computeToPlainTextV2(
+ StringBuffer buffer,
+ Map<int, String> map, {
+ bool includeSemanticsLabels = true,
+ bool includePlaceholders = true,
+ }) {
+ if (includePlaceholders) {
+ if (rawText != null) {
+ map[buffer.length] = rawText!;
+ }
+ buffer.writeCharCode(placeholderCodeUnit);
+ }
+ }
+
@override
void computeSemanticsInformation(List<InlineSpanSemanticsInformation> collector) {
collector.add(InlineSpanSemanticsInformation.placeholder);
diff --git a/packages/flutter/lib/src/painting/text_span.dart b/packages/flutter/lib/src/painting/text_span.dart
index 09fba7fc4fb..57ee469358e 100644
--- a/packages/flutter/lib/src/painting/text_span.dart
+++ b/packages/flutter/lib/src/painting/text_span.dart
@@ -398,6 +398,31 @@ class TextSpan extends InlineSpan implements HitTestTarget, MouseTrackerAnnotati
}
}
+ @override
+ void computeToPlainTextV2(
+ StringBuffer buffer,
+ Map<int, String> map, {
+ bool includeSemanticsLabels = true,
+ bool includePlaceholders = true,
+ }) {
+ assert(debugAssertIsValid());
+ if (semanticsLabel != null && includeSemanticsLabels) {
+ buffer.write(semanticsLabel);
+ } else if (text != null) {
+ buffer.write(text);
+ }
+ if (children != null) {
+ for (final InlineSpan child in children!) {
+ child.computeToPlainTextV2(
+ buffer,
+ map,
+ includeSemanticsLabels: includeSemanticsLabels,
+ includePlaceholders: includePlaceholders,
+ );
+ }
+ }
+ }
+
@override
void computeSemanticsInformation(
List<InlineSpanSemanticsInformation> collector, {
diff --git a/packages/flutter/lib/src/rendering/paragraph.dart b/packages/flutter/lib/src/rendering/paragraph.dart
index 97a41dafb8d..00672540c54 100644
--- a/packages/flutter/lib/src/rendering/paragraph.dart
+++ b/packages/flutter/lib/src/rendering/paragraph.dart
@@ -502,13 +502,16 @@ class RenderParagraph extends RenderBox
}
List<_SelectableFragment> _getSelectableFragments() {
- final String plainText = text.toPlainText(includeSemanticsLabels: false);
+ final (String plainText, Map<int, String> placeHolder) = text.toPlainTextV2(
+ includeSemanticsLabels: false,
+ );
final result = <_SelectableFragment>[];
var start = 0;
while (start < plainText.length) {
int end = plainText.indexOf(_placeholderCharacter, start);
+ final hasPlaceholder = end != -1;
if (start != end) {
- if (end == -1) {
+ if (!hasPlaceholder) {
end = plainText.length;
}
result.add(
@@ -521,6 +524,18 @@ class RenderParagraph extends RenderBox
start = end;
}
start += 1;
+ if (hasPlaceholder) {
+ if (placeHolder[end] case final rawText?) {
+ result.add(
+ _SelectableFragment(
+ paragraph: this,
+ range: TextRange(start: end, end: end + 1),
+ fullText: plainText,
+ rawText: rawText,
+ ),
+ );
+ }
+ }
}
return result;
}
@@ -1467,7 +1482,7 @@ class RenderParagraph extends RenderBox
class _SelectableFragment
with Selectable, Diagnosticable, ChangeNotifier
implements TextLayoutMetrics {
- _SelectableFragment({required this.paragraph, required this.fullText, required this.range})
+ _SelectableFragment({required this.paragraph, required this.fullText, required this.range, this.rawText})
: assert(range.isValid && !range.isCollapsed && range.isNormalized) {
if (kFlutterMemoryAllocationsEnabled) {
ChangeNotifier.maybeDispatchObjectCreation(this);
@@ -1478,6 +1493,7 @@ class _SelectableFragment
final TextRange range;
final RenderParagraph paragraph;
final String fullText;
+ final String? rawText;
TextPosition? _textSelectionStart;
TextPosition? _textSelectionEnd;
@@ -1626,6 +1642,9 @@ class _SelectableFragment
if (_textSelectionStart == null || _textSelectionEnd == null) {
return null;
}
+ if (rawText != null) {
+ return SelectedContent(plainText: rawText!);
+ }
final int start = math.min(_textSelectionStart!.offset, _textSelectionEnd!.offset);
final int end = math.max(_textSelectionStart!.offset, _textSelectionEnd!.offset);
return SelectedContent(plainText: fullText.substring(start, end));
@@ -1934,9 +1953,16 @@ class _SelectableFragment
direction: paragraph.textDirection,
);
- final TextPosition position = _clampTextPosition(
- paragraph.getPositionForOffset(adjustedOffset),
- );
+ final TextPosition position;
+ if (rawText != null) {
+ position = _clampTextPosition(
+ adjustedOffset.dx * 2 > _rect.left + _rect.right
+ ? TextPosition(offset: range.end)
+ : TextPosition(offset: range.start),
+ );
+ } else {
+ position = _clampTextPosition(paragraph.getPositionForOffset(adjustedOffset));
+ }
_setSelectionPosition(position, isEnd: isEnd);
if (position.offset == range.end) {
return SelectionResult.next;
diff --git a/packages/flutter/lib/src/widgets/widget_span.dart b/packages/flutter/lib/src/widgets/widget_span.dart
index 09da3b76525..4a83efb0392 100644
--- a/packages/flutter/lib/src/widgets/widget_span.dart
+++ b/packages/flutter/lib/src/widgets/widget_span.dart
@@ -78,7 +78,7 @@ class WidgetSpan extends PlaceholderSpan {
///
/// A [TextStyle] may be provided with the [style] property, but only the
/// decoration, foreground, background, and spacing options will be used.
- const WidgetSpan({required this.child, super.alignment, super.baseline, super.style})
+ const WidgetSpan({required this.child, super.alignment, super.baseline, super.style, super.rawText})
: assert(
baseline != null ||
!(identical(alignment, ui.PlaceholderAlignment.aboveBaseline) ||