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
57e577a5
Unverified
Commit
57e577a5
authored
Oct 17, 2022
by
LongCatIsLooong
Committed by
GitHub
Oct 17, 2022
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Clean up `_updateSelectionRects` (#113425)
parent
c84897c6
Changes
2
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
215 additions
and
91 deletions
+215
-91
editable_text.dart
packages/flutter/lib/src/widgets/editable_text.dart
+95
-66
editable_text_test.dart
packages/flutter/test/widgets/editable_text_test.dart
+120
-25
No files found.
packages/flutter/lib/src/widgets/editable_text.dart
View file @
57e577a5
...
...
@@ -1779,6 +1779,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
final
ClipboardStatusNotifier
?
_clipboardStatus
=
kIsWeb
?
null
:
ClipboardStatusNotifier
();
TextInputConnection
?
_textInputConnection
;
bool
get
_hasInputConnection
=>
_textInputConnection
?.
attached
??
false
;
TextSelectionOverlay
?
_selectionOverlay
;
ScrollController
?
_internalScrollController
;
...
...
@@ -2037,7 +2039,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
_clipboardStatus
?.
addListener
(
_onChangedClipboardStatus
);
widget
.
controller
.
addListener
(
_didChangeTextEditingValue
);
widget
.
focusNode
.
addListener
(
_handleFocusChanged
);
_scrollController
.
addListener
(
_
updateSelectionOverlayFor
Scroll
);
_scrollController
.
addListener
(
_
onEditable
Scroll
);
_cursorVisibilityNotifier
.
value
=
widget
.
showCursor
;
_spellCheckConfiguration
=
_inferSpellCheckConfiguration
(
widget
.
spellCheckConfiguration
);
}
...
...
@@ -2125,8 +2127,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
}
if
(
widget
.
scrollController
!=
oldWidget
.
scrollController
)
{
(
oldWidget
.
scrollController
??
_internalScrollController
)?.
removeListener
(
_
updateSelectionOverlayFor
Scroll
);
_scrollController
.
addListener
(
_
updateSelectionOverlayFor
Scroll
);
(
oldWidget
.
scrollController
??
_internalScrollController
)?.
removeListener
(
_
onEditable
Scroll
);
_scrollController
.
addListener
(
_
onEditable
Scroll
);
}
if
(!
_shouldCreateInputConnection
)
{
...
...
@@ -2567,7 +2569,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
return
RevealedOffset
(
rect:
rect
.
shift
(
unitOffset
*
offsetDelta
),
offset:
targetOffset
);
}
bool
get
_hasInputConnection
=>
_textInputConnection
?.
attached
??
false
;
/// Whether to send the autofill information to the autofill service. True by
/// default.
bool
get
_needsAutofill
=>
_effectiveAutofillClient
.
textInputConfiguration
.
autofillConfiguration
.
enabled
;
...
...
@@ -2718,8 +2719,9 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
}
}
void
_
updateSelectionOverlayFor
Scroll
()
{
void
_
onEditable
Scroll
()
{
_selectionOverlay
?.
updateForScroll
();
_scribbleCacheKey
=
null
;
}
void
_createSelectionOverlay
()
{
...
...
@@ -3115,11 +3117,6 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
// Place cursor at the end if the selection is invalid when we receive focus.
_handleSelectionChanged
(
TextSelection
.
collapsed
(
offset:
_value
.
text
.
length
),
null
);
}
_cachedText
=
''
;
_cachedFirstRect
=
null
;
_cachedSize
=
Size
.
zero
;
_cachedPlaceholder
=
-
1
;
}
else
{
WidgetsBinding
.
instance
.
removeObserver
(
this
);
setState
(()
{
_currentPromptRectRange
=
null
;
});
...
...
@@ -3127,74 +3124,66 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
updateKeepAlive
();
}
String
_cachedText
=
''
;
Rect
?
_cachedFirstRect
;
Size
_cachedSize
=
Size
.
zero
;
int
_cachedPlaceholder
=
-
1
;
TextStyle
?
_cachedTextStyle
;
_ScribbleCacheKey
?
_scribbleCacheKey
;
void
_updateSelectionRects
({
bool
force
=
false
})
{
if
(!
widget
.
scribbleEnabled
)
{
if
(!
widget
.
scribbleEnabled
||
defaultTargetPlatform
!=
TargetPlatform
.
iOS
)
{
return
;
}
if
(
defaultTargetPlatform
!=
TargetPlatform
.
iOS
)
{
final
ScrollDirection
scrollDirection
=
_scrollController
.
position
.
userScrollDirection
;
if
(
scrollDirection
!=
ScrollDirection
.
idle
)
{
return
;
}
final
String
text
=
renderEditable
.
text
?.
toPlainText
(
includeSemanticsLabels:
false
)
??
''
;
final
List
<
Rect
>
firstSelectionBoxes
=
renderEditable
.
getBoxesForSelection
(
const
TextSelection
(
baseOffset:
0
,
extentOffset:
1
));
final
Rect
?
firstRect
=
firstSelectionBoxes
.
isNotEmpty
?
firstSelectionBoxes
.
first
:
null
;
final
ScrollDirection
scrollDirection
=
_scrollController
.
position
.
userScrollDirection
;
final
Size
size
=
renderEditable
.
size
;
final
bool
textChanged
=
text
!=
_cachedText
;
final
bool
textStyleChanged
=
_cachedTextStyle
!=
widget
.
style
;
final
bool
firstRectChanged
=
_cachedFirstRect
!=
firstRect
;
final
bool
sizeChanged
=
_cachedSize
!=
size
;
final
bool
placeholderChanged
=
_cachedPlaceholder
!=
_placeholderLocation
;
if
(
scrollDirection
==
ScrollDirection
.
idle
&&
(
force
||
textChanged
||
textStyleChanged
||
firstRectChanged
||
sizeChanged
||
placeholderChanged
))
{
_cachedText
=
text
;
_cachedFirstRect
=
firstRect
;
_cachedTextStyle
=
widget
.
style
;
_cachedSize
=
size
;
_cachedPlaceholder
=
_placeholderLocation
;
bool
belowRenderEditableBottom
=
false
;
final
List
<
SelectionRect
>
rects
=
List
<
SelectionRect
?>.
generate
(
_cachedText
.
characters
.
length
,
(
int
i
)
{
if
(
belowRenderEditableBottom
)
{
return
null
;
}
final
InlineSpan
inlineSpan
=
renderEditable
.
text
!;
final
_ScribbleCacheKey
newCacheKey
=
_ScribbleCacheKey
(
inlineSpan:
inlineSpan
,
textAlign:
widget
.
textAlign
,
textDirection:
_textDirection
,
textScaleFactor:
widget
.
textScaleFactor
??
MediaQuery
.
textScaleFactorOf
(
context
),
textHeightBehavior:
widget
.
textHeightBehavior
??
DefaultTextHeightBehavior
.
of
(
context
),
locale:
widget
.
locale
,
structStyle:
widget
.
strutStyle
,
placeholder:
_placeholderLocation
,
size:
renderEditable
.
size
,
);
final
int
offset
=
_cachedText
.
characters
.
getRange
(
0
,
i
).
string
.
length
;
final
List
<
Rect
>
boxes
=
renderEditable
.
getBoxesForSelection
(
TextSelection
(
baseOffset:
offset
,
extentOffset:
offset
+
_cachedText
.
characters
.
characterAt
(
i
).
string
.
length
));
if
(
boxes
.
isEmpty
)
{
return
null
;
}
final
RenderComparison
comparison
=
force
?
RenderComparison
.
layout
:
_scribbleCacheKey
?.
compare
(
newCacheKey
)
??
RenderComparison
.
layout
;
if
(
comparison
.
index
<
RenderComparison
.
layout
.
index
)
{
return
;
}
_scribbleCacheKey
=
newCacheKey
;
final
List
<
SelectionRect
>
rects
=
<
SelectionRect
>[];
int
graphemeStart
=
0
;
// Can't use _value.text here: the controller value could change between
// frames.
final
String
plainText
=
inlineSpan
.
toPlainText
(
includeSemanticsLabels:
false
);
final
CharacterRange
characterRange
=
CharacterRange
(
plainText
);
while
(
characterRange
.
moveNext
())
{
final
int
graphemeEnd
=
graphemeStart
+
characterRange
.
current
.
length
;
final
List
<
Rect
>
boxes
=
renderEditable
.
getBoxesForSelection
(
TextSelection
(
baseOffset:
graphemeStart
,
extentOffset:
graphemeEnd
),
);
final
SelectionRect
selectionRect
=
SelectionRect
(
bounds:
boxes
.
first
,
position:
offset
,
);
if
(
renderEditable
.
paintBounds
.
bottom
<
selectionRect
.
bounds
.
top
)
{
belowRenderEditableBottom
=
true
;
return
null
;
}
return
selectionRect
;
},
).
where
((
SelectionRect
?
selectionRect
)
{
if
(
selectionRect
==
null
)
{
return
false
;
}
if
(
renderEditable
.
paintBounds
.
right
<
selectionRect
.
bounds
.
left
||
selectionRect
.
bounds
.
right
<
renderEditable
.
paintBounds
.
left
)
{
return
false
;
final
Rect
?
box
=
boxes
.
isEmpty
?
null
:
boxes
.
first
;
if
(
box
!=
null
)
{
final
Rect
paintBounds
=
renderEditable
.
paintBounds
;
// Stop early when characters are already below the bottom edge of the
// RenderEditable, regardless of its clipBehavior.
if
(
paintBounds
.
bottom
<=
box
.
top
)
{
break
;
}
if
(
renderEditable
.
paintBounds
.
bottom
<
selectionRect
.
bounds
.
top
||
selectionRect
.
bounds
.
bottom
<
renderEditable
.
paintBounds
.
top
)
{
re
turn
false
;
if
(
paintBounds
.
contains
(
box
.
topLeft
)
||
paintBounds
.
contains
(
box
.
bottomRight
)
)
{
re
cts
.
add
(
SelectionRect
(
position:
graphemeStart
,
bounds:
box
))
;
}
return
true
;
}).
map
<
SelectionRect
>((
SelectionRect
?
selectionRect
)
=>
selectionRect
!).
toList
();
_textInputConnection
!.
setSelectionRects
(
rects
);
}
graphemeStart
=
graphemeEnd
;
}
_textInputConnection
!.
setSelectionRects
(
rects
);
}
void
_updateSizeAndTransform
()
{
...
...
@@ -4103,6 +4092,46 @@ class _Editable extends MultiChildRenderObjectWidget {
}
}
@immutable
class
_ScribbleCacheKey
{
const
_ScribbleCacheKey
({
required
this
.
inlineSpan
,
required
this
.
textAlign
,
required
this
.
textDirection
,
required
this
.
textScaleFactor
,
required
this
.
textHeightBehavior
,
required
this
.
locale
,
required
this
.
structStyle
,
required
this
.
placeholder
,
required
this
.
size
,
});
final
TextAlign
textAlign
;
final
TextDirection
textDirection
;
final
double
textScaleFactor
;
final
TextHeightBehavior
?
textHeightBehavior
;
final
Locale
?
locale
;
final
StrutStyle
structStyle
;
final
int
placeholder
;
final
Size
size
;
final
InlineSpan
inlineSpan
;
RenderComparison
compare
(
_ScribbleCacheKey
other
)
{
if
(
identical
(
other
,
this
))
{
return
RenderComparison
.
identical
;
}
final
bool
needsLayout
=
textAlign
!=
other
.
textAlign
||
textDirection
!=
other
.
textDirection
||
textScaleFactor
!=
other
.
textScaleFactor
||
(
textHeightBehavior
??
const
TextHeightBehavior
())
!=
(
other
.
textHeightBehavior
??
const
TextHeightBehavior
())
||
locale
!=
other
.
locale
||
structStyle
!=
other
.
structStyle
||
placeholder
!=
other
.
placeholder
||
size
!=
other
.
size
;
return
needsLayout
?
RenderComparison
.
layout
:
inlineSpan
.
compareTo
(
other
.
inlineSpan
);
}
}
class
_ScribbleFocusable
extends
StatefulWidget
{
const
_ScribbleFocusable
({
required
this
.
child
,
...
...
packages/flutter/test/widgets/editable_text_test.dart
View file @
57e577a5
...
...
@@ -4628,41 +4628,136 @@ void main() {
// Ensure selection rects are sent on iPhone (using SE 3rd gen size)
tester
.
binding
.
window
.
physicalSizeTestValue
=
const
Size
(
750.0
,
1334.0
);
final
List
<
MethodCall
>
log
=
<
MethodCall
>[];
final
List
<
List
<
SelectionRect
>>
log
=
<
List
<
SelectionRect
>
>[];
SystemChannels
.
textInput
.
setMockMethodCallHandler
((
MethodCall
methodCall
)
async
{
log
.
add
(
methodCall
);
if
(
methodCall
.
method
==
'TextInput.setSelectionRects'
)
{
final
List
<
dynamic
>
args
=
methodCall
.
arguments
as
List
<
dynamic
>;
final
List
<
SelectionRect
>
selectionRects
=
<
SelectionRect
>[];
for
(
final
dynamic
rect
in
args
)
{
selectionRects
.
add
(
SelectionRect
(
position:
(
rect
as
List
<
dynamic
>)[
4
]
as
int
,
bounds:
Rect
.
fromLTWH
(
rect
[
0
]
as
double
,
rect
[
1
]
as
double
,
rect
[
2
]
as
double
,
rect
[
3
]
as
double
),
));
}
log
.
add
(
selectionRects
);
}
});
final
TextEditingController
controller
=
TextEditingController
();
final
ScrollController
scrollController
=
ScrollController
();
controller
.
text
=
'Text1'
;
await
tester
.
pumpWidget
(
MediaQuery
(
data:
const
MediaQueryData
(),
child:
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
<
Widget
>[
EditableText
(
key:
ValueKey
<
String
>(
controller
.
text
),
controller:
controller
,
focusNode:
FocusNode
(),
style:
Typography
.
material2018
().
black
.
titleMedium
!,
cursorColor:
Colors
.
blue
,
backgroundCursorColor:
Colors
.
grey
,
Future
<
void
>
pumpEditableText
({
double
?
width
,
double
?
height
,
TextAlign
textAlign
=
TextAlign
.
start
})
async
{
await
tester
.
pumpWidget
(
MediaQuery
(
data:
const
MediaQueryData
(),
child:
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
Center
(
child:
SizedBox
(
width:
width
,
height:
height
,
child:
EditableText
(
controller:
controller
,
textAlign:
textAlign
,
scrollController:
scrollController
,
maxLines:
null
,
focusNode:
focusNode
,
cursorWidth:
0
,
style:
Typography
.
material2018
().
black
.
titleMedium
!,
cursorColor:
Colors
.
blue
,
backgroundCursorColor:
Colors
.
grey
,
),
),
]
,
)
,
),
),
),
);
await
tester
.
showKeyboard
(
find
.
byKey
(
ValueKey
<
String
>(
controller
.
text
)));
);
}
// There should be a new platform message updating the selection rects.
final
MethodCall
methodCall
=
log
.
firstWhere
((
MethodCall
m
)
=>
m
.
method
==
'TextInput.setSelectionRects'
);
expect
(
methodCall
.
method
,
'TextInput.setSelectionRects'
);
expect
((
methodCall
.
arguments
as
List
<
dynamic
>).
length
,
5
);
await
pumpEditableText
();
expect
(
log
,
isEmpty
);
await
tester
.
showKeyboard
(
find
.
byType
(
EditableText
));
// First update.
expect
(
log
.
single
,
const
<
SelectionRect
>[
SelectionRect
(
position:
0
,
bounds:
Rect
.
fromLTRB
(
0.0
,
0.0
,
14.0
,
14.0
)),
SelectionRect
(
position:
1
,
bounds:
Rect
.
fromLTRB
(
14.0
,
0.0
,
28.0
,
14.0
)),
SelectionRect
(
position:
2
,
bounds:
Rect
.
fromLTRB
(
28.0
,
0.0
,
42.0
,
14.0
)),
SelectionRect
(
position:
3
,
bounds:
Rect
.
fromLTRB
(
42.0
,
0.0
,
56.0
,
14.0
)),
SelectionRect
(
position:
4
,
bounds:
Rect
.
fromLTRB
(
56.0
,
0.0
,
70.0
,
14.0
))
]);
log
.
clear
();
await
tester
.
pumpAndSettle
();
expect
(
log
,
isEmpty
);
await
pumpEditableText
();
expect
(
log
,
isEmpty
);
// Change the width such that each character occupies a line.
await
pumpEditableText
(
width:
20
);
expect
(
log
.
single
,
const
<
SelectionRect
>[
SelectionRect
(
position:
0
,
bounds:
Rect
.
fromLTRB
(
0.0
,
0.0
,
14.0
,
14.0
)),
SelectionRect
(
position:
1
,
bounds:
Rect
.
fromLTRB
(
0.0
,
14.0
,
14.0
,
28.0
)),
SelectionRect
(
position:
2
,
bounds:
Rect
.
fromLTRB
(
0.0
,
28.0
,
14.0
,
42.0
)),
SelectionRect
(
position:
3
,
bounds:
Rect
.
fromLTRB
(
0.0
,
42.0
,
14.0
,
56.0
)),
SelectionRect
(
position:
4
,
bounds:
Rect
.
fromLTRB
(
0.0
,
56.0
,
14.0
,
70.0
))
]);
log
.
clear
();
await
tester
.
enterText
(
find
.
byType
(
EditableText
),
'Text1👨👩👦'
);
await
tester
.
pump
();
expect
(
log
.
single
,
const
<
SelectionRect
>[
SelectionRect
(
position:
0
,
bounds:
Rect
.
fromLTRB
(
0.0
,
0.0
,
14.0
,
14.0
)),
SelectionRect
(
position:
1
,
bounds:
Rect
.
fromLTRB
(
0.0
,
14.0
,
14.0
,
28.0
)),
SelectionRect
(
position:
2
,
bounds:
Rect
.
fromLTRB
(
0.0
,
28.0
,
14.0
,
42.0
)),
SelectionRect
(
position:
3
,
bounds:
Rect
.
fromLTRB
(
0.0
,
42.0
,
14.0
,
56.0
)),
SelectionRect
(
position:
4
,
bounds:
Rect
.
fromLTRB
(
0.0
,
56.0
,
14.0
,
70.0
)),
SelectionRect
(
position:
5
,
bounds:
Rect
.
fromLTRB
(
0.0
,
70.0
,
42.0
,
84.0
)),
]);
log
.
clear
();
// The 4th line will be partially visible.
await
pumpEditableText
(
width:
20
,
height:
45
);
expect
(
log
.
single
,
const
<
SelectionRect
>[
SelectionRect
(
position:
0
,
bounds:
Rect
.
fromLTRB
(
0.0
,
0.0
,
14.0
,
14.0
)),
SelectionRect
(
position:
1
,
bounds:
Rect
.
fromLTRB
(
0.0
,
14.0
,
14.0
,
28.0
)),
SelectionRect
(
position:
2
,
bounds:
Rect
.
fromLTRB
(
0.0
,
28.0
,
14.0
,
42.0
)),
SelectionRect
(
position:
3
,
bounds:
Rect
.
fromLTRB
(
0.0
,
42.0
,
14.0
,
56.0
)),
]);
log
.
clear
();
await
pumpEditableText
(
width:
20
,
height:
45
,
textAlign:
TextAlign
.
right
);
// This is 1px off from being completely right-aligned. The 1px width is
// reserved for caret.
expect
(
log
.
single
,
const
<
SelectionRect
>[
SelectionRect
(
position:
0
,
bounds:
Rect
.
fromLTRB
(
5.0
,
0.0
,
19.0
,
14.0
)),
SelectionRect
(
position:
1
,
bounds:
Rect
.
fromLTRB
(
5.0
,
14.0
,
19.0
,
28.0
)),
SelectionRect
(
position:
2
,
bounds:
Rect
.
fromLTRB
(
5.0
,
28.0
,
19.0
,
42.0
)),
SelectionRect
(
position:
3
,
bounds:
Rect
.
fromLTRB
(
5.0
,
42.0
,
19.0
,
56.0
)),
// These 2 lines will be out of bounds.
// SelectionRect(position: 4, bounds: Rect.fromLTRB(5.0, 56.0, 19.0, 70.0)),
// SelectionRect(position: 5, bounds: Rect.fromLTRB(-23.0, 70.0, 19.0, 84.0)),
]);
log
.
clear
();
expect
(
scrollController
.
offset
,
0
);
// Scrolling also triggers update.
scrollController
.
jumpTo
(
14
);
await
tester
.
pumpAndSettle
();
expect
(
log
.
single
,
const
<
SelectionRect
>[
SelectionRect
(
position:
0
,
bounds:
Rect
.
fromLTRB
(
5.0
,
-
14.0
,
19.0
,
0.0
)),
SelectionRect
(
position:
1
,
bounds:
Rect
.
fromLTRB
(
5.0
,
0.0
,
19.0
,
14.0
)),
SelectionRect
(
position:
2
,
bounds:
Rect
.
fromLTRB
(
5.0
,
14.0
,
19.0
,
28.0
)),
SelectionRect
(
position:
3
,
bounds:
Rect
.
fromLTRB
(
5.0
,
28.0
,
19.0
,
42.0
)),
SelectionRect
(
position:
4
,
bounds:
Rect
.
fromLTRB
(
5.0
,
42.0
,
19.0
,
56.0
)),
// This line is skipped because it's below the bottom edge of the render
// object.
// SelectionRect(position: 5, bounds: Rect.fromLTRB(5.0, 56.0, 47.0, 70.0)),
]);
log
.
clear
();
// On web, we should rely on the browser's implementation of Scribble, so we will not send selection rects.
},
skip:
kIsWeb
,
variant:
const
TargetPlatformVariant
(<
TargetPlatform
>{
TargetPlatform
.
iOS
}));
// [intended]
...
...
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