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
5647407f
Unverified
Commit
5647407f
authored
Mar 25, 2021
by
Tong Mu
Committed by
GitHub
Mar 25, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add mouseCursor, onEnter, and onExit to TextSpan (#77754)
parent
b44e536c
Changes
6
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
229 additions
and
42 deletions
+229
-42
cupertino_navigation_demo_test.dart
...y/test/demo/cupertino/cupertino_navigation_demo_test.dart
+1
-0
text_span.dart
packages/flutter/lib/src/painting/text_span.dart
+60
-4
editable.dart
packages/flutter/lib/src/rendering/editable.dart
+12
-8
paragraph.dart
packages/flutter/lib/src/rendering/paragraph.dart
+13
-18
text_span_test.dart
packages/flutter/test/painting/text_span_test.dart
+126
-0
slivers_padding_test.dart
packages/flutter/test/widgets/slivers_padding_test.dart
+17
-12
No files found.
dev/integration_tests/flutter_gallery/test/demo/cupertino/cupertino_navigation_demo_test.dart
View file @
5647407f
...
...
@@ -28,6 +28,7 @@ void main() {
matchesGoldenFile
(
'cupertino_navigation_demo.screen.1.png'
),
);
await
tester
.
pump
();
// Need a new frame after loading fonts to refresh layout.
// Tap some row to go to the next page.
await
tester
.
tap
(
find
.
text
(
'Buy this cool color'
).
first
);
await
tester
.
pump
();
...
...
packages/flutter/lib/src/painting/text_span.dart
View file @
5647407f
...
...
@@ -60,7 +60,7 @@ import 'text_style.dart';
/// * [RichText], a widget for finer control of text rendering.
/// * [TextPainter], a class for painting [TextSpan] objects on a [Canvas].
@immutable
class
TextSpan
extends
InlineSpan
{
class
TextSpan
extends
InlineSpan
implements
HitTestTarget
,
MouseTrackerAnnotation
{
/// Creates a [TextSpan] with the given values.
///
/// For the object to be useful, at least one of [text] or
...
...
@@ -70,8 +70,13 @@ class TextSpan extends InlineSpan {
this
.
children
,
TextStyle
?
style
,
this
.
recognizer
,
MouseCursor
?
mouseCursor
,
this
.
onEnter
,
this
.
onExit
,
this
.
semanticsLabel
,
})
:
assert
(!(
text
==
null
&&
semanticsLabel
!=
null
)),
})
:
mouseCursor
=
mouseCursor
??
(
recognizer
==
null
?
MouseCursor
.
defer
:
SystemMouseCursors
.
click
),
assert
(!(
text
==
null
&&
semanticsLabel
!=
null
)),
super
(
style:
style
);
/// The text contained in this span.
...
...
@@ -82,7 +87,6 @@ class TextSpan extends InlineSpan {
/// This getter does not include the contents of its children.
final
String
?
text
;
/// Additional spans to include as children.
///
/// If both [text] and [children] are non-null, the text will precede the
...
...
@@ -115,7 +119,8 @@ class TextSpan extends InlineSpan {
/// provided to an [InlineSpan] object. It defines a `BuzzingText` widget
/// which uses the [HapticFeedback] class to vibrate the device when the user
/// long-presses the "find the" span, which is underlined in wavy green. The
/// hit-testing is handled by the [RichText] widget.
/// hit-testing is handled by the [RichText] widget. It also changes the
/// hovering mouse cursor to `precise`.
///
/// ```dart
/// class BuzzingText extends StatefulWidget {
...
...
@@ -160,6 +165,7 @@ class TextSpan extends InlineSpan {
/// decorationStyle: TextDecorationStyle.wavy,
/// ),
/// recognizer: _longPressRecognizer,
/// mouseCursor: SystemMouseCursors.precise,
/// ),
/// const TextSpan(
/// text: ' secret?',
...
...
@@ -173,6 +179,32 @@ class TextSpan extends InlineSpan {
/// {@end-tool}
final
GestureRecognizer
?
recognizer
;
/// Mouse cursor when the mouse hovers over this span.
///
/// The default value is [SystemMouseCursors.click] if [recognizer] is not
/// null, or [MouseCursor.defer] otherwise.
///
/// [TextSpan] itself does not implement hit testing or cursor changing.
/// The object that manages the [TextSpan] painting is responsible
/// to return the [TextSpan] in its hit test, as well as providing the
/// correct mouse cursor when the [TextSpan]'s mouse cursor is
/// [MouseCursor.defer].
final
MouseCursor
mouseCursor
;
@override
final
PointerEnterEventListener
?
onEnter
;
@override
final
PointerExitEventListener
?
onExit
;
/// Returns the value of [mouseCursor].
///
/// This field, required by [MouseTrackerAnnotation], is hidden publicly to
/// avoid the confusion as a text cursor.
@protected
@override
MouseCursor
get
cursor
=>
mouseCursor
;
/// An alternative semantics label for this [TextSpan].
///
/// If present, the semantics of this span will contain this value instead
...
...
@@ -186,6 +218,15 @@ class TextSpan extends InlineSpan {
/// ```
final
String
?
semanticsLabel
;
@override
bool
get
validForMouseTracker
=>
true
;
@override
void
handleEvent
(
PointerEvent
event
,
HitTestEntry
entry
)
{
if
(
event
is
PointerDownEvent
)
recognizer
?.
addPointer
(
event
);
}
/// Apply the [style], [text], and [children] of this object to the
/// given [ParagraphBuilder], from which a [Paragraph] can be obtained.
/// [Paragraph] objects can be drawn on [Canvas] objects.
...
...
@@ -405,6 +446,9 @@ class TextSpan extends InlineSpan {
&&
other
.
text
==
text
&&
other
.
recognizer
==
recognizer
&&
other
.
semanticsLabel
==
semanticsLabel
&&
onEnter
==
other
.
onEnter
&&
onExit
==
other
.
onExit
&&
mouseCursor
==
other
.
mouseCursor
&&
listEquals
<
InlineSpan
>(
other
.
children
,
children
);
}
...
...
@@ -414,6 +458,9 @@ class TextSpan extends InlineSpan {
text
,
recognizer
,
semanticsLabel
,
onEnter
,
onExit
,
mouseCursor
,
hashList
(
children
),
);
...
...
@@ -441,6 +488,15 @@ class TextSpan extends InlineSpan {
defaultValue:
null
,
));
properties
.
add
(
FlagsSummary
<
Function
?>(
'callbacks'
,
<
String
,
Function
?>
{
'enter'
:
onEnter
,
'exit'
:
onExit
,
},
));
properties
.
add
(
DiagnosticsProperty
<
MouseCursor
>(
'mouseCursor'
,
cursor
,
defaultValue:
MouseCursor
.
defer
));
if
(
semanticsLabel
!=
null
)
{
properties
.
add
(
StringProperty
(
'semanticsLabel'
,
semanticsLabel
));
}
...
...
packages/flutter/lib/src/rendering/editable.dart
View file @
5647407f
...
...
@@ -2834,6 +2834,18 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
@override
bool
hitTestSelf
(
Offset
position
)
=>
true
;
@override
@protected
bool
hitTestChildren
(
BoxHitTestResult
result
,
{
required
Offset
position
})
{
final
TextPosition
textPosition
=
_textPainter
.
getPositionForOffset
(
position
);
final
InlineSpan
?
span
=
_textPainter
.
text
!.
getSpanForPosition
(
textPosition
);
if
(
span
!=
null
&&
span
is
HitTestTarget
)
{
result
.
add
(
HitTestEntry
(
span
as
HitTestTarget
));
return
true
;
}
return
false
;
}
late
TapGestureRecognizer
_tap
;
late
LongPressGestureRecognizer
_longPress
;
...
...
@@ -2842,14 +2854,6 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
assert
(
debugHandleEvent
(
event
,
entry
));
if
(
event
is
PointerDownEvent
)
{
assert
(!
debugNeedsLayout
);
// Checks if there is any gesture recognizer in the text span.
final
Offset
offset
=
entry
.
localPosition
;
final
TextPosition
position
=
_textPainter
.
getPositionForOffset
(
offset
);
final
InlineSpan
?
span
=
_textPainter
.
text
!.
getSpanForPosition
(
position
);
if
(
span
!=
null
&&
span
is
TextSpan
)
{
final
TextSpan
textSpan
=
span
;
textSpan
.
recognizer
?.
addPointer
(
event
);
}
if
(!
ignorePointer
)
{
// Propagates the pointer event to selection handlers.
...
...
packages/flutter/lib/src/rendering/paragraph.dart
View file @
5647407f
...
...
@@ -449,6 +449,18 @@ class RenderParagraph extends RenderBox
@override
bool
hitTestChildren
(
BoxHitTestResult
result
,
{
required
Offset
position
})
{
// Hit test text spans.
late
final
bool
hitText
;
final
TextPosition
textPosition
=
_textPainter
.
getPositionForOffset
(
position
);
final
InlineSpan
?
span
=
_textPainter
.
text
!.
getSpanForPosition
(
textPosition
);
if
(
span
!=
null
&&
span
is
HitTestTarget
)
{
result
.
add
(
HitTestEntry
(
span
as
HitTestTarget
));
hitText
=
true
;
}
else
{
hitText
=
false
;
}
// Hit test render object children
RenderBox
?
child
=
firstChild
;
int
childIndex
=
0
;
while
(
child
!=
null
&&
childIndex
<
_textPainter
.
inlinePlaceholderBoxes
!.
length
)
{
...
...
@@ -480,24 +492,7 @@ class RenderParagraph extends RenderBox
child
=
childAfter
(
child
);
childIndex
+=
1
;
}
return
false
;
}
@override
void
handleEvent
(
PointerEvent
event
,
BoxHitTestEntry
entry
)
{
assert
(
debugHandleEvent
(
event
,
entry
));
if
(
event
is
!
PointerDownEvent
)
return
;
_layoutTextWithConstraints
(
constraints
);
final
Offset
offset
=
entry
.
localPosition
;
final
TextPosition
position
=
_textPainter
.
getPositionForOffset
(
offset
);
final
InlineSpan
?
span
=
_textPainter
.
text
!.
getSpanForPosition
(
position
);
if
(
span
==
null
)
{
return
;
}
if
(
span
is
TextSpan
)
{
span
.
recognizer
?.
addPointer
(
event
);
}
return
hitText
;
}
bool
_needsClipping
=
false
;
...
...
packages/flutter/test/painting/text_span_test.dart
View file @
5647407f
...
...
@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'package:flutter/gestures.dart'
;
import
'package:flutter/rendering.dart'
;
import
'package:flutter/widgets.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
...
...
@@ -25,6 +27,21 @@ void main() {
expect
(
a1
==
c2
,
isFalse
);
expect
(
b1
==
a2
,
isFalse
);
expect
(
c1
==
b2
,
isFalse
);
void
callback1
(
PointerEnterEvent
_
)
{}
void
callback2
(
PointerEnterEvent
_
)
{}
final
TextSpan
d1
=
TextSpan
(
text:
'a'
,
onEnter:
callback1
);
final
TextSpan
d2
=
TextSpan
(
text:
'a'
,
onEnter:
callback1
);
final
TextSpan
d3
=
TextSpan
(
text:
'a'
,
onEnter:
callback2
);
final
TextSpan
e1
=
TextSpan
(
text:
'a'
,
onEnter:
callback2
,
mouseCursor:
SystemMouseCursors
.
forbidden
);
final
TextSpan
e2
=
TextSpan
(
text:
'a'
,
onEnter:
callback2
,
mouseCursor:
SystemMouseCursors
.
forbidden
);
expect
(
a1
==
d1
,
isFalse
);
expect
(
d1
==
d2
,
isTrue
);
expect
(
d2
==
d3
,
isFalse
);
expect
(
d3
==
e1
,
isFalse
);
expect
(
e1
==
e2
,
isTrue
);
});
test
(
'TextSpan toStringDeep'
,
()
{
...
...
@@ -59,6 +76,30 @@ void main() {
));
});
test
(
'TextSpan toStringDeep for mouse'
,
()
{
const
TextSpan
test1
=
TextSpan
(
text:
'a'
,
);
expect
(
test1
.
toStringDeep
(),
equals
(
'TextSpan:
\n
'
' "a"
\n
'
));
final
TextSpan
test2
=
TextSpan
(
text:
'a'
,
onEnter:
(
_
)
{},
onExit:
(
_
)
{},
mouseCursor:
SystemMouseCursors
.
forbidden
,
);
expect
(
test2
.
toStringDeep
(),
equals
(
'TextSpan:
\n
'
' "a"
\n
'
' callbacks: enter, exit
\n
'
' mouseCursor: SystemMouseCursor(forbidden)
\n
'
));
});
test
(
'TextSpan toPlainText'
,
()
{
const
TextSpan
textSpan
=
TextSpan
(
text:
'a'
,
...
...
@@ -238,4 +279,89 @@ void main() {
expect
(
collector
[
0
].
text
,
'aaa'
);
expect
(
collector
[
0
].
semanticsLabel
,
'bbb'
);
});
testWidgets
(
'handles mouse cursor'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
const
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
Center
(
child:
Text
.
rich
(
TextSpan
(
text:
'xxxxx'
,
children:
<
InlineSpan
>[
TextSpan
(
text:
'yyyyy'
,
mouseCursor:
SystemMouseCursors
.
forbidden
,
),
TextSpan
(
text:
'xxxxx'
,
),
],
),
textAlign:
TextAlign
.
center
,
),
),
),
);
final
TestGesture
gesture
=
await
tester
.
createGesture
(
kind:
PointerDeviceKind
.
mouse
);
await
gesture
.
addPointer
();
addTearDown
(
gesture
.
removePointer
);
await
gesture
.
moveTo
(
tester
.
getCenter
(
find
.
byType
(
RichText
))
-
const
Offset
(
40
,
0
));
expect
(
RendererBinding
.
instance
!.
mouseTracker
.
debugDeviceActiveCursor
(
1
),
SystemMouseCursors
.
basic
);
await
gesture
.
moveTo
(
tester
.
getCenter
(
find
.
byType
(
RichText
)));
expect
(
RendererBinding
.
instance
!.
mouseTracker
.
debugDeviceActiveCursor
(
1
),
SystemMouseCursors
.
forbidden
);
await
gesture
.
moveTo
(
tester
.
getCenter
(
find
.
byType
(
RichText
))
+
const
Offset
(
40
,
0
));
expect
(
RendererBinding
.
instance
!.
mouseTracker
.
debugDeviceActiveCursor
(
1
),
SystemMouseCursors
.
basic
);
});
testWidgets
(
'handles onEnter and onExit'
,
(
WidgetTester
tester
)
async
{
final
List
<
PointerEvent
>
logEvents
=
<
PointerEvent
>[];
await
tester
.
pumpWidget
(
Directionality
(
textDirection:
TextDirection
.
ltr
,
child:
Center
(
child:
Text
.
rich
(
TextSpan
(
text:
'xxxxx'
,
children:
<
InlineSpan
>[
TextSpan
(
text:
'yyyyy'
,
onEnter:
(
PointerEnterEvent
event
)
{
logEvents
.
add
(
event
);
},
onExit:
(
PointerExitEvent
event
)
{
logEvents
.
add
(
event
);
}
),
const
TextSpan
(
text:
'xxxxx'
,
),
],
),
textAlign:
TextAlign
.
center
,
),
),
),
);
final
TestGesture
gesture
=
await
tester
.
createGesture
(
kind:
PointerDeviceKind
.
mouse
);
await
gesture
.
addPointer
();
addTearDown
(
gesture
.
removePointer
);
await
gesture
.
moveTo
(
tester
.
getCenter
(
find
.
byType
(
RichText
))
-
const
Offset
(
40
,
0
));
expect
(
logEvents
,
isEmpty
);
await
gesture
.
moveTo
(
tester
.
getCenter
(
find
.
byType
(
RichText
)));
expect
(
logEvents
.
length
,
1
);
expect
(
logEvents
[
0
],
isA
<
PointerEnterEvent
>());
await
gesture
.
moveTo
(
tester
.
getCenter
(
find
.
byType
(
RichText
))
+
const
Offset
(
40
,
0
));
expect
(
logEvents
.
length
,
2
);
expect
(
logEvents
[
1
],
isA
<
PointerExitEvent
>());
});
}
packages/flutter/test/widgets/slivers_padding_test.dart
View file @
5647407f
...
...
@@ -178,15 +178,15 @@ void main() {
]);
HitTestResult
result
;
result
=
tester
.
hitTestOnBinding
(
const
Offset
(
10.0
,
10.0
));
expect
(
result
.
path
.
first
.
target
,
tester
.
firstRenderObject
<
RenderObject
>(
find
.
byType
(
Text
))
);
expect
IsTextSpan
(
result
.
path
.
first
.
target
,
'before'
);
result
=
tester
.
hitTestOnBinding
(
const
Offset
(
10.0
,
60.0
));
expect
(
result
.
path
.
first
.
target
,
isA
<
RenderView
>());
result
=
tester
.
hitTestOnBinding
(
const
Offset
(
100.0
,
100.0
));
expect
(
result
.
path
.
first
.
target
,
tester
.
renderObjectList
<
RenderObject
>(
find
.
byType
(
Text
)).
skip
(
1
).
first
);
expect
IsTextSpan
(
result
.
path
.
first
.
target
,
'padded'
);
result
=
tester
.
hitTestOnBinding
(
const
Offset
(
100.0
,
490.0
));
expect
(
result
.
path
.
first
.
target
,
isA
<
RenderView
>());
result
=
tester
.
hitTestOnBinding
(
const
Offset
(
10.0
,
520.0
));
expect
(
result
.
path
.
first
.
target
,
tester
.
renderObjectList
<
RenderObject
>(
find
.
byType
(
Text
)).
last
);
expect
IsTextSpan
(
result
.
path
.
first
.
target
,
'after'
);
});
testWidgets
(
'Viewport+SliverPadding hit testing up'
,
(
WidgetTester
tester
)
async
{
...
...
@@ -200,15 +200,15 @@ void main() {
]);
HitTestResult
result
;
result
=
tester
.
hitTestOnBinding
(
const
Offset
(
10.0
,
600.0
-
10.0
));
expect
(
result
.
path
.
first
.
target
,
tester
.
firstRenderObject
<
RenderObject
>(
find
.
byType
(
Text
))
);
expect
IsTextSpan
(
result
.
path
.
first
.
target
,
'before'
);
result
=
tester
.
hitTestOnBinding
(
const
Offset
(
10.0
,
600.0
-
60.0
));
expect
(
result
.
path
.
first
.
target
,
isA
<
RenderView
>());
result
=
tester
.
hitTestOnBinding
(
const
Offset
(
100.0
,
600.0
-
100.0
));
expect
(
result
.
path
.
first
.
target
,
tester
.
renderObjectList
<
RenderObject
>(
find
.
byType
(
Text
)).
skip
(
1
).
first
);
expect
IsTextSpan
(
result
.
path
.
first
.
target
,
'padded'
);
result
=
tester
.
hitTestOnBinding
(
const
Offset
(
100.0
,
600.0
-
490.0
));
expect
(
result
.
path
.
first
.
target
,
isA
<
RenderView
>());
result
=
tester
.
hitTestOnBinding
(
const
Offset
(
10.0
,
600.0
-
520.0
));
expect
(
result
.
path
.
first
.
target
,
tester
.
renderObjectList
<
RenderObject
>(
find
.
byType
(
Text
)).
last
);
expect
IsTextSpan
(
result
.
path
.
first
.
target
,
'after'
);
});
testWidgets
(
'Viewport+SliverPadding hit testing left'
,
(
WidgetTester
tester
)
async
{
...
...
@@ -222,15 +222,15 @@ void main() {
]);
HitTestResult
result
;
result
=
tester
.
hitTestOnBinding
(
const
Offset
(
800.0
-
10.0
,
10.0
));
expect
(
result
.
path
.
first
.
target
,
tester
.
firstRenderObject
<
RenderObject
>(
find
.
byType
(
Text
))
);
expect
IsTextSpan
(
result
.
path
.
first
.
target
,
'before'
);
result
=
tester
.
hitTestOnBinding
(
const
Offset
(
800.0
-
60.0
,
10.0
));
expect
(
result
.
path
.
first
.
target
,
isA
<
RenderView
>());
result
=
tester
.
hitTestOnBinding
(
const
Offset
(
800.0
-
100.0
,
100.0
));
expect
(
result
.
path
.
first
.
target
,
tester
.
renderObjectList
<
RenderObject
>(
find
.
byType
(
Text
)).
skip
(
1
).
first
);
expect
IsTextSpan
(
result
.
path
.
first
.
target
,
'padded'
);
result
=
tester
.
hitTestOnBinding
(
const
Offset
(
800.0
-
490.0
,
100.0
));
expect
(
result
.
path
.
first
.
target
,
isA
<
RenderView
>());
result
=
tester
.
hitTestOnBinding
(
const
Offset
(
800.0
-
520.0
,
10.0
));
expect
(
result
.
path
.
first
.
target
,
tester
.
renderObjectList
<
RenderObject
>(
find
.
byType
(
Text
)).
last
);
expect
IsTextSpan
(
result
.
path
.
first
.
target
,
'after'
);
});
testWidgets
(
'Viewport+SliverPadding hit testing right'
,
(
WidgetTester
tester
)
async
{
...
...
@@ -244,15 +244,15 @@ void main() {
]);
HitTestResult
result
;
result
=
tester
.
hitTestOnBinding
(
const
Offset
(
10.0
,
10.0
));
expect
(
result
.
path
.
first
.
target
,
tester
.
firstRenderObject
<
RenderObject
>(
find
.
byType
(
Text
))
);
expect
IsTextSpan
(
result
.
path
.
first
.
target
,
'before'
);
result
=
tester
.
hitTestOnBinding
(
const
Offset
(
60.0
,
10.0
));
expect
(
result
.
path
.
first
.
target
,
isA
<
RenderView
>());
result
=
tester
.
hitTestOnBinding
(
const
Offset
(
100.0
,
100.0
));
expect
(
result
.
path
.
first
.
target
,
tester
.
renderObjectList
<
RenderObject
>(
find
.
byType
(
Text
)).
skip
(
1
).
first
);
expect
IsTextSpan
(
result
.
path
.
first
.
target
,
'padded'
);
result
=
tester
.
hitTestOnBinding
(
const
Offset
(
490.0
,
100.0
));
expect
(
result
.
path
.
first
.
target
,
isA
<
RenderView
>());
result
=
tester
.
hitTestOnBinding
(
const
Offset
(
520.0
,
10.0
));
expect
(
result
.
path
.
first
.
target
,
tester
.
renderObjectList
<
RenderObject
>(
find
.
byType
(
Text
)).
last
);
expect
IsTextSpan
(
result
.
path
.
first
.
target
,
'after'
);
});
testWidgets
(
'Viewport+SliverPadding no child'
,
(
WidgetTester
tester
)
async
{
...
...
@@ -544,3 +544,8 @@ void main() {
expect
(
renderObject
.
geometry
!.
paintOrigin
,
10.0
);
});
}
void
expectIsTextSpan
(
Object
target
,
String
text
)
{
expect
(
target
,
isA
<
TextSpan
>());
expect
((
target
as
TextSpan
).
text
,
text
);
}
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