Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
F
Front-End
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
abdullh.alsoleman
Front-End
Commits
bcc1dc6b
Unverified
Commit
bcc1dc6b
authored
Sep 16, 2022
by
chunhtai
Committed by
GitHub
Sep 16, 2022
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Makes TextBoundary and its subclasses public (#110367)
parent
a7f028f1
Changes
11
Hide whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
518 additions
and
290 deletions
+518
-290
services.dart
packages/flutter/lib/services.dart
+1
-0
text_painter.dart
packages/flutter/lib/src/painting/text_painter.dart
+6
-0
editable.dart
packages/flutter/lib/src/rendering/editable.dart
+19
-6
text_boundary.dart
packages/flutter/lib/src/services/text_boundary.dart
+179
-0
editable_text.dart
packages/flutter/lib/src/widgets/editable_text.dart
+173
-267
text_field_test.dart
packages/flutter/test/cupertino/text_field_test.dart
+4
-2
text_field_test.dart
packages/flutter/test/material/text_field_test.dart
+8
-13
text_boundary_test.dart
packages/flutter/test/services/text_boundary_test.dart
+97
-0
editable_text_shortcuts_test.dart
...es/flutter/test/widgets/editable_text_shortcuts_test.dart
+7
-0
editable_text_test.dart
packages/flutter/test/widgets/editable_text_test.dart
+22
-1
editable_text_utils.dart
packages/flutter/test/widgets/editable_text_utils.dart
+2
-1
No files found.
packages/flutter/lib/services.dart
View file @
bcc1dc6b
...
@@ -43,6 +43,7 @@ export 'src/services/system_channels.dart';
...
@@ -43,6 +43,7 @@ export 'src/services/system_channels.dart';
export
'src/services/system_chrome.dart'
;
export
'src/services/system_chrome.dart'
;
export
'src/services/system_navigator.dart'
;
export
'src/services/system_navigator.dart'
;
export
'src/services/system_sound.dart'
;
export
'src/services/system_sound.dart'
;
export
'src/services/text_boundary.dart'
;
export
'src/services/text_editing.dart'
;
export
'src/services/text_editing.dart'
;
export
'src/services/text_editing_delta.dart'
;
export
'src/services/text_editing_delta.dart'
;
export
'src/services/text_formatter.dart'
;
export
'src/services/text_formatter.dart'
;
...
...
packages/flutter/lib/src/painting/text_painter.dart
View file @
bcc1dc6b
...
@@ -1129,6 +1129,12 @@ class TextPainter {
...
@@ -1129,6 +1129,12 @@ class TextPainter {
/// {@endtemplate}
/// {@endtemplate}
TextRange
getWordBoundary
(
TextPosition
position
)
{
TextRange
getWordBoundary
(
TextPosition
position
)
{
assert
(
_debugAssertTextLayoutIsValid
);
assert
(
_debugAssertTextLayoutIsValid
);
// TODO(chunhtai): remove this workaround once ui.Paragraph.getWordBoundary
// can handle caret position.
// https://github.com/flutter/flutter/issues/111751.
if
(
position
.
affinity
==
TextAffinity
.
upstream
)
{
position
=
TextPosition
(
offset:
position
.
offset
-
1
);
}
return
_paragraph
!.
getWordBoundary
(
position
);
return
_paragraph
!.
getWordBoundary
(
position
);
}
}
...
...
packages/flutter/lib/src/rendering/editable.dart
View file @
bcc1dc6b
...
@@ -2040,7 +2040,6 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
...
@@ -2040,7 +2040,6 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
final
TextSelection
firstWord
=
_getWordAtOffset
(
firstPosition
);
final
TextSelection
firstWord
=
_getWordAtOffset
(
firstPosition
);
final
TextSelection
lastWord
=
to
==
null
?
final
TextSelection
lastWord
=
to
==
null
?
firstWord
:
_getWordAtOffset
(
_textPainter
.
getPositionForOffset
(
globalToLocal
(
to
-
_paintOffset
)));
firstWord
:
_getWordAtOffset
(
_textPainter
.
getPositionForOffset
(
globalToLocal
(
to
-
_paintOffset
)));
_setSelection
(
_setSelection
(
TextSelection
(
TextSelection
(
baseOffset:
firstWord
.
base
.
offset
,
baseOffset:
firstWord
.
base
.
offset
,
...
@@ -2071,14 +2070,28 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
...
@@ -2071,14 +2070,28 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
TextSelection
_getWordAtOffset
(
TextPosition
position
)
{
TextSelection
_getWordAtOffset
(
TextPosition
position
)
{
debugAssertLayoutUpToDate
();
debugAssertLayoutUpToDate
();
final
TextRange
word
=
_textPainter
.
getWordBoundary
(
position
);
// When long-pressing past the end of the text, we want a collapsed cursor.
// When long-pressing past the end of the text, we want a collapsed cursor.
if
(
position
.
offset
>=
word
.
end
)
{
if
(
position
.
offset
>=
_plainText
.
length
)
{
return
TextSelection
.
fromPosition
(
position
);
return
TextSelection
.
fromPosition
(
TextPosition
(
offset:
_plainText
.
length
,
affinity:
TextAffinity
.
upstream
)
);
}
}
// If text is obscured, the entire sentence should be treated as one word.
// If text is obscured, the entire sentence should be treated as one word.
if
(
obscureText
)
{
if
(
obscureText
)
{
return
TextSelection
(
baseOffset:
0
,
extentOffset:
_plainText
.
length
);
return
TextSelection
(
baseOffset:
0
,
extentOffset:
_plainText
.
length
);
}
final
TextRange
word
=
_textPainter
.
getWordBoundary
(
position
);
final
int
effectiveOffset
;
switch
(
position
.
affinity
)
{
case
TextAffinity
.
upstream
:
// upstream affinity is effectively -1 in text position.
effectiveOffset
=
position
.
offset
-
1
;
break
;
case
TextAffinity
.
downstream
:
effectiveOffset
=
position
.
offset
;
break
;
}
// On iOS, select the previous word if there is a previous word, or select
// On iOS, select the previous word if there is a previous word, or select
// to the end of the next word if there is a next word. Select nothing if
// to the end of the next word if there is a next word. Select nothing if
// there is neither a previous word nor a next word.
// there is neither a previous word nor a next word.
...
@@ -2086,8 +2099,8 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
...
@@ -2086,8 +2099,8 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
// If the platform is Android and the text is read only, try to select the
// If the platform is Android and the text is read only, try to select the
// previous word if there is one; otherwise, select the single whitespace at
// previous word if there is one; otherwise, select the single whitespace at
// the position.
// the position.
}
else
if
(
TextLayoutMetrics
.
isWhitespace
(
_plainText
.
codeUnitAt
(
position
.
o
ffset
))
if
(
TextLayoutMetrics
.
isWhitespace
(
_plainText
.
codeUnitAt
(
effectiveO
ffset
))
&&
position
.
o
ffset
>
0
)
{
&&
effectiveO
ffset
>
0
)
{
assert
(
defaultTargetPlatform
!=
null
);
assert
(
defaultTargetPlatform
!=
null
);
final
TextRange
?
previousWord
=
_getPreviousWord
(
word
.
start
);
final
TextRange
?
previousWord
=
_getPreviousWord
(
word
.
start
);
switch
(
defaultTargetPlatform
)
{
switch
(
defaultTargetPlatform
)
{
...
...
packages/flutter/lib/src/services/text_boundary.dart
0 → 100644
View file @
bcc1dc6b
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'dart:math'
as
math
;
import
'dart:ui'
;
import
'package:characters/characters.dart'
show
CharacterRange
;
import
'text_layout_metrics.dart'
;
/// An interface for retrieving the logical text boundary (left-closed-right-open)
/// at a given location in a document.
///
/// The input [TextPosition] points to a position between 2 code units (which
/// can be visually represented by the caret if the selection were to collapse
/// to that position). The [TextPosition.affinity] is used to determine which
/// code unit it points. For example, `TextPosition(i, upstream)` points to
/// code unit `i - 1` and `TextPosition(i, downstream)` points to code unit `i`.
abstract
class
TextBoundary
{
/// A constant constructor to enable subclass override.
const
TextBoundary
();
/// Returns the leading text boundary at the given location.
///
/// The return value must be less or equal to the input position.
TextPosition
getLeadingTextBoundaryAt
(
TextPosition
position
);
/// Returns the trailing text boundary at the given location, exclusive.
///
/// The return value must be greater or equal to the input position.
TextPosition
getTrailingTextBoundaryAt
(
TextPosition
position
);
/// Gets the text boundary range that encloses the input position.
TextRange
getTextBoundaryAt
(
TextPosition
position
)
{
return
TextRange
(
start:
getLeadingTextBoundaryAt
(
position
).
offset
,
end:
getTrailingTextBoundaryAt
(
position
).
offset
,
);
}
}
/// A text boundary that uses characters as logical boundaries.
///
/// This class takes grapheme clusters into account and avoid creating
/// boundaries that generate malformed utf-16 characters.
class
CharacterBoundary
extends
TextBoundary
{
/// Creates a [CharacterBoundary] with the text.
const
CharacterBoundary
(
this
.
_text
);
final
String
_text
;
@override
TextPosition
getLeadingTextBoundaryAt
(
TextPosition
position
)
{
if
(
position
.
offset
<=
0
)
{
return
const
TextPosition
(
offset:
0
);
}
if
(
position
.
offset
>
_text
.
length
||
(
position
.
offset
==
_text
.
length
&&
position
.
affinity
==
TextAffinity
.
downstream
))
{
return
TextPosition
(
offset:
_text
.
length
,
affinity:
TextAffinity
.
upstream
);
}
final
int
endOffset
;
final
int
startOffset
;
switch
(
position
.
affinity
)
{
case
TextAffinity
.
upstream
:
startOffset
=
math
.
min
(
position
.
offset
-
1
,
_text
.
length
);
endOffset
=
math
.
min
(
position
.
offset
,
_text
.
length
);
break
;
case
TextAffinity
.
downstream
:
startOffset
=
math
.
min
(
position
.
offset
,
_text
.
length
);
endOffset
=
math
.
min
(
position
.
offset
+
1
,
_text
.
length
);
break
;
}
return
TextPosition
(
offset:
CharacterRange
.
at
(
_text
,
startOffset
,
endOffset
).
stringBeforeLength
,
);
}
@override
TextPosition
getTrailingTextBoundaryAt
(
TextPosition
position
)
{
if
(
position
.
offset
<
0
||
(
position
.
offset
==
0
&&
position
.
affinity
==
TextAffinity
.
upstream
))
{
return
const
TextPosition
(
offset:
0
);
}
if
(
position
.
offset
>=
_text
.
length
)
{
return
TextPosition
(
offset:
_text
.
length
,
affinity:
TextAffinity
.
upstream
);
}
final
int
endOffset
;
final
int
startOffset
;
switch
(
position
.
affinity
)
{
case
TextAffinity
.
upstream
:
startOffset
=
math
.
min
(
position
.
offset
-
1
,
_text
.
length
);
endOffset
=
math
.
min
(
position
.
offset
,
_text
.
length
);
break
;
case
TextAffinity
.
downstream
:
startOffset
=
math
.
min
(
position
.
offset
,
_text
.
length
);
endOffset
=
math
.
min
(
position
.
offset
+
1
,
_text
.
length
);
break
;
}
final
CharacterRange
range
=
CharacterRange
.
at
(
_text
,
startOffset
,
endOffset
);
return
TextPosition
(
offset:
_text
.
length
-
range
.
stringAfterLength
,
affinity:
TextAffinity
.
upstream
,
);
}
}
/// A text boundary that uses words as logical boundaries.
///
/// This class uses [UAX #29](https://unicode.org/reports/tr29/) defined word
/// boundaries to calculate its logical boundaries.
class
WordBoundary
extends
TextBoundary
{
/// Creates a [CharacterBoundary] with the text and layout information.
const
WordBoundary
(
this
.
_textLayout
);
final
TextLayoutMetrics
_textLayout
;
@override
TextPosition
getLeadingTextBoundaryAt
(
TextPosition
position
)
{
return
TextPosition
(
offset:
_textLayout
.
getWordBoundary
(
position
).
start
,
affinity:
TextAffinity
.
downstream
,
// ignore: avoid_redundant_argument_values
);
}
@override
TextPosition
getTrailingTextBoundaryAt
(
TextPosition
position
)
{
return
TextPosition
(
offset:
_textLayout
.
getWordBoundary
(
position
).
end
,
affinity:
TextAffinity
.
upstream
,
);
}
}
/// A text boundary that uses line breaks as logical boundaries.
///
/// The input [TextPosition]s will be interpreted as caret locations if
/// [TextLayoutMetrics.getLineAtOffset] is text-affinity-aware.
class
LineBreak
extends
TextBoundary
{
/// Creates a [CharacterBoundary] with the text and layout information.
const
LineBreak
(
this
.
_textLayout
);
final
TextLayoutMetrics
_textLayout
;
@override
TextPosition
getLeadingTextBoundaryAt
(
TextPosition
position
)
{
return
TextPosition
(
offset:
_textLayout
.
getLineAtOffset
(
position
).
start
,
);
}
@override
TextPosition
getTrailingTextBoundaryAt
(
TextPosition
position
)
{
return
TextPosition
(
offset:
_textLayout
.
getLineAtOffset
(
position
).
end
,
affinity:
TextAffinity
.
upstream
,
);
}
}
/// A text boundary that uses the entire document as logical boundary.
///
/// The document boundary is unique and is a constant function of the input
/// position.
class
DocumentBoundary
extends
TextBoundary
{
/// Creates a [CharacterBoundary] with the text
const
DocumentBoundary
(
this
.
_text
);
final
String
_text
;
@override
TextPosition
getLeadingTextBoundaryAt
(
TextPosition
position
)
=>
const
TextPosition
(
offset:
0
);
@override
TextPosition
getTrailingTextBoundaryAt
(
TextPosition
position
)
{
return
TextPosition
(
offset:
_text
.
length
,
affinity:
TextAffinity
.
upstream
,
);
}
}
packages/flutter/lib/src/widgets/editable_text.dart
View file @
bcc1dc6b
...
@@ -3478,23 +3478,23 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
...
@@ -3478,23 +3478,23 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
// --------------------------- Text Editing Actions ---------------------------
// --------------------------- Text Editing Actions ---------------------------
_
TextBoundary
_characterBoundary
(
DirectionalTextEditingIntent
intent
)
{
TextBoundary
_characterBoundary
(
DirectionalTextEditingIntent
intent
)
{
final
_TextBoundary
atomicTextBoundary
=
widget
.
obscureText
?
_CodeUnitBoundary
(
_value
)
:
_CharacterBoundary
(
_value
);
final
TextBoundary
atomicTextBoundary
=
widget
.
obscureText
?
_CodeUnitBoundary
(
_value
.
text
)
:
CharacterBoundary
(
_value
.
text
);
return
_
CollapsedSelectionBoundary
(
atomicTextBoundary
,
intent
.
forward
);
return
_
PushTextPosition
(
atomicTextBoundary
,
intent
.
forward
);
}
}
_
TextBoundary
_nextWordBoundary
(
DirectionalTextEditingIntent
intent
)
{
TextBoundary
_nextWordBoundary
(
DirectionalTextEditingIntent
intent
)
{
final
_
TextBoundary
atomicTextBoundary
;
final
TextBoundary
atomicTextBoundary
;
final
_
TextBoundary
boundary
;
final
TextBoundary
boundary
;
if
(
widget
.
obscureText
)
{
if
(
widget
.
obscureText
)
{
atomicTextBoundary
=
_CodeUnitBoundary
(
_value
);
atomicTextBoundary
=
_CodeUnitBoundary
(
_value
.
text
);
boundary
=
_DocumentBoundary
(
_value
);
boundary
=
DocumentBoundary
(
_value
.
text
);
}
else
{
}
else
{
final
TextEditingValue
textEditingValue
=
_textEditingValueforTextLayoutMetrics
;
final
TextEditingValue
textEditingValue
=
_textEditingValueforTextLayoutMetrics
;
atomicTextBoundary
=
_CharacterBoundary
(
textEditingValue
);
atomicTextBoundary
=
CharacterBoundary
(
textEditingValue
.
text
);
// This isn't enough. Newline characters.
// This isn't enough. Newline characters.
boundary
=
_ExpandedTextBoundary
(
_WhitespaceBoundary
(
textEditingValue
),
_WordBoundary
(
renderEditable
,
textEditingValu
e
));
boundary
=
_ExpandedTextBoundary
(
_WhitespaceBoundary
(
textEditingValue
.
text
),
WordBoundary
(
renderEditabl
e
));
}
}
final
_MixedBoundary
mixedBoundary
=
intent
.
forward
final
_MixedBoundary
mixedBoundary
=
intent
.
forward
...
@@ -3502,20 +3502,20 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
...
@@ -3502,20 +3502,20 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
:
_MixedBoundary
(
boundary
,
atomicTextBoundary
);
:
_MixedBoundary
(
boundary
,
atomicTextBoundary
);
// Use a _MixedBoundary to make sure we don't leave invalid codepoints in
// Use a _MixedBoundary to make sure we don't leave invalid codepoints in
// the field after deletion.
// the field after deletion.
return
_
CollapsedSelectionBoundary
(
mixedBoundary
,
intent
.
forward
);
return
_
PushTextPosition
(
mixedBoundary
,
intent
.
forward
);
}
}
_
TextBoundary
_linebreak
(
DirectionalTextEditingIntent
intent
)
{
TextBoundary
_linebreak
(
DirectionalTextEditingIntent
intent
)
{
final
_
TextBoundary
atomicTextBoundary
;
final
TextBoundary
atomicTextBoundary
;
final
_
TextBoundary
boundary
;
final
TextBoundary
boundary
;
if
(
widget
.
obscureText
)
{
if
(
widget
.
obscureText
)
{
atomicTextBoundary
=
_CodeUnitBoundary
(
_value
);
atomicTextBoundary
=
_CodeUnitBoundary
(
_value
.
text
);
boundary
=
_DocumentBoundary
(
_value
);
boundary
=
DocumentBoundary
(
_value
.
text
);
}
else
{
}
else
{
final
TextEditingValue
textEditingValue
=
_textEditingValueforTextLayoutMetrics
;
final
TextEditingValue
textEditingValue
=
_textEditingValueforTextLayoutMetrics
;
atomicTextBoundary
=
_CharacterBoundary
(
textEditingValue
);
atomicTextBoundary
=
CharacterBoundary
(
textEditingValue
.
text
);
boundary
=
_LineBreak
(
renderEditable
,
textEditingValu
e
);
boundary
=
LineBreak
(
renderEditabl
e
);
}
}
// The _MixedBoundary is to make sure we don't leave invalid code units in
// The _MixedBoundary is to make sure we don't leave invalid code units in
...
@@ -3524,11 +3524,11 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
...
@@ -3524,11 +3524,11 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
// since the document boundary is unique and the linebreak boundary is
// since the document boundary is unique and the linebreak boundary is
// already caret-location based.
// already caret-location based.
return
intent
.
forward
return
intent
.
forward
?
_MixedBoundary
(
_
CollapsedSelectionBoundary
(
atomicTextBoundary
,
true
),
boundary
)
?
_MixedBoundary
(
_
PushTextPosition
(
atomicTextBoundary
,
true
),
boundary
)
:
_MixedBoundary
(
boundary
,
_
CollapsedSelectionBoundary
(
atomicTextBoundary
,
false
));
:
_MixedBoundary
(
boundary
,
_
PushTextPosition
(
atomicTextBoundary
,
false
));
}
}
_TextBoundary
_documentBoundary
(
DirectionalTextEditingIntent
intent
)
=>
_DocumentBoundary
(
_value
);
TextBoundary
_documentBoundary
(
DirectionalTextEditingIntent
intent
)
=>
DocumentBoundary
(
_value
.
text
);
Action
<
T
>
_makeOverridable
<
T
extends
Intent
>(
Action
<
T
>
defaultAction
)
{
Action
<
T
>
_makeOverridable
<
T
extends
Intent
>(
Action
<
T
>
defaultAction
)
{
return
Action
<
T
>.
overridable
(
context:
context
,
defaultAction:
defaultAction
);
return
Action
<
T
>.
overridable
(
context:
context
,
defaultAction:
defaultAction
);
...
@@ -3615,17 +3615,17 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
...
@@ -3615,17 +3615,17 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
late
final
_UpdateTextSelectionToAdjacentLineAction
<
ExtendSelectionVerticallyToAdjacentLineIntent
>
_adjacentLineAction
=
_UpdateTextSelectionToAdjacentLineAction
<
ExtendSelectionVerticallyToAdjacentLineIntent
>(
this
);
late
final
_UpdateTextSelectionToAdjacentLineAction
<
ExtendSelectionVerticallyToAdjacentLineIntent
>
_adjacentLineAction
=
_UpdateTextSelectionToAdjacentLineAction
<
ExtendSelectionVerticallyToAdjacentLineIntent
>(
this
);
void
_expandSelectionToDocumentBoundary
(
ExpandSelectionToDocumentBoundaryIntent
intent
)
{
void
_expandSelectionToDocumentBoundary
(
ExpandSelectionToDocumentBoundaryIntent
intent
)
{
final
_
TextBoundary
textBoundary
=
_documentBoundary
(
intent
);
final
TextBoundary
textBoundary
=
_documentBoundary
(
intent
);
_expandSelection
(
intent
.
forward
,
textBoundary
,
true
);
_expandSelection
(
intent
.
forward
,
textBoundary
,
true
);
}
}
void
_expandSelectionToLinebreak
(
ExpandSelectionToLineBreakIntent
intent
)
{
void
_expandSelectionToLinebreak
(
ExpandSelectionToLineBreakIntent
intent
)
{
final
_
TextBoundary
textBoundary
=
_linebreak
(
intent
);
final
TextBoundary
textBoundary
=
_linebreak
(
intent
);
_expandSelection
(
intent
.
forward
,
textBoundary
);
_expandSelection
(
intent
.
forward
,
textBoundary
);
}
}
void
_expandSelection
(
bool
forward
,
_
TextBoundary
textBoundary
,
[
bool
extentAtIndex
=
false
])
{
void
_expandSelection
(
bool
forward
,
TextBoundary
textBoundary
,
[
bool
extentAtIndex
=
false
])
{
final
TextSelection
textBoundarySelection
=
textBoundary
.
textEditingV
alue
.
selection
;
final
TextSelection
textBoundarySelection
=
_v
alue
.
selection
;
if
(!
textBoundarySelection
.
isValid
)
{
if
(!
textBoundarySelection
.
isValid
)
{
return
;
return
;
}
}
...
@@ -4220,214 +4220,120 @@ class _ScribblePlaceholder extends WidgetSpan {
...
@@ -4220,214 +4220,120 @@ class _ScribblePlaceholder extends WidgetSpan {
}
}
}
}
/// An interface for retrieving the logical text boundary (left-closed-right-open)
/// A text boundary that uses code units as logical boundaries.
/// at a given location in a document.
///
///
/// Depending on the implementation of the [_TextBoundary], the input
/// This text boundary treats every character in input string as an utf-16 code
/// [TextPosition] can either point to a code unit, or a position between 2 code
/// unit. This can be useful when handling text without any grapheme cluster,
/// units (which can be visually represented by the caret if the selection were
/// e.g. the obscure string in [EditableText]. If you are handling text that may
/// to collapse to that position).
/// include grapheme clusters, consider using [CharacterBoundary].
///
class
_CodeUnitBoundary
extends
TextBoundary
{
/// For example, [_LineBreak] interprets the input [TextPosition] as a caret
const
_CodeUnitBoundary
(
this
.
_text
);
/// location, since in Flutter the caret is generally painted between the
/// character the [TextPosition] points to and its previous character, and
/// [_LineBreak] cares about the affinity of the input [TextPosition]. Most
/// other text boundaries however, interpret the input [TextPosition] as the
/// location of a code unit in the document, since it's easier to reason about
/// the text boundary given a code unit in the text.
///
/// To convert a "code-unit-based" [_TextBoundary] to "caret-location-based",
/// use the [_CollapsedSelectionBoundary] combinator.
abstract
class
_TextBoundary
{
const
_TextBoundary
();
TextEditingValue
get
textEditingValue
;
/// Returns the leading text boundary at the given location, inclusive.
TextPosition
getLeadingTextBoundaryAt
(
TextPosition
position
);
/// Returns the trailing text boundary at the given location, exclusive.
TextPosition
getTrailingTextBoundaryAt
(
TextPosition
position
);
TextRange
getTextBoundaryAt
(
TextPosition
position
)
{
return
TextRange
(
start:
getLeadingTextBoundaryAt
(
position
).
offset
,
end:
getTrailingTextBoundaryAt
(
position
).
offset
,
);
}
}
// ----------------------------- Text Boundaries -----------------------------
class
_CodeUnitBoundary
extends
_TextBoundary
{
const
_CodeUnitBoundary
(
this
.
textEditingValue
);
@override
final
String
_text
;
final
TextEditingValue
textEditingValue
;
@override
TextPosition
getLeadingTextBoundaryAt
(
TextPosition
position
)
=>
TextPosition
(
offset:
position
.
offset
);
@override
TextPosition
getTrailingTextBoundaryAt
(
TextPosition
position
)
=>
TextPosition
(
offset:
math
.
min
(
position
.
offset
+
1
,
textEditingValue
.
text
.
length
));
}
// The word modifier generally removes the word boundaries around white spaces
// (and newlines), IOW white spaces and some other punctuations are considered
// a part of the next word in the search direction.
class
_WhitespaceBoundary
extends
_TextBoundary
{
const
_WhitespaceBoundary
(
this
.
textEditingValue
);
@override
final
TextEditingValue
textEditingValue
;
@override
@override
TextPosition
getLeadingTextBoundaryAt
(
TextPosition
position
)
{
TextPosition
getLeadingTextBoundaryAt
(
TextPosition
position
)
{
for
(
int
index
=
position
.
offset
;
index
>=
0
;
index
-=
1
)
{
if
(
position
.
offset
<=
0
)
{
if
(!
TextLayoutMetrics
.
isWhitespace
(
textEditingValue
.
text
.
codeUnitAt
(
index
)))
{
return
const
TextPosition
(
offset:
0
);
return
TextPosition
(
offset:
index
);
}
}
}
return
const
TextPosition
(
offset:
0
);
if
(
position
.
offset
>
_text
.
length
||
}
(
position
.
offset
==
_text
.
length
&&
position
.
affinity
==
TextAffinity
.
downstream
))
{
return
TextPosition
(
offset:
_text
.
length
,
affinity:
TextAffinity
.
upstream
);
@override
}
TextPosition
getTrailingTextBoundaryAt
(
TextPosition
position
)
{
switch
(
position
.
affinity
)
{
for
(
int
index
=
position
.
offset
;
index
<
textEditingValue
.
text
.
length
;
index
+=
1
)
{
case
TextAffinity
.
upstream
:
if
(!
TextLayoutMetrics
.
isWhitespace
(
textEditingValue
.
text
.
codeUnitAt
(
index
)))
{
return
TextPosition
(
offset:
math
.
min
(
position
.
offset
-
1
,
_text
.
length
));
return
TextPosition
(
offset:
index
+
1
);
case
TextAffinity
.
downstream
:
}
return
TextPosition
(
offset:
math
.
min
(
position
.
offset
,
_text
.
length
));
}
}
return
TextPosition
(
offset:
textEditingValue
.
text
.
length
);
}
}
// Most apps delete the entire grapheme when the backspace key is pressed.
// Also always put the new caret location to character boundaries to avoid
// sending malformed UTF-16 code units to the paragraph builder.
class
_CharacterBoundary
extends
_TextBoundary
{
const
_CharacterBoundary
(
this
.
textEditingValue
);
@override
final
TextEditingValue
textEditingValue
;
@override
TextPosition
getLeadingTextBoundaryAt
(
TextPosition
position
)
{
final
int
endOffset
=
math
.
min
(
position
.
offset
+
1
,
textEditingValue
.
text
.
length
);
return
TextPosition
(
offset:
CharacterRange
.
at
(
textEditingValue
.
text
,
position
.
offset
,
endOffset
).
stringBeforeLength
,
);
}
}
@override
@override
TextPosition
getTrailingTextBoundaryAt
(
TextPosition
position
)
{
TextPosition
getTrailingTextBoundaryAt
(
TextPosition
position
)
{
final
int
endOffset
=
math
.
min
(
position
.
offset
+
1
,
textEditingValue
.
text
.
length
);
if
(
position
.
offset
<
0
||
final
CharacterRange
range
=
CharacterRange
.
at
(
textEditingValue
.
text
,
position
.
offset
,
endOffset
);
(
position
.
offset
==
0
&&
position
.
affinity
==
TextAffinity
.
upstream
))
{
return
TextPosition
(
return
const
TextPosition
(
offset:
0
);
offset:
textEditingValue
.
text
.
length
-
range
.
stringAfterLength
,
}
);
if
(
position
.
offset
>=
_text
.
length
)
{
}
return
TextPosition
(
offset:
_text
.
length
,
affinity:
TextAffinity
.
upstream
);
}
@override
switch
(
position
.
affinity
)
{
TextRange
getTextBoundaryAt
(
TextPosition
position
)
{
case
TextAffinity
.
upstream
:
final
int
endOffset
=
math
.
min
(
position
.
offset
+
1
,
textEditingValue
.
text
.
length
);
return
TextPosition
(
offset:
math
.
min
(
position
.
offset
,
_text
.
length
),
affinity:
TextAffinity
.
upstream
);
final
CharacterRange
range
=
CharacterRange
.
at
(
textEditingValue
.
text
,
position
.
offset
,
endOffset
);
case
TextAffinity
.
downstream
:
return
TextRange
(
return
TextPosition
(
offset:
math
.
min
(
position
.
offset
+
1
,
_text
.
length
),
affinity:
TextAffinity
.
upstream
);
start:
range
.
stringBeforeLength
,
}
end:
textEditingValue
.
text
.
length
-
range
.
stringAfterLength
,
);
}
}
}
}
// [UAX #29](https://unicode.org/reports/tr29/) defined word boundaries.
// ------------------------ Text Boundary Combinators ------------------------
class
_WordBoundary
extends
_TextBoundary
{
const
_WordBoundary
(
this
.
textLayout
,
this
.
textEditingValue
);
final
TextLayoutMetrics
textLayout
;
/// A text boundary that use the first non-whitespace character as the logical
/// boundary.
///
/// This text boundary uses [TextLayoutMetrics.isWhitespace] to identify white
/// spaces, this include newline characters from ASCII and separators from the
/// [unicode separator category](https://www.compart.com/en/unicode/category/Zs).
class
_WhitespaceBoundary
extends
TextBoundary
{
/// Creates a [_WhitespaceBoundary] with the text.
const
_WhitespaceBoundary
(
this
.
_text
);
@override
final
String
_text
;
final
TextEditingValue
textEditingValue
;
@override
@override
TextPosition
getLeadingTextBoundaryAt
(
TextPosition
position
)
{
TextPosition
getLeadingTextBoundaryAt
(
TextPosition
position
)
{
return
TextPosition
(
// Handles outside of right bound.
offset:
textLayout
.
getWordBoundary
(
position
).
start
,
if
(
position
.
offset
>
_text
.
length
||
(
position
.
offset
==
_text
.
length
&&
position
.
affinity
==
TextAffinity
.
downstream
))
{
// Word boundary seems to always report downstream on many platforms.
position
=
TextPosition
(
offset:
_text
.
length
,
affinity:
TextAffinity
.
upstream
);
affinity:
TextAffinity
.
downstream
,
// ignore: avoid_redundant_argument_values
}
);
// Handles outside of left bound.
}
if
(
position
.
offset
<=
0
)
{
@override
return
const
TextPosition
(
offset:
0
);
TextPosition
getTrailingTextBoundaryAt
(
TextPosition
position
)
{
}
return
TextPosition
(
int
index
=
position
.
offset
;
offset:
textLayout
.
getWordBoundary
(
position
).
end
,
if
(!
TextLayoutMetrics
.
isWhitespace
(
_text
.
codeUnitAt
(
index
))
&&
position
.
affinity
==
TextAffinity
.
downstream
)
{
// Word boundary seems to always report downstream on many platforms.
return
position
;
affinity:
TextAffinity
.
downstream
,
// ignore: avoid_redundant_argument_values
}
);
}
}
// The linebreaks of the current text layout. The input [TextPosition]s are
// interpreted as caret locations because [TextPainter.getLineAtOffset] is
// text-affinity-aware.
class
_LineBreak
extends
_TextBoundary
{
const
_LineBreak
(
this
.
textLayout
,
this
.
textEditingValue
,
);
final
TextLayoutMetrics
textLayout
;
@override
final
TextEditingValue
textEditingValue
;
@override
for
(
index
-=
1
;
index
>=
0
;
index
-=
1
)
{
TextPosition
getLeadingTextBoundaryAt
(
TextPosition
position
)
{
if
(!
TextLayoutMetrics
.
isWhitespace
(
_text
.
codeUnitAt
(
index
)))
{
return
TextPosition
(
return
TextPosition
(
offset:
index
+
1
,
affinity:
TextAffinity
.
upstream
);
offset:
textLayout
.
getLineAtOffset
(
position
).
start
,
}
);
}
return
const
TextPosition
(
offset:
0
);
}
}
@override
@override
TextPosition
getTrailingTextBoundaryAt
(
TextPosition
position
)
{
TextPosition
getTrailingTextBoundaryAt
(
TextPosition
position
)
{
return
TextPosition
(
// Handles outside of right bound.
offset:
textLayout
.
getLineAtOffset
(
position
).
end
,
if
(
position
.
offset
>=
_text
.
length
)
{
affinity:
TextAffinity
.
upstream
,
return
TextPosition
(
offset:
_text
.
length
,
affinity:
TextAffinity
.
upstream
);
);
}
}
// Handles outside of left bound.
}
if
(
position
.
offset
<
0
||
(
position
.
offset
==
0
&&
position
.
affinity
==
TextAffinity
.
upstream
))
{
position
=
const
TextPosition
(
offset:
0
);
// The document boundary is unique and is a constant function of the input
}
// position.
class
_DocumentBoundary
extends
_TextBoundary
{
const
_DocumentBoundary
(
this
.
textEditingValue
);
@override
int
index
=
position
.
offset
;
final
TextEditingValue
textEditingValue
;
if
(!
TextLayoutMetrics
.
isWhitespace
(
_text
.
codeUnitAt
(
index
))
&&
position
.
affinity
==
TextAffinity
.
downstream
)
{
return
position
;
}
@override
for
(
index
+=
1
;
index
<
_text
.
length
;
index
+=
1
)
{
TextPosition
getLeadingTextBoundaryAt
(
TextPosition
position
)
=>
const
TextPosition
(
offset:
0
);
if
(!
TextLayoutMetrics
.
isWhitespace
(
_text
.
codeUnitAt
(
index
)))
{
@override
return
TextPosition
(
offset:
index
);
TextPosition
getTrailingTextBoundaryAt
(
TextPosition
position
)
{
}
return
TextPosition
(
}
offset:
textEditingValue
.
text
.
length
,
return
TextPosition
(
offset:
_text
.
length
,
affinity:
TextAffinity
.
upstream
);
affinity:
TextAffinity
.
upstream
,
);
}
}
}
}
// ------------------------ Text Boundary Combinators ------------------------
// Expands the innerTextBoundary with outerTextBoundary.
// Expands the innerTextBoundary with outerTextBoundary.
class
_ExpandedTextBoundary
extends
_
TextBoundary
{
class
_ExpandedTextBoundary
extends
TextBoundary
{
_ExpandedTextBoundary
(
this
.
innerTextBoundary
,
this
.
outerTextBoundary
);
_ExpandedTextBoundary
(
this
.
innerTextBoundary
,
this
.
outerTextBoundary
);
final
_TextBoundary
innerTextBoundary
;
final
TextBoundary
innerTextBoundary
;
final
_TextBoundary
outerTextBoundary
;
final
TextBoundary
outerTextBoundary
;
@override
TextEditingValue
get
textEditingValue
{
assert
(
innerTextBoundary
.
textEditingValue
==
outerTextBoundary
.
textEditingValue
);
return
innerTextBoundary
.
textEditingValue
;
}
@override
@override
TextPosition
getLeadingTextBoundaryAt
(
TextPosition
position
)
{
TextPosition
getLeadingTextBoundaryAt
(
TextPosition
position
)
{
...
@@ -4444,49 +4350,70 @@ class _ExpandedTextBoundary extends _TextBoundary {
...
@@ -4444,49 +4350,70 @@ class _ExpandedTextBoundary extends _TextBoundary {
}
}
}
}
// Force the innerTextBoundary to interpret the input [TextPosition]s as caret
/// A proxy text boundary that will push input text position forward or backward
// locations instead of code unit positions.
/// one affinity unit before sending it to the [innerTextBoundary].
//
///
// The innerTextBoundary must be a [_TextBoundary] that interprets the input
/// If the [isForward] is true, this proxy text boundary push the position
// [TextPosition]s as code unit positions.
/// forward; otherwise, backward.
class
_CollapsedSelectionBoundary
extends
_TextBoundary
{
///
_CollapsedSelectionBoundary
(
this
.
innerTextBoundary
,
this
.
isForward
);
/// To push a text position forward one affinity unit, this proxy converts
/// affinity to downstream if it is upstream; otherwise it increase the offset
/// by one with its affinity sets to upstream. For example,
/// `TextPosition(1, upstream)` becomes `TextPosition(1, downstream)`,
/// `TextPosition(4, downstream)` becomes `TextPosition(5, upstream)`.
///
/// This class is used to kick-start the text position to find the next boundary
/// determined by [innerTextBoundary] so that it won't be trapped if the input
/// text position is right at the edge.
class
_PushTextPosition
extends
TextBoundary
{
_PushTextPosition
(
this
.
innerTextBoundary
,
this
.
isForward
);
final
_
TextBoundary
innerTextBoundary
;
final
TextBoundary
innerTextBoundary
;
final
bool
isForward
;
final
bool
isForward
;
@override
TextPosition
_calculateTargetPosition
(
TextPosition
position
)
{
TextEditingValue
get
textEditingValue
=>
innerTextBoundary
.
textEditingValue
;
if
(
isForward
)
{
switch
(
position
.
affinity
)
{
case
TextAffinity
.
upstream
:
return
TextPosition
(
offset:
position
.
offset
);
case
TextAffinity
.
downstream
:
return
position
=
TextPosition
(
offset:
position
.
offset
+
1
,
affinity:
TextAffinity
.
upstream
,
);
}
}
else
{
switch
(
position
.
affinity
)
{
case
TextAffinity
.
upstream
:
return
position
=
TextPosition
(
offset:
position
.
offset
-
1
);
case
TextAffinity
.
downstream
:
return
TextPosition
(
offset:
position
.
offset
,
affinity:
TextAffinity
.
upstream
,
);
}
}
}
@override
@override
TextPosition
getLeadingTextBoundaryAt
(
TextPosition
position
)
{
TextPosition
getLeadingTextBoundaryAt
(
TextPosition
position
)
{
return
isForward
return
innerTextBoundary
.
getLeadingTextBoundaryAt
(
_calculateTargetPosition
(
position
));
?
innerTextBoundary
.
getLeadingTextBoundaryAt
(
position
)
:
position
.
offset
<=
0
?
const
TextPosition
(
offset:
0
)
:
innerTextBoundary
.
getLeadingTextBoundaryAt
(
TextPosition
(
offset:
position
.
offset
-
1
));
}
}
@override
@override
TextPosition
getTrailingTextBoundaryAt
(
TextPosition
position
)
{
TextPosition
getTrailingTextBoundaryAt
(
TextPosition
position
)
{
return
isForward
return
innerTextBoundary
.
getTrailingTextBoundaryAt
(
_calculateTargetPosition
(
position
));
?
innerTextBoundary
.
getTrailingTextBoundaryAt
(
position
)
:
position
.
offset
<=
0
?
const
TextPosition
(
offset:
0
)
:
innerTextBoundary
.
getTrailingTextBoundaryAt
(
TextPosition
(
offset:
position
.
offset
-
1
));
}
}
}
}
// A _TextBoundary that creates a [TextRange] where its start is from the
// A _TextBoundary that creates a [TextRange] where its start is from the
// specified leading text boundary and its end is from the specified trailing
// specified leading text boundary and its end is from the specified trailing
// text boundary.
// text boundary.
class
_MixedBoundary
extends
_
TextBoundary
{
class
_MixedBoundary
extends
TextBoundary
{
_MixedBoundary
(
this
.
leadingTextBoundary
,
this
.
trailingTextBoundary
);
_MixedBoundary
(
this
.
leadingTextBoundary
,
this
.
trailingTextBoundary
);
final
_TextBoundary
leadingTextBoundary
;
final
TextBoundary
leadingTextBoundary
;
final
_TextBoundary
trailingTextBoundary
;
final
TextBoundary
trailingTextBoundary
;
@override
TextEditingValue
get
textEditingValue
{
assert
(
leadingTextBoundary
.
textEditingValue
==
trailingTextBoundary
.
textEditingValue
);
return
leadingTextBoundary
.
textEditingValue
;
}
@override
@override
TextPosition
getLeadingTextBoundaryAt
(
TextPosition
position
)
=>
leadingTextBoundary
.
getLeadingTextBoundaryAt
(
position
);
TextPosition
getLeadingTextBoundaryAt
(
TextPosition
position
)
=>
leadingTextBoundary
.
getLeadingTextBoundaryAt
(
position
);
...
@@ -4500,15 +4427,15 @@ class _DeleteTextAction<T extends DirectionalTextEditingIntent> extends ContextA
...
@@ -4500,15 +4427,15 @@ class _DeleteTextAction<T extends DirectionalTextEditingIntent> extends ContextA
_DeleteTextAction
(
this
.
state
,
this
.
getTextBoundariesForIntent
);
_DeleteTextAction
(
this
.
state
,
this
.
getTextBoundariesForIntent
);
final
EditableTextState
state
;
final
EditableTextState
state
;
final
_
TextBoundary
Function
(
T
intent
)
getTextBoundariesForIntent
;
final
TextBoundary
Function
(
T
intent
)
getTextBoundariesForIntent
;
TextRange
_expandNonCollapsedRange
(
TextEditingValue
value
)
{
TextRange
_expandNonCollapsedRange
(
TextEditingValue
value
)
{
final
TextRange
selection
=
value
.
selection
;
final
TextRange
selection
=
value
.
selection
;
assert
(
selection
.
isValid
);
assert
(
selection
.
isValid
);
assert
(!
selection
.
isCollapsed
);
assert
(!
selection
.
isCollapsed
);
final
_
TextBoundary
atomicBoundary
=
state
.
widget
.
obscureText
final
TextBoundary
atomicBoundary
=
state
.
widget
.
obscureText
?
_CodeUnitBoundary
(
value
)
?
_CodeUnitBoundary
(
value
.
text
)
:
_CharacterBoundary
(
value
);
:
CharacterBoundary
(
value
.
text
);
return
TextRange
(
return
TextRange
(
start:
atomicBoundary
.
getLeadingTextBoundaryAt
(
TextPosition
(
offset:
selection
.
start
)).
offset
,
start:
atomicBoundary
.
getLeadingTextBoundaryAt
(
TextPosition
(
offset:
selection
.
start
)).
offset
,
...
@@ -4528,23 +4455,23 @@ class _DeleteTextAction<T extends DirectionalTextEditingIntent> extends ContextA
...
@@ -4528,23 +4455,23 @@ class _DeleteTextAction<T extends DirectionalTextEditingIntent> extends ContextA
);
);
}
}
final
_
TextBoundary
textBoundary
=
getTextBoundariesForIntent
(
intent
);
final
TextBoundary
textBoundary
=
getTextBoundariesForIntent
(
intent
);
if
(!
textBoundary
.
textEditingV
alue
.
selection
.
isValid
)
{
if
(!
state
.
_v
alue
.
selection
.
isValid
)
{
return
null
;
return
null
;
}
}
if
(!
textBoundary
.
textEditingV
alue
.
selection
.
isCollapsed
)
{
if
(!
state
.
_v
alue
.
selection
.
isCollapsed
)
{
return
Actions
.
invoke
(
return
Actions
.
invoke
(
context
!,
context
!,
ReplaceTextIntent
(
state
.
_value
,
''
,
_expandNonCollapsedRange
(
textBoundary
.
textEditingV
alue
),
SelectionChangedCause
.
keyboard
),
ReplaceTextIntent
(
state
.
_value
,
''
,
_expandNonCollapsedRange
(
state
.
_v
alue
),
SelectionChangedCause
.
keyboard
),
);
);
}
}
return
Actions
.
invoke
(
return
Actions
.
invoke
(
context
!,
context
!,
ReplaceTextIntent
(
ReplaceTextIntent
(
textBoundary
.
textEditingV
alue
,
state
.
_v
alue
,
''
,
''
,
textBoundary
.
getTextBoundaryAt
(
textBoundary
.
textEditingV
alue
.
selection
.
base
),
textBoundary
.
getTextBoundaryAt
(
state
.
_v
alue
.
selection
.
base
),
SelectionChangedCause
.
keyboard
,
SelectionChangedCause
.
keyboard
,
),
),
);
);
...
@@ -4563,7 +4490,7 @@ class _UpdateTextSelectionAction<T extends DirectionalCaretMovementIntent> exten
...
@@ -4563,7 +4490,7 @@ class _UpdateTextSelectionAction<T extends DirectionalCaretMovementIntent> exten
final
EditableTextState
state
;
final
EditableTextState
state
;
final
bool
ignoreNonCollapsedSelection
;
final
bool
ignoreNonCollapsedSelection
;
final
_
TextBoundary
Function
(
T
intent
)
getTextBoundariesForIntent
;
final
TextBoundary
Function
(
T
intent
)
getTextBoundariesForIntent
;
static
const
int
NEWLINE_CODE_UNIT
=
10
;
static
const
int
NEWLINE_CODE_UNIT
=
10
;
...
@@ -4578,7 +4505,7 @@ class _UpdateTextSelectionAction<T extends DirectionalCaretMovementIntent> exten
...
@@ -4578,7 +4505,7 @@ class _UpdateTextSelectionAction<T extends DirectionalCaretMovementIntent> exten
&&
state
.
textEditingValue
.
text
.
codeUnitAt
(
position
.
offset
)
!=
NEWLINE_CODE_UNIT
;
&&
state
.
textEditingValue
.
text
.
codeUnitAt
(
position
.
offset
)
!=
NEWLINE_CODE_UNIT
;
}
}
// Returns true if
f
the given position at a wordwrap boundary in the
// Returns true if the given position at a wordwrap boundary in the
// downstream position.
// downstream position.
bool
_isAtWordwrapDownstream
(
TextPosition
position
)
{
bool
_isAtWordwrapDownstream
(
TextPosition
position
)
{
final
TextPosition
start
=
TextPosition
(
final
TextPosition
start
=
TextPosition
(
...
@@ -4611,29 +4538,9 @@ class _UpdateTextSelectionAction<T extends DirectionalCaretMovementIntent> exten
...
@@ -4611,29 +4538,9 @@ class _UpdateTextSelectionAction<T extends DirectionalCaretMovementIntent> exten
);
);
}
}
final
_TextBoundary
textBoundary
=
getTextBoundariesForIntent
(
intent
);
final
TextBoundary
textBoundary
=
getTextBoundariesForIntent
(
intent
);
// "textBoundary's selection is only updated after rebuild; if the text
// is the same, use the selection from state, which is more recent.
// This is necessary on macOS where alt+up sends the moveBackward:
// and moveToBeginningOfParagraph: selectors at the same time.
final
TextSelection
textBoundarySelection
=
textBoundary
.
textEditingValue
.
text
==
state
.
_value
.
text
?
state
.
_value
.
selection
:
textBoundary
.
textEditingValue
.
selection
;
if
(!
textBoundarySelection
.
isValid
)
{
return
null
;
}
if
(!
textBoundarySelection
.
isCollapsed
&&
!
ignoreNonCollapsedSelection
&&
collapseSelection
)
{
return
Actions
.
invoke
(
context
!,
UpdateSelectionIntent
(
state
.
_value
,
collapse
(
textBoundarySelection
),
SelectionChangedCause
.
keyboard
),
);
}
TextPosition
extent
=
textBoundarySelection
.
extent
;
TextPosition
extent
=
selection
.
extent
;
// If continuesAtWrap is true extent and is at the relevant wordwrap, then
// If continuesAtWrap is true extent and is at the relevant wordwrap, then
// move it just to the other side of the wordwrap.
// move it just to the other side of the wordwrap.
if
(
intent
.
continuesAtWrap
)
{
if
(
intent
.
continuesAtWrap
)
{
...
@@ -4652,10 +4559,9 @@ class _UpdateTextSelectionAction<T extends DirectionalCaretMovementIntent> exten
...
@@ -4652,10 +4559,9 @@ class _UpdateTextSelectionAction<T extends DirectionalCaretMovementIntent> exten
final
TextPosition
newExtent
=
intent
.
forward
final
TextPosition
newExtent
=
intent
.
forward
?
textBoundary
.
getTrailingTextBoundaryAt
(
extent
)
?
textBoundary
.
getTrailingTextBoundaryAt
(
extent
)
:
textBoundary
.
getLeadingTextBoundaryAt
(
extent
);
:
textBoundary
.
getLeadingTextBoundaryAt
(
extent
);
final
TextSelection
newSelection
=
collapseSelection
final
TextSelection
newSelection
=
collapseSelection
?
TextSelection
.
fromPosition
(
newExtent
)
?
TextSelection
.
fromPosition
(
newExtent
)
:
textBoundaryS
election
.
extendTo
(
newExtent
);
:
s
election
.
extendTo
(
newExtent
);
// If collapseAtReversal is true and would have an effect, collapse it.
// If collapseAtReversal is true and would have an effect, collapse it.
if
(!
selection
.
isCollapsed
&&
intent
.
collapseAtReversal
if
(!
selection
.
isCollapsed
&&
intent
.
collapseAtReversal
...
@@ -4673,7 +4579,7 @@ class _UpdateTextSelectionAction<T extends DirectionalCaretMovementIntent> exten
...
@@ -4673,7 +4579,7 @@ class _UpdateTextSelectionAction<T extends DirectionalCaretMovementIntent> exten
return
Actions
.
invoke
(
return
Actions
.
invoke
(
context
!,
context
!,
UpdateSelectionIntent
(
textBoundary
.
textEditingV
alue
,
newSelection
,
SelectionChangedCause
.
keyboard
),
UpdateSelectionIntent
(
state
.
_v
alue
,
newSelection
,
SelectionChangedCause
.
keyboard
),
);
);
}
}
...
@@ -4685,15 +4591,15 @@ class _ExtendSelectionOrCaretPositionAction extends ContextAction<ExtendSelectio
...
@@ -4685,15 +4591,15 @@ class _ExtendSelectionOrCaretPositionAction extends ContextAction<ExtendSelectio
_ExtendSelectionOrCaretPositionAction
(
this
.
state
,
this
.
getTextBoundariesForIntent
);
_ExtendSelectionOrCaretPositionAction
(
this
.
state
,
this
.
getTextBoundariesForIntent
);
final
EditableTextState
state
;
final
EditableTextState
state
;
final
_
TextBoundary
Function
(
ExtendSelectionToNextWordBoundaryOrCaretLocationIntent
intent
)
getTextBoundariesForIntent
;
final
TextBoundary
Function
(
ExtendSelectionToNextWordBoundaryOrCaretLocationIntent
intent
)
getTextBoundariesForIntent
;
@override
@override
Object
?
invoke
(
ExtendSelectionToNextWordBoundaryOrCaretLocationIntent
intent
,
[
BuildContext
?
context
])
{
Object
?
invoke
(
ExtendSelectionToNextWordBoundaryOrCaretLocationIntent
intent
,
[
BuildContext
?
context
])
{
final
TextSelection
selection
=
state
.
_value
.
selection
;
final
TextSelection
selection
=
state
.
_value
.
selection
;
assert
(
selection
.
isValid
);
assert
(
selection
.
isValid
);
final
_
TextBoundary
textBoundary
=
getTextBoundariesForIntent
(
intent
);
final
TextBoundary
textBoundary
=
getTextBoundariesForIntent
(
intent
);
final
TextSelection
textBoundarySelection
=
textBoundary
.
textEditingV
alue
.
selection
;
final
TextSelection
textBoundarySelection
=
state
.
_v
alue
.
selection
;
if
(!
textBoundarySelection
.
isValid
)
{
if
(!
textBoundarySelection
.
isValid
)
{
return
null
;
return
null
;
}
}
...
@@ -4712,7 +4618,7 @@ class _ExtendSelectionOrCaretPositionAction extends ContextAction<ExtendSelectio
...
@@ -4712,7 +4618,7 @@ class _ExtendSelectionOrCaretPositionAction extends ContextAction<ExtendSelectio
return
Actions
.
invoke
(
return
Actions
.
invoke
(
context
!,
context
!,
UpdateSelectionIntent
(
textBoundary
.
textEditingV
alue
,
newSelection
,
SelectionChangedCause
.
keyboard
),
UpdateSelectionIntent
(
state
.
_v
alue
,
newSelection
,
SelectionChangedCause
.
keyboard
),
);
);
}
}
...
...
packages/flutter/test/cupertino/text_field_test.dart
View file @
bcc1dc6b
...
@@ -199,7 +199,8 @@ void main() {
...
@@ -199,7 +199,8 @@ void main() {
return
endpoints
[
0
].
point
;
return
endpoints
[
0
].
point
;
}
}
Offset
textOffsetToPosition
(
WidgetTester
tester
,
int
offset
)
=>
textOffsetToBottomLeftPosition
(
tester
,
offset
)
+
const
Offset
(
0
,
-
2
);
// Web has a less threshold for downstream/upstream text position.
Offset
textOffsetToPosition
(
WidgetTester
tester
,
int
offset
)
=>
textOffsetToBottomLeftPosition
(
tester
,
offset
)
+
const
Offset
(
kIsWeb
?
1
:
0
,
-
2
);
setUp
(()
async
{
setUp
(()
async
{
EditableText
.
debugDeterministicCursor
=
false
;
EditableText
.
debugDeterministicCursor
=
false
;
...
@@ -2087,6 +2088,7 @@ void main() {
...
@@ -2087,6 +2088,7 @@ void main() {
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
await
tester
.
pump
(
const
Duration
(
milliseconds:
50
));
await
tester
.
tapAt
(
textOffsetToPosition
(
tester
,
5
));
await
tester
.
tapAt
(
textOffsetToPosition
(
tester
,
5
));
await
tester
.
pumpAndSettle
();
await
tester
.
pumpAndSettle
();
expect
(
controller
.
value
.
selection
,
isNotNull
);
expect
(
controller
.
value
.
selection
,
isNotNull
);
expect
(
controller
.
value
.
selection
.
baseOffset
,
5
);
expect
(
controller
.
value
.
selection
.
baseOffset
,
5
);
expect
(
controller
.
value
.
selection
.
extentOffset
,
6
);
expect
(
controller
.
value
.
selection
.
extentOffset
,
6
);
...
@@ -3053,7 +3055,7 @@ void main() {
...
@@ -3053,7 +3055,7 @@ void main() {
expect
(
controller
.
selection
.
extentOffset
,
8
);
expect
(
controller
.
selection
.
extentOffset
,
8
);
// Tiny movement shouldn't cause text selection to change.
// Tiny movement shouldn't cause text selection to change.
await
gesture
.
moveTo
(
gPos
+
const
Offset
(
4
.0
,
0.0
));
await
gesture
.
moveTo
(
gPos
+
const
Offset
(
2
.0
,
0.0
));
await
tester
.
pumpAndSettle
();
await
tester
.
pumpAndSettle
();
expect
(
selectionChangedCount
,
0
);
expect
(
selectionChangedCount
,
0
);
...
...
packages/flutter/test/material/text_field_test.dart
View file @
bcc1dc6b
...
@@ -2172,7 +2172,7 @@ void main() {
...
@@ -2172,7 +2172,7 @@ void main() {
expect
(
controller
.
selection
.
extentOffset
,
8
);
expect
(
controller
.
selection
.
extentOffset
,
8
);
// Tiny movement shouldn't cause text selection to change.
// Tiny movement shouldn't cause text selection to change.
await
gesture
.
moveTo
(
gPos
+
const
Offset
(
4
.0
,
0.0
));
await
gesture
.
moveTo
(
gPos
+
const
Offset
(
2
.0
,
0.0
));
await
tester
.
pumpAndSettle
();
await
tester
.
pumpAndSettle
();
expect
(
selectionChangedCount
,
0
);
expect
(
selectionChangedCount
,
0
);
...
@@ -3372,10 +3372,7 @@ void main() {
...
@@ -3372,10 +3372,7 @@ void main() {
final
Offset
secondPos
=
textOffsetToPosition
(
tester
,
testValue
.
indexOf
(
'Second'
));
final
Offset
secondPos
=
textOffsetToPosition
(
tester
,
testValue
.
indexOf
(
'Second'
));
final
Offset
thirdPos
=
textOffsetToPosition
(
tester
,
testValue
.
indexOf
(
'Third'
));
final
Offset
thirdPos
=
textOffsetToPosition
(
tester
,
testValue
.
indexOf
(
'Third'
));
final
Offset
middleStringPos
=
textOffsetToPosition
(
tester
,
testValue
.
indexOf
(
'irst'
));
final
Offset
middleStringPos
=
textOffsetToPosition
(
tester
,
testValue
.
indexOf
(
'irst'
));
expect
(
firstPos
.
dx
,
0
);
expect
(
firstPos
.
dx
,
lessThan
(
middleStringPos
.
dx
));
expect
(
secondPos
.
dx
,
0
);
expect
(
thirdPos
.
dx
,
0
);
expect
(
middleStringPos
.
dx
,
34
);
expect
(
firstPos
.
dx
,
secondPos
.
dx
);
expect
(
firstPos
.
dx
,
secondPos
.
dx
);
expect
(
firstPos
.
dx
,
thirdPos
.
dx
);
expect
(
firstPos
.
dx
,
thirdPos
.
dx
);
expect
(
firstPos
.
dy
,
lessThan
(
secondPos
.
dy
));
expect
(
firstPos
.
dy
,
lessThan
(
secondPos
.
dy
));
...
@@ -3457,8 +3454,6 @@ void main() {
...
@@ -3457,8 +3454,6 @@ void main() {
// Check that the last line of text is not displayed.
// Check that the last line of text is not displayed.
final
Offset
firstPos
=
textOffsetToPosition
(
tester
,
kMoreThanFourLines
.
indexOf
(
'First'
));
final
Offset
firstPos
=
textOffsetToPosition
(
tester
,
kMoreThanFourLines
.
indexOf
(
'First'
));
final
Offset
fourthPos
=
textOffsetToPosition
(
tester
,
kMoreThanFourLines
.
indexOf
(
'Fourth'
));
final
Offset
fourthPos
=
textOffsetToPosition
(
tester
,
kMoreThanFourLines
.
indexOf
(
'Fourth'
));
expect
(
firstPos
.
dx
,
0
);
expect
(
fourthPos
.
dx
,
0
);
expect
(
firstPos
.
dx
,
fourthPos
.
dx
);
expect
(
firstPos
.
dx
,
fourthPos
.
dx
);
expect
(
firstPos
.
dy
,
lessThan
(
fourthPos
.
dy
));
expect
(
firstPos
.
dy
,
lessThan
(
fourthPos
.
dy
));
expect
(
inputBox
.
hitTest
(
BoxHitTestResult
(),
position:
inputBox
.
globalToLocal
(
firstPos
)),
isTrue
);
expect
(
inputBox
.
hitTest
(
BoxHitTestResult
(),
position:
inputBox
.
globalToLocal
(
firstPos
)),
isTrue
);
...
@@ -8397,10 +8392,10 @@ void main() {
...
@@ -8397,10 +8392,10 @@ void main() {
),
),
),
),
);
);
final
Size
screenSize
=
MediaQuery
.
of
(
tester
.
element
(
find
.
byType
(
TextField
))).
size
;
// Just testing the test and making sure that the last character is off
// Just testing the test and making sure that the last character is off
// the right side of the screen.
// the right side of the screen.
expect
(
textOffsetToPosition
(
tester
,
66
).
dx
,
1056
);
expect
(
textOffsetToPosition
(
tester
,
66
).
dx
,
greaterThan
(
screenSize
.
width
)
);
final
TestGesture
gesture
=
final
TestGesture
gesture
=
await
tester
.
startGesture
(
await
tester
.
startGesture
(
...
@@ -8448,7 +8443,7 @@ void main() {
...
@@ -8448,7 +8443,7 @@ void main() {
);
);
// The first character is now offscreen to the left.
// The first character is now offscreen to the left.
expect
(
textOffsetToPosition
(
tester
,
0
).
dx
,
moreOrLessEquals
(-
257.0
,
epsilon:
1
));
expect
(
textOffsetToPosition
(
tester
,
0
).
dx
,
lessThan
(-
100.0
));
},
variant:
TargetPlatformVariant
.
all
());
},
variant:
TargetPlatformVariant
.
all
());
testWidgets
(
'keyboard selection change scrolls the field'
,
(
WidgetTester
tester
)
async
{
testWidgets
(
'keyboard selection change scrolls the field'
,
(
WidgetTester
tester
)
async
{
...
@@ -8485,7 +8480,7 @@ void main() {
...
@@ -8485,7 +8480,7 @@ void main() {
await
tester
.
pumpAndSettle
();
await
tester
.
pumpAndSettle
();
expect
(
expect
(
controller
.
selection
,
controller
.
selection
,
const
TextSelection
.
collapsed
(
offset:
56
),
const
TextSelection
.
collapsed
(
offset:
56
,
affinity:
TextAffinity
.
upstream
),
);
);
// Keep moving out.
// Keep moving out.
...
@@ -8495,7 +8490,7 @@ void main() {
...
@@ -8495,7 +8490,7 @@ void main() {
await
tester
.
pumpAndSettle
();
await
tester
.
pumpAndSettle
();
expect
(
expect
(
controller
.
selection
,
controller
.
selection
,
const
TextSelection
.
collapsed
(
offset:
62
),
const
TextSelection
.
collapsed
(
offset:
62
,
affinity:
TextAffinity
.
upstream
),
);
);
for
(
int
i
=
0
;
i
<
(
66
-
62
);
i
+=
1
)
{
for
(
int
i
=
0
;
i
<
(
66
-
62
);
i
+=
1
)
{
await
tester
.
sendKeyEvent
(
LogicalKeyboardKey
.
arrowRight
);
await
tester
.
sendKeyEvent
(
LogicalKeyboardKey
.
arrowRight
);
...
@@ -8503,7 +8498,7 @@ void main() {
...
@@ -8503,7 +8498,7 @@ void main() {
await
tester
.
pumpAndSettle
();
await
tester
.
pumpAndSettle
();
expect
(
expect
(
controller
.
selection
,
controller
.
selection
,
const
TextSelection
.
collapsed
(
offset:
66
),
const
TextSelection
.
collapsed
(
offset:
66
,
affinity:
TextAffinity
.
upstream
),
);
// We're at the edge now.
);
// We're at the edge now.
await
tester
.
pumpAndSettle
();
await
tester
.
pumpAndSettle
();
...
...
packages/flutter/test/services/text_boundary_test.dart
0 → 100644
View file @
bcc1dc6b
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'package:flutter/services.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
void
main
(
)
{
test
(
'Character boundary works'
,
()
{
const
CharacterBoundary
boundary
=
CharacterBoundary
(
'abc'
);
const
TextPosition
midPosition
=
TextPosition
(
offset:
1
);
expect
(
boundary
.
getLeadingTextBoundaryAt
(
midPosition
),
const
TextPosition
(
offset:
1
));
expect
(
boundary
.
getTrailingTextBoundaryAt
(
midPosition
),
const
TextPosition
(
offset:
2
,
affinity:
TextAffinity
.
upstream
));
const
TextPosition
startPosition
=
TextPosition
(
offset:
0
);
expect
(
boundary
.
getLeadingTextBoundaryAt
(
startPosition
),
const
TextPosition
(
offset:
0
));
expect
(
boundary
.
getTrailingTextBoundaryAt
(
startPosition
),
const
TextPosition
(
offset:
1
,
affinity:
TextAffinity
.
upstream
));
const
TextPosition
endPosition
=
TextPosition
(
offset:
3
);
expect
(
boundary
.
getLeadingTextBoundaryAt
(
endPosition
),
const
TextPosition
(
offset:
3
,
affinity:
TextAffinity
.
upstream
));
expect
(
boundary
.
getTrailingTextBoundaryAt
(
endPosition
),
const
TextPosition
(
offset:
3
,
affinity:
TextAffinity
.
upstream
));
});
test
(
'Character boundary works with grapheme'
,
()
{
const
String
text
=
'a❄︎c'
;
const
CharacterBoundary
boundary
=
CharacterBoundary
(
text
);
TextPosition
position
=
const
TextPosition
(
offset:
1
);
expect
(
boundary
.
getLeadingTextBoundaryAt
(
position
),
const
TextPosition
(
offset:
1
));
// The `❄` takes two character length.
expect
(
boundary
.
getTrailingTextBoundaryAt
(
position
),
const
TextPosition
(
offset:
3
,
affinity:
TextAffinity
.
upstream
));
position
=
const
TextPosition
(
offset:
2
);
expect
(
boundary
.
getLeadingTextBoundaryAt
(
position
),
const
TextPosition
(
offset:
1
));
expect
(
boundary
.
getTrailingTextBoundaryAt
(
position
),
const
TextPosition
(
offset:
3
,
affinity:
TextAffinity
.
upstream
));
position
=
const
TextPosition
(
offset:
0
);
expect
(
boundary
.
getLeadingTextBoundaryAt
(
position
),
const
TextPosition
(
offset:
0
));
expect
(
boundary
.
getTrailingTextBoundaryAt
(
position
),
const
TextPosition
(
offset:
1
,
affinity:
TextAffinity
.
upstream
));
position
=
const
TextPosition
(
offset:
text
.
length
);
expect
(
boundary
.
getLeadingTextBoundaryAt
(
position
),
const
TextPosition
(
offset:
text
.
length
,
affinity:
TextAffinity
.
upstream
));
expect
(
boundary
.
getTrailingTextBoundaryAt
(
position
),
const
TextPosition
(
offset:
text
.
length
,
affinity:
TextAffinity
.
upstream
));
});
test
(
'word boundary works'
,
()
{
final
WordBoundary
boundary
=
WordBoundary
(
TestTextLayoutMetrics
());
const
TextPosition
position
=
TextPosition
(
offset:
3
);
expect
(
boundary
.
getLeadingTextBoundaryAt
(
position
).
offset
,
TestTextLayoutMetrics
.
wordBoundaryAt3
.
start
);
expect
(
boundary
.
getTrailingTextBoundaryAt
(
position
).
offset
,
TestTextLayoutMetrics
.
wordBoundaryAt3
.
end
);
});
test
(
'line boundary works'
,
()
{
final
LineBreak
boundary
=
LineBreak
(
TestTextLayoutMetrics
());
const
TextPosition
position
=
TextPosition
(
offset:
3
);
expect
(
boundary
.
getLeadingTextBoundaryAt
(
position
).
offset
,
TestTextLayoutMetrics
.
lineAt3
.
start
);
expect
(
boundary
.
getTrailingTextBoundaryAt
(
position
).
offset
,
TestTextLayoutMetrics
.
lineAt3
.
end
);
});
test
(
'document boundary works'
,
()
{
const
String
text
=
'abcd efg hi
\n
jklmno
\n
pqrstuv'
;
const
DocumentBoundary
boundary
=
DocumentBoundary
(
text
);
const
TextPosition
position
=
TextPosition
(
offset:
10
);
expect
(
boundary
.
getLeadingTextBoundaryAt
(
position
),
const
TextPosition
(
offset:
0
));
expect
(
boundary
.
getTrailingTextBoundaryAt
(
position
),
const
TextPosition
(
offset:
text
.
length
,
affinity:
TextAffinity
.
upstream
));
});
}
class
TestTextLayoutMetrics
extends
TextLayoutMetrics
{
static
const
TextSelection
lineAt3
=
TextSelection
(
baseOffset:
0
,
extentOffset:
10
);
static
const
TextRange
wordBoundaryAt3
=
TextRange
(
start:
4
,
end:
7
);
@override
TextSelection
getLineAtOffset
(
TextPosition
position
)
{
if
(
position
.
offset
==
3
)
{
return
lineAt3
;
}
throw
UnimplementedError
();
}
@override
TextPosition
getTextPositionAbove
(
TextPosition
position
)
{
throw
UnimplementedError
();
}
@override
TextPosition
getTextPositionBelow
(
TextPosition
position
)
{
throw
UnimplementedError
();
}
@override
TextRange
getWordBoundary
(
TextPosition
position
)
{
if
(
position
.
offset
==
3
)
{
return
wordBoundaryAt3
;
}
throw
UnimplementedError
();
}
}
packages/flutter/test/widgets/editable_text_shortcuts_test.dart
View file @
bcc1dc6b
...
@@ -1229,6 +1229,7 @@ void main() {
...
@@ -1229,6 +1229,7 @@ void main() {
expect
(
controller
.
selection
,
const
TextSelection
.
collapsed
(
expect
(
controller
.
selection
,
const
TextSelection
.
collapsed
(
offset:
21
,
offset:
21
,
affinity:
TextAffinity
.
upstream
,
));
));
},
variant:
TargetPlatformVariant
.
all
());
},
variant:
TargetPlatformVariant
.
all
());
...
@@ -1243,6 +1244,7 @@ void main() {
...
@@ -1243,6 +1244,7 @@ void main() {
expect
(
controller
.
selection
,
const
TextSelection
.
collapsed
(
expect
(
controller
.
selection
,
const
TextSelection
.
collapsed
(
offset:
10
,
offset:
10
,
affinity:
TextAffinity
.
upstream
,
));
));
},
variant:
allExceptApple
);
},
variant:
allExceptApple
);
...
@@ -1353,6 +1355,7 @@ void main() {
...
@@ -1353,6 +1355,7 @@ void main() {
await
tester
.
pump
();
await
tester
.
pump
();
expect
(
controller
.
selection
,
const
TextSelection
.
collapsed
(
expect
(
controller
.
selection
,
const
TextSelection
.
collapsed
(
offset:
46
,
// After "to".
offset:
46
,
// After "to".
affinity:
TextAffinity
.
upstream
,
));
));
// "good" to "come" is selected.
// "good" to "come" is selected.
...
@@ -1365,6 +1368,7 @@ void main() {
...
@@ -1365,6 +1368,7 @@ void main() {
await
tester
.
pump
();
await
tester
.
pump
();
expect
(
controller
.
selection
,
const
TextSelection
.
collapsed
(
expect
(
controller
.
selection
,
const
TextSelection
.
collapsed
(
offset:
28
,
// After "good".
offset:
28
,
// After "good".
affinity:
TextAffinity
.
upstream
,
));
));
},
variant:
allExceptApple
);
},
variant:
allExceptApple
);
...
@@ -1673,6 +1677,7 @@ void main() {
...
@@ -1673,6 +1677,7 @@ void main() {
expect
(
controller
.
selection
,
const
TextSelection
.
collapsed
(
expect
(
controller
.
selection
,
const
TextSelection
.
collapsed
(
offset:
10
,
offset:
10
,
affinity:
TextAffinity
.
upstream
,
));
));
},
variant:
macOSOnly
);
},
variant:
macOSOnly
);
...
@@ -1743,6 +1748,7 @@ void main() {
...
@@ -1743,6 +1748,7 @@ void main() {
await
tester
.
pump
();
await
tester
.
pump
();
expect
(
controller
.
selection
,
const
TextSelection
.
collapsed
(
expect
(
controller
.
selection
,
const
TextSelection
.
collapsed
(
offset:
46
,
// After "to".
offset:
46
,
// After "to".
affinity:
TextAffinity
.
upstream
,
));
));
// "good" to "come" is selected.
// "good" to "come" is selected.
...
@@ -1755,6 +1761,7 @@ void main() {
...
@@ -1755,6 +1761,7 @@ void main() {
await
tester
.
pump
();
await
tester
.
pump
();
expect
(
controller
.
selection
,
const
TextSelection
.
collapsed
(
expect
(
controller
.
selection
,
const
TextSelection
.
collapsed
(
offset:
28
,
// After "good".
offset:
28
,
// After "good".
affinity:
TextAffinity
.
upstream
,
));
));
},
variant:
macOSOnly
);
},
variant:
macOSOnly
);
...
...
packages/flutter/test/widgets/editable_text_test.dart
View file @
bcc1dc6b
...
@@ -5832,6 +5832,7 @@ void main() {
...
@@ -5832,6 +5832,7 @@ void main() {
equals
(
equals
(
const
TextSelection
.
collapsed
(
const
TextSelection
.
collapsed
(
offset:
3
,
offset:
3
,
affinity:
TextAffinity
.
upstream
,
),
),
),
),
reason:
'on
$platform
'
,
reason:
'on
$platform
'
,
...
@@ -5941,6 +5942,7 @@ void main() {
...
@@ -5941,6 +5942,7 @@ void main() {
const
TextSelection
(
const
TextSelection
(
baseOffset:
10
,
baseOffset:
10
,
extentOffset:
10
,
extentOffset:
10
,
affinity:
TextAffinity
.
upstream
,
),
),
),
),
reason:
'on
$platform
'
,
reason:
'on
$platform
'
,
...
@@ -6398,6 +6400,7 @@ void main() {
...
@@ -6398,6 +6400,7 @@ void main() {
equals
(
equals
(
const
TextSelection
.
collapsed
(
const
TextSelection
.
collapsed
(
offset:
23
,
offset:
23
,
affinity:
TextAffinity
.
upstream
,
),
),
),
),
reason:
'on
$platform
'
,
reason:
'on
$platform
'
,
...
@@ -6422,6 +6425,7 @@ void main() {
...
@@ -6422,6 +6425,7 @@ void main() {
equals
(
equals
(
const
TextSelection
.
collapsed
(
const
TextSelection
.
collapsed
(
offset:
23
,
offset:
23
,
affinity:
TextAffinity
.
upstream
,
),
),
),
),
reason:
'on
$platform
'
,
reason:
'on
$platform
'
,
...
@@ -6464,6 +6468,7 @@ void main() {
...
@@ -6464,6 +6468,7 @@ void main() {
equals
(
equals
(
const
TextSelection
.
collapsed
(
const
TextSelection
.
collapsed
(
offset:
23
,
offset:
23
,
affinity:
TextAffinity
.
upstream
,
),
),
),
),
reason:
'on
$platform
'
,
reason:
'on
$platform
'
,
...
@@ -6549,6 +6554,7 @@ void main() {
...
@@ -6549,6 +6554,7 @@ void main() {
equals
(
equals
(
const
TextSelection
.
collapsed
(
const
TextSelection
.
collapsed
(
offset:
32
,
offset:
32
,
affinity:
TextAffinity
.
upstream
,
),
),
),
),
reason:
'on
$platform
'
,
reason:
'on
$platform
'
,
...
@@ -6573,6 +6579,7 @@ void main() {
...
@@ -6573,6 +6579,7 @@ void main() {
equals
(
equals
(
const
TextSelection
.
collapsed
(
const
TextSelection
.
collapsed
(
offset:
32
,
offset:
32
,
affinity:
TextAffinity
.
upstream
,
),
),
),
),
reason:
'on
$platform
'
,
reason:
'on
$platform
'
,
...
@@ -6615,6 +6622,7 @@ void main() {
...
@@ -6615,6 +6622,7 @@ void main() {
equals
(
equals
(
const
TextSelection
.
collapsed
(
const
TextSelection
.
collapsed
(
offset:
32
,
offset:
32
,
affinity:
TextAffinity
.
upstream
,
),
),
),
),
reason:
'on
$platform
'
,
reason:
'on
$platform
'
,
...
@@ -6710,6 +6718,7 @@ void main() {
...
@@ -6710,6 +6718,7 @@ void main() {
equals
(
equals
(
const
TextSelection
.
collapsed
(
const
TextSelection
.
collapsed
(
offset:
32
,
offset:
32
,
affinity:
TextAffinity
.
upstream
,
),
),
),
),
reason:
'on
$platform
'
,
reason:
'on
$platform
'
,
...
@@ -6734,6 +6743,7 @@ void main() {
...
@@ -6734,6 +6743,7 @@ void main() {
equals
(
equals
(
const
TextSelection
.
collapsed
(
const
TextSelection
.
collapsed
(
offset:
32
,
offset:
32
,
affinity:
TextAffinity
.
upstream
,
),
),
),
),
reason:
'on
$platform
'
,
reason:
'on
$platform
'
,
...
@@ -6776,6 +6786,7 @@ void main() {
...
@@ -6776,6 +6786,7 @@ void main() {
equals
(
equals
(
const
TextSelection
.
collapsed
(
const
TextSelection
.
collapsed
(
offset:
32
,
offset:
32
,
affinity:
TextAffinity
.
upstream
,
),
),
),
),
reason:
'on
$platform
'
,
reason:
'on
$platform
'
,
...
@@ -6872,6 +6883,7 @@ void main() {
...
@@ -6872,6 +6883,7 @@ void main() {
equals
(
equals
(
const
TextSelection
.
collapsed
(
const
TextSelection
.
collapsed
(
offset:
23
,
offset:
23
,
affinity:
TextAffinity
.
upstream
,
),
),
),
),
reason:
'on
$platform
'
,
reason:
'on
$platform
'
,
...
@@ -6917,6 +6929,7 @@ void main() {
...
@@ -6917,6 +6929,7 @@ void main() {
const
TextSelection
(
const
TextSelection
(
baseOffset:
23
,
baseOffset:
23
,
extentOffset:
23
,
extentOffset:
23
,
affinity:
TextAffinity
.
upstream
,
),
),
),
),
reason:
'on
$platform
'
,
reason:
'on
$platform
'
,
...
@@ -6927,6 +6940,7 @@ void main() {
...
@@ -6927,6 +6940,7 @@ void main() {
const
TextSelection
(
const
TextSelection
(
baseOffset:
23
,
baseOffset:
23
,
extentOffset:
23
,
extentOffset:
23
,
affinity:
TextAffinity
.
upstream
,
),
),
),
),
reason:
'on
$platform
'
,
reason:
'on
$platform
'
,
...
@@ -7060,6 +7074,7 @@ void main() {
...
@@ -7060,6 +7074,7 @@ void main() {
controller
.
selection
,
controller
.
selection
,
equals
(
const
TextSelection
.
collapsed
(
equals
(
const
TextSelection
.
collapsed
(
offset:
4
,
offset:
4
,
affinity:
TextAffinity
.
upstream
,
)),
)),
);
);
...
@@ -7243,6 +7258,7 @@ void main() {
...
@@ -7243,6 +7258,7 @@ void main() {
equals
(
equals
(
const
TextSelection
.
collapsed
(
const
TextSelection
.
collapsed
(
offset:
32
,
offset:
32
,
affinity:
TextAffinity
.
upstream
,
),
),
),
),
reason:
'on
$platform
'
,
reason:
'on
$platform
'
,
...
@@ -7266,6 +7282,7 @@ void main() {
...
@@ -7266,6 +7282,7 @@ void main() {
equals
(
equals
(
const
TextSelection
.
collapsed
(
const
TextSelection
.
collapsed
(
offset:
32
,
offset:
32
,
affinity:
TextAffinity
.
upstream
,
),
),
),
),
reason:
'on
$platform
'
,
reason:
'on
$platform
'
,
...
@@ -7323,6 +7340,7 @@ void main() {
...
@@ -7323,6 +7340,7 @@ void main() {
equals
(
equals
(
const
TextSelection
.
collapsed
(
const
TextSelection
.
collapsed
(
offset:
32
,
offset:
32
,
affinity:
TextAffinity
.
upstream
,
),
),
),
),
reason:
'on
$platform
'
,
reason:
'on
$platform
'
,
...
@@ -7435,6 +7453,7 @@ void main() {
...
@@ -7435,6 +7453,7 @@ void main() {
equals
(
equals
(
const
TextSelection
.
collapsed
(
const
TextSelection
.
collapsed
(
offset:
32
,
offset:
32
,
affinity:
TextAffinity
.
upstream
,
),
),
),
),
reason:
'on
$platform
'
,
reason:
'on
$platform
'
,
...
@@ -7458,6 +7477,7 @@ void main() {
...
@@ -7458,6 +7477,7 @@ void main() {
equals
(
equals
(
const
TextSelection
.
collapsed
(
const
TextSelection
.
collapsed
(
offset:
32
,
offset:
32
,
affinity:
TextAffinity
.
upstream
,
),
),
),
),
reason:
'on
$platform
'
,
reason:
'on
$platform
'
,
...
@@ -7515,6 +7535,7 @@ void main() {
...
@@ -7515,6 +7535,7 @@ void main() {
equals
(
equals
(
const
TextSelection
.
collapsed
(
const
TextSelection
.
collapsed
(
offset:
32
,
offset:
32
,
affinity:
TextAffinity
.
upstream
,
),
),
),
),
reason:
'on
$platform
'
,
reason:
'on
$platform
'
,
...
@@ -7626,6 +7647,7 @@ void main() {
...
@@ -7626,6 +7647,7 @@ void main() {
equals
(
equals
(
const
TextSelection
.
collapsed
(
const
TextSelection
.
collapsed
(
offset:
32
,
offset:
32
,
affinity:
TextAffinity
.
upstream
,
),
),
),
),
);
);
...
@@ -10383,7 +10405,6 @@ void main() {
...
@@ -10383,7 +10405,6 @@ void main() {
expect
(
controller
.
selection
.
isCollapsed
,
false
);
expect
(
controller
.
selection
.
isCollapsed
,
false
);
expect
(
controller
.
selection
.
baseOffset
,
7
);
expect
(
controller
.
selection
.
baseOffset
,
7
);
expect
(
controller
.
selection
.
extentOffset
,
10
);
expect
(
controller
.
selection
.
extentOffset
,
10
);
await
sendKeys
(
await
sendKeys
(
tester
,
tester
,
<
LogicalKeyboardKey
>[
LogicalKeyboardKey
.
arrowLeft
],
<
LogicalKeyboardKey
>[
LogicalKeyboardKey
.
arrowLeft
],
...
...
packages/flutter/test/widgets/editable_text_utils.dart
View file @
bcc1dc6b
...
@@ -2,6 +2,7 @@
...
@@ -2,6 +2,7 @@
// 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
'package:flutter/foundation.dart'
;
import
'package:flutter/material.dart'
;
import
'package:flutter/material.dart'
;
import
'package:flutter/rendering.dart'
;
import
'package:flutter/rendering.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
...
@@ -42,7 +43,7 @@ Offset textOffsetToPosition(WidgetTester tester, int offset) {
...
@@ -42,7 +43,7 @@ Offset textOffsetToPosition(WidgetTester tester, int offset) {
renderEditable
,
renderEditable
,
);
);
expect
(
endpoints
.
length
,
1
);
expect
(
endpoints
.
length
,
1
);
return
endpoints
[
0
].
point
+
const
Offset
(
0.0
,
-
2.0
);
return
endpoints
[
0
].
point
+
const
Offset
(
kIsWeb
?
1.0
:
0.0
,
-
2.0
);
}
}
// Simple controller that builds a WidgetSpan with 100 height.
// Simple controller that builds a WidgetSpan with 100 height.
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment