Skip to content

Commit 50cd737

Browse files
authored
Expose text boundary combiner class (#112085)
1 parent 2adee31 commit 50cd737

File tree

4 files changed

+249
-154
lines changed

4 files changed

+249
-154
lines changed

packages/flutter/lib/src/painting/text_painter.dart

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1129,12 +1129,6 @@ class TextPainter {
11291129
/// {@endtemplate}
11301130
TextRange getWordBoundary(TextPosition position) {
11311131
assert(_debugAssertTextLayoutIsValid);
1132-
// TODO(chunhtai): remove this workaround once ui.Paragraph.getWordBoundary
1133-
// can handle caret position.
1134-
// https://github.com/flutter/flutter/issues/111751.
1135-
if (position.affinity == TextAffinity.upstream) {
1136-
position = TextPosition(offset: position.offset - 1);
1137-
}
11381132
return _paragraph!.getWordBoundary(position);
11391133
}
11401134

packages/flutter/lib/src/services/text_boundary.dart

Lines changed: 172 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,18 @@ abstract class TextBoundary {
3838
end: getTrailingTextBoundaryAt(position).offset,
3939
);
4040
}
41+
42+
/// Gets the boundary by calling the left-hand side and pipe the result to
43+
/// right-hand side.
44+
///
45+
/// Combining two text boundaries can be useful if one wants to ignore certain
46+
/// text before finding the text boundary. For example, use
47+
/// [WhitespaceBoundary] + [WordBoundary] to ignores any white space before
48+
/// finding word boundary if the input position happens to be a whitespace
49+
/// character.
50+
TextBoundary operator +(TextBoundary other) {
51+
return _ExpandedTextBoundary(inner: other, outer: this);
52+
}
4153
}
4254

4355
/// A text boundary that uses characters as logical boundaries.
@@ -110,7 +122,7 @@ class CharacterBoundary extends TextBoundary {
110122
/// This class uses [UAX #29](https://unicode.org/reports/tr29/) defined word
111123
/// boundaries to calculate its logical boundaries.
112124
class WordBoundary extends TextBoundary {
113-
/// Creates a [CharacterBoundary] with the text and layout information.
125+
/// Creates a [WordBoundary] with the text and layout information.
114126
const WordBoundary(this._textLayout);
115127

116128
final TextLayoutMetrics _textLayout;
@@ -122,21 +134,27 @@ class WordBoundary extends TextBoundary {
122134
affinity: TextAffinity.downstream, // ignore: avoid_redundant_argument_values
123135
);
124136
}
137+
125138
@override
126139
TextPosition getTrailingTextBoundaryAt(TextPosition position) {
127140
return TextPosition(
128141
offset: _textLayout.getWordBoundary(position).end,
129142
affinity: TextAffinity.upstream,
130143
);
131144
}
145+
146+
@override
147+
TextRange getTextBoundaryAt(TextPosition position) {
148+
return _textLayout.getWordBoundary(position);
149+
}
132150
}
133151

134152
/// A text boundary that uses line breaks as logical boundaries.
135153
///
136154
/// The input [TextPosition]s will be interpreted as caret locations if
137155
/// [TextLayoutMetrics.getLineAtOffset] is text-affinity-aware.
138156
class LineBreak extends TextBoundary {
139-
/// Creates a [CharacterBoundary] with the text and layout information.
157+
/// Creates a [LineBreak] with the text and layout information.
140158
const LineBreak(this._textLayout);
141159

142160
final TextLayoutMetrics _textLayout;
@@ -155,14 +173,19 @@ class LineBreak extends TextBoundary {
155173
affinity: TextAffinity.upstream,
156174
);
157175
}
176+
177+
@override
178+
TextRange getTextBoundaryAt(TextPosition position) {
179+
return _textLayout.getLineAtOffset(position);
180+
}
158181
}
159182

160183
/// A text boundary that uses the entire document as logical boundary.
161184
///
162185
/// The document boundary is unique and is a constant function of the input
163186
/// position.
164187
class DocumentBoundary extends TextBoundary {
165-
/// Creates a [CharacterBoundary] with the text
188+
/// Creates a [DocumentBoundary] with the text
166189
const DocumentBoundary(this._text);
167190

168191
final String _text;
@@ -177,3 +200,149 @@ class DocumentBoundary extends TextBoundary {
177200
);
178201
}
179202
}
203+
204+
/// A text boundary that uses the first non-whitespace character as the logical
205+
/// boundary.
206+
///
207+
/// This text boundary uses [TextLayoutMetrics.isWhitespace] to identify white
208+
/// spaces, this includes newline characters from ASCII and separators from the
209+
/// [unicode separator category](https://en.wikipedia.org/wiki/Whitespace_character).
210+
class WhitespaceBoundary extends TextBoundary {
211+
/// Creates a [WhitespaceBoundary] with the text.
212+
const WhitespaceBoundary(this._text);
213+
214+
final String _text;
215+
216+
@override
217+
TextPosition getLeadingTextBoundaryAt(TextPosition position) {
218+
// Handles outside of string end.
219+
if (position.offset > _text.length || (position.offset == _text.length && position.affinity == TextAffinity.downstream)) {
220+
position = TextPosition(offset: _text.length, affinity: TextAffinity.upstream);
221+
}
222+
// Handles outside of string start.
223+
if (position.offset <= 0) {
224+
return const TextPosition(offset: 0);
225+
}
226+
int index = position.offset;
227+
if (position.affinity == TextAffinity.downstream && !TextLayoutMetrics.isWhitespace(_text.codeUnitAt(index))) {
228+
return position;
229+
}
230+
231+
while ((index -= 1) >= 0) {
232+
if (!TextLayoutMetrics.isWhitespace(_text.codeUnitAt(index))) {
233+
return TextPosition(offset: index + 1, affinity: TextAffinity.upstream);
234+
}
235+
}
236+
return const TextPosition(offset: 0);
237+
}
238+
239+
@override
240+
TextPosition getTrailingTextBoundaryAt(TextPosition position) {
241+
// Handles outside of right bound.
242+
if (position.offset >= _text.length) {
243+
return TextPosition(offset: _text.length, affinity: TextAffinity.upstream);
244+
}
245+
// Handles outside of left bound.
246+
if (position.offset < 0 || (position.offset == 0 && position.affinity == TextAffinity.upstream)) {
247+
position = const TextPosition(offset: 0);
248+
}
249+
250+
int index = position.offset;
251+
if (position.affinity == TextAffinity.upstream && !TextLayoutMetrics.isWhitespace(_text.codeUnitAt(index - 1))) {
252+
return position;
253+
}
254+
255+
for (; index < _text.length; index += 1) {
256+
if (!TextLayoutMetrics.isWhitespace(_text.codeUnitAt(index))) {
257+
return TextPosition(offset: index);
258+
}
259+
}
260+
return TextPosition(offset: _text.length, affinity: TextAffinity.upstream);
261+
}
262+
}
263+
264+
/// Gets the boundary by calling the [outer] and pipe the result to
265+
/// [inner].
266+
class _ExpandedTextBoundary extends TextBoundary {
267+
/// Creates a [_ExpandedTextBoundary] with inner and outter boundaries
268+
const _ExpandedTextBoundary({required this.inner, required this.outer});
269+
270+
/// The inner boundary to call with the result from [outer].
271+
final TextBoundary inner;
272+
273+
/// The outer boundary to call with the input position.
274+
///
275+
/// The result is piped to the [inner] before returning to the caller.
276+
final TextBoundary outer;
277+
278+
@override
279+
TextPosition getLeadingTextBoundaryAt(TextPosition position) {
280+
return inner.getLeadingTextBoundaryAt(
281+
outer.getLeadingTextBoundaryAt(position),
282+
);
283+
}
284+
285+
@override
286+
TextPosition getTrailingTextBoundaryAt(TextPosition position) {
287+
return inner.getTrailingTextBoundaryAt(
288+
outer.getTrailingTextBoundaryAt(position),
289+
);
290+
}
291+
}
292+
293+
/// A text boundary that will push input text position forward or backward
294+
/// one affinity
295+
///
296+
/// To push a text position forward one affinity unit, this proxy converts
297+
/// affinity to downstream if it is upstream; otherwise it increase the offset
298+
/// by one with its affinity sets to upstream. For example,
299+
/// `TextPosition(1, upstream)` becomes `TextPosition(1, downstream)`,
300+
/// `TextPosition(4, downstream)` becomes `TextPosition(5, upstream)`.
301+
///
302+
/// See also:
303+
/// * [PushTextPosition.forward], a text boundary to push the input position
304+
/// forward.
305+
/// * [PushTextPosition.backward], a text boundary to push the input position
306+
/// backward.
307+
class PushTextPosition extends TextBoundary {
308+
const PushTextPosition._(this._forward);
309+
310+
/// A text boundary that pushes the input position forward.
311+
static const TextBoundary forward = PushTextPosition._(true);
312+
313+
/// A text boundary that pushes the input position backward.
314+
static const TextBoundary backward = PushTextPosition._(false);
315+
316+
/// Whether to push the input position forward or backward.
317+
final bool _forward;
318+
319+
TextPosition _calculateTargetPosition(TextPosition position) {
320+
if (_forward) {
321+
switch(position.affinity) {
322+
case TextAffinity.upstream:
323+
return TextPosition(offset: position.offset);
324+
case TextAffinity.downstream:
325+
return position = TextPosition(
326+
offset: position.offset + 1,
327+
affinity: TextAffinity.upstream,
328+
);
329+
}
330+
} else {
331+
switch(position.affinity) {
332+
case TextAffinity.upstream:
333+
return position = TextPosition(offset: position.offset - 1);
334+
case TextAffinity.downstream:
335+
return TextPosition(
336+
offset: position.offset,
337+
affinity: TextAffinity.upstream,
338+
);
339+
}
340+
}
341+
}
342+
343+
@override
344+
TextPosition getLeadingTextBoundaryAt(TextPosition position) => _calculateTargetPosition(position);
345+
346+
@override
347+
TextPosition getTrailingTextBoundaryAt(TextPosition position) => _calculateTargetPosition(position);
348+
}

0 commit comments

Comments
 (0)