Floating cursor cleanup (#116746)
* Floating cursor cleanup * Use TextSelection.fromPosition
This commit is contained in:
parent
cbdc763cfd
commit
c4b8046d96
@ -3102,7 +3102,7 @@ class _FloatingCursorPainter extends RenderEditablePainter {
|
||||
}
|
||||
|
||||
canvas.drawRRect(
|
||||
RRect.fromRectAndRadius(floatingCursorRect.shift(renderEditable._paintOffset), _kFloatingCaretRadius),
|
||||
RRect.fromRectAndRadius(floatingCursorRect, _kFloatingCaretRadius),
|
||||
floatingCursorPaint..color = floatingCursorColor,
|
||||
);
|
||||
}
|
||||
|
@ -2671,7 +2671,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
// we cache the position.
|
||||
_pointOffsetOrigin = point.offset;
|
||||
|
||||
final TextPosition currentTextPosition = TextPosition(offset: renderEditable.selection!.baseOffset);
|
||||
final TextPosition currentTextPosition = TextPosition(offset: renderEditable.selection!.baseOffset, affinity: renderEditable.selection!.affinity);
|
||||
_startCaretRect = renderEditable.getLocalRectForCaret(currentTextPosition);
|
||||
|
||||
_lastBoundedOffset = _startCaretRect!.center - _floatingCursorOffset;
|
||||
@ -2702,9 +2702,11 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
||||
final Offset finalPosition = renderEditable.getLocalRectForCaret(_lastTextPosition!).centerLeft - _floatingCursorOffset;
|
||||
if (_floatingCursorResetController!.isCompleted) {
|
||||
renderEditable.setFloatingCursor(FloatingCursorDragState.End, finalPosition, _lastTextPosition!);
|
||||
if (_lastTextPosition!.offset != renderEditable.selection!.baseOffset) {
|
||||
// Only change if new position is out of current selection range, as the
|
||||
// selection may have been modified using the iOS keyboard selection gesture.
|
||||
if (_lastTextPosition!.offset < renderEditable.selection!.start || _lastTextPosition!.offset >= renderEditable.selection!.end) {
|
||||
// The cause is technically the force cursor, but the cause is listed as tap as the desired functionality is the same.
|
||||
_handleSelectionChanged(TextSelection.collapsed(offset: _lastTextPosition!.offset), SelectionChangedCause.forcePress);
|
||||
_handleSelectionChanged(TextSelection.fromPosition(_lastTextPosition!), SelectionChangedCause.forcePress);
|
||||
}
|
||||
_startCaretRect = null;
|
||||
_lastTextPosition = null;
|
||||
|
@ -12,6 +12,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/src/services/text_input.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'mock_canvas.dart';
|
||||
@ -1725,6 +1726,79 @@ void main() {
|
||||
editable.forceLine = false;
|
||||
expect(editable.computeDryLayout(constraints).width, lessThan(initialWidth));
|
||||
});
|
||||
|
||||
test('Floating cursor position is independent of viewport offset', () {
|
||||
final TextSelectionDelegate delegate = _FakeEditableTextState();
|
||||
final ValueNotifier<bool> showCursor = ValueNotifier<bool>(true);
|
||||
EditableText.debugDeterministicCursor = true;
|
||||
|
||||
const Color cursorColor = Color.fromARGB(0xFF, 0xFF, 0x00, 0x00);
|
||||
|
||||
final RenderEditable editable = RenderEditable(
|
||||
backgroundCursorColor: Colors.grey,
|
||||
textDirection: TextDirection.ltr,
|
||||
cursorColor: cursorColor,
|
||||
offset: ViewportOffset.zero(),
|
||||
textSelectionDelegate: delegate,
|
||||
text: const TextSpan(
|
||||
text: 'test',
|
||||
style: TextStyle(
|
||||
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||||
),
|
||||
),
|
||||
maxLines: 3,
|
||||
startHandleLayerLink: LayerLink(),
|
||||
endHandleLayerLink: LayerLink(),
|
||||
selection: const TextSelection.collapsed(
|
||||
offset: 4,
|
||||
affinity: TextAffinity.upstream,
|
||||
),
|
||||
);
|
||||
|
||||
layout(editable);
|
||||
|
||||
editable.layout(BoxConstraints.loose(const Size(100, 100)));
|
||||
// Prepare for painting after layout.
|
||||
pumpFrame(phase: EnginePhase.compositingBits);
|
||||
|
||||
expect(
|
||||
editable,
|
||||
// Draw no cursor by default.
|
||||
paintsExactlyCountTimes(#drawRect, 0),
|
||||
);
|
||||
|
||||
editable.showCursor = showCursor;
|
||||
editable.setFloatingCursor(FloatingCursorDragState.Start, const Offset(50, 50), const TextPosition(
|
||||
offset: 4,
|
||||
affinity: TextAffinity.upstream,
|
||||
));
|
||||
pumpFrame(phase: EnginePhase.compositingBits);
|
||||
|
||||
final RRect expectedRRect = RRect.fromRectAndRadius(
|
||||
const Rect.fromLTWH(49.5, 51, 2, 8),
|
||||
const Radius.circular(1)
|
||||
);
|
||||
|
||||
expect(editable, paints..rrect(
|
||||
color: cursorColor.withOpacity(0.75),
|
||||
rrect: expectedRRect
|
||||
));
|
||||
|
||||
// Change the text viewport offset.
|
||||
editable.offset = ViewportOffset.fixed(200);
|
||||
|
||||
// Floating cursor should be drawn in the same position.
|
||||
editable.setFloatingCursor(FloatingCursorDragState.Start, const Offset(50, 50), const TextPosition(
|
||||
offset: 4,
|
||||
affinity: TextAffinity.upstream,
|
||||
));
|
||||
pumpFrame(phase: EnginePhase.compositingBits);
|
||||
|
||||
expect(editable, paints..rrect(
|
||||
color: cursorColor.withOpacity(0.75),
|
||||
rrect: expectedRRect
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
class _TestRenderEditable extends RenderEditable {
|
||||
|
@ -11701,6 +11701,163 @@ void main() {
|
||||
expect(tester.hasRunningAnimations, isFalse);
|
||||
});
|
||||
|
||||
testWidgets('Floating cursor affinity', (WidgetTester tester) async {
|
||||
EditableText.debugDeterministicCursor = true;
|
||||
final FocusNode focusNode = FocusNode();
|
||||
final GlobalKey key = GlobalKey();
|
||||
// Set it up so that there will be word-wrap.
|
||||
final TextEditingController controller = TextEditingController(text: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz');
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 500,
|
||||
),
|
||||
child: EditableText(
|
||||
key: key,
|
||||
autofocus: true,
|
||||
maxLines: 2,
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
style: textStyle,
|
||||
cursorColor: Colors.blue,
|
||||
backgroundCursorColor: Colors.grey,
|
||||
cursorOpacityAnimates: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
final EditableTextState state = tester.state(find.byType(EditableText));
|
||||
|
||||
// Select after the first word, with default affinity (downstream).
|
||||
controller.selection = const TextSelection.collapsed(offset: 27);
|
||||
await tester.pump();
|
||||
state.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Start, offset: Offset.zero));
|
||||
await tester.pump();
|
||||
|
||||
// The floating cursor should be drawn at the end of the first line.
|
||||
expect(key.currentContext!.findRenderObject(), paints..rrect(
|
||||
rrect: RRect.fromRectAndRadius(
|
||||
const Rect.fromLTWH(0.5, 15, 3, 12),
|
||||
const Radius.circular(1)
|
||||
)
|
||||
));
|
||||
|
||||
// Select after the first word, with upstream affinity.
|
||||
controller.selection = const TextSelection.collapsed(offset: 27, affinity: TextAffinity.upstream);
|
||||
await tester.pump();
|
||||
|
||||
state.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Start, offset: Offset.zero));
|
||||
await tester.pump();
|
||||
|
||||
// The floating cursor should be drawn at the beginning of the second line.
|
||||
expect(key.currentContext!.findRenderObject(), paints..rrect(
|
||||
rrect: RRect.fromRectAndRadius(
|
||||
const Rect.fromLTWH(378.5, 1, 3, 12),
|
||||
const Radius.circular(1)
|
||||
)
|
||||
));
|
||||
|
||||
EditableText.debugDeterministicCursor = false;
|
||||
});
|
||||
|
||||
testWidgets('Floating cursor ending with selection', (WidgetTester tester) async {
|
||||
EditableText.debugDeterministicCursor = true;
|
||||
final FocusNode focusNode = FocusNode();
|
||||
final GlobalKey key = GlobalKey();
|
||||
// Set it up so that there will be word-wrap.
|
||||
final TextEditingController controller = TextEditingController(text: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ');
|
||||
controller.selection = const TextSelection.collapsed(offset: 0);
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: EditableText(
|
||||
key: key,
|
||||
autofocus: true,
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
style: textStyle,
|
||||
cursorColor: Colors.blue,
|
||||
backgroundCursorColor: Colors.grey,
|
||||
cursorOpacityAnimates: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
final EditableTextState state = tester.state(find.byType(EditableText));
|
||||
|
||||
state.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Start, offset: Offset.zero));
|
||||
await tester.pump();
|
||||
|
||||
// The floating cursor should be drawn at the start of the line.
|
||||
expect(key.currentContext!.findRenderObject(), paints..rrect(
|
||||
rrect: RRect.fromRectAndRadius(
|
||||
const Rect.fromLTWH(0.5, 1, 3, 12),
|
||||
const Radius.circular(1)
|
||||
)
|
||||
));
|
||||
|
||||
state.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Update, offset: const Offset(50, 0)));
|
||||
await tester.pump();
|
||||
|
||||
// The floating cursor should be drawn somewhere in the middle of the line
|
||||
expect(key.currentContext!.findRenderObject(), paints..rrect(
|
||||
rrect: RRect.fromRectAndRadius(
|
||||
const Rect.fromLTWH(50.5, 1, 3, 12),
|
||||
const Radius.circular(1)
|
||||
)
|
||||
));
|
||||
|
||||
state.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.End, offset: Offset.zero));
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 125)); // Floating cursor has an end animation.
|
||||
|
||||
// Selection should be updated based on the floating cursor location.
|
||||
expect(controller.selection.isCollapsed, true);
|
||||
expect(controller.selection.baseOffset, 4);
|
||||
|
||||
state.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Start, offset: Offset.zero));
|
||||
await tester.pump();
|
||||
|
||||
// The floating cursor should be drawn near to the previous position.
|
||||
// It's different because it's snapped to exactly between characters.
|
||||
expect(key.currentContext!.findRenderObject(), paints..rrect(
|
||||
rrect: RRect.fromRectAndRadius(
|
||||
const Rect.fromLTWH(56.5, 1, 3, 12),
|
||||
const Radius.circular(1)
|
||||
)
|
||||
));
|
||||
|
||||
state.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.Update, offset: const Offset(-56, 0)));
|
||||
await tester.pump();
|
||||
|
||||
// The floating cursor should be drawn at the start of the line.
|
||||
expect(key.currentContext!.findRenderObject(), paints..rrect(
|
||||
rrect: RRect.fromRectAndRadius(
|
||||
const Rect.fromLTWH(0.5, 1, 3, 12),
|
||||
const Radius.circular(1)
|
||||
)
|
||||
));
|
||||
|
||||
// Simulate UIKit setting the selection using keyboard selection.
|
||||
controller.selection = const TextSelection(baseOffset: 0, extentOffset: 4);
|
||||
await tester.pump();
|
||||
|
||||
state.updateFloatingCursor(RawFloatingCursorPoint(state: FloatingCursorDragState.End, offset: Offset.zero));
|
||||
await tester.pump();
|
||||
|
||||
// Selection should not be updated as the new position is within the selection range.
|
||||
expect(controller.selection.isCollapsed, false);
|
||||
expect(controller.selection.baseOffset, 0);
|
||||
expect(controller.selection.extentOffset, 4);
|
||||
|
||||
EditableText.debugDeterministicCursor = false;
|
||||
});
|
||||
|
||||
|
||||
group('Selection changed scroll into view', () {
|
||||
final String text = List<int>.generate(64, (int index) => index).join('\n');
|
||||
final TextEditingController controller = TextEditingController(text: text);
|
||||
|
Loading…
x
Reference in New Issue
Block a user