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(
|
canvas.drawRRect(
|
||||||
RRect.fromRectAndRadius(floatingCursorRect.shift(renderEditable._paintOffset), _kFloatingCaretRadius),
|
RRect.fromRectAndRadius(floatingCursorRect, _kFloatingCaretRadius),
|
||||||
floatingCursorPaint..color = floatingCursorColor,
|
floatingCursorPaint..color = floatingCursorColor,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2671,7 +2671,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
// we cache the position.
|
// we cache the position.
|
||||||
_pointOffsetOrigin = point.offset;
|
_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);
|
_startCaretRect = renderEditable.getLocalRectForCaret(currentTextPosition);
|
||||||
|
|
||||||
_lastBoundedOffset = _startCaretRect!.center - _floatingCursorOffset;
|
_lastBoundedOffset = _startCaretRect!.center - _floatingCursorOffset;
|
||||||
@ -2702,9 +2702,11 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
final Offset finalPosition = renderEditable.getLocalRectForCaret(_lastTextPosition!).centerLeft - _floatingCursorOffset;
|
final Offset finalPosition = renderEditable.getLocalRectForCaret(_lastTextPosition!).centerLeft - _floatingCursorOffset;
|
||||||
if (_floatingCursorResetController!.isCompleted) {
|
if (_floatingCursorResetController!.isCompleted) {
|
||||||
renderEditable.setFloatingCursor(FloatingCursorDragState.End, finalPosition, _lastTextPosition!);
|
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.
|
// 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;
|
_startCaretRect = null;
|
||||||
_lastTextPosition = null;
|
_lastTextPosition = null;
|
||||||
|
@ -12,6 +12,7 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
|
import 'package:flutter/src/services/text_input.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
import 'mock_canvas.dart';
|
import 'mock_canvas.dart';
|
||||||
@ -1725,6 +1726,79 @@ void main() {
|
|||||||
editable.forceLine = false;
|
editable.forceLine = false;
|
||||||
expect(editable.computeDryLayout(constraints).width, lessThan(initialWidth));
|
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 {
|
class _TestRenderEditable extends RenderEditable {
|
||||||
|
@ -11701,6 +11701,163 @@ void main() {
|
|||||||
expect(tester.hasRunningAnimations, isFalse);
|
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', () {
|
group('Selection changed scroll into view', () {
|
||||||
final String text = List<int>.generate(64, (int index) => index).join('\n');
|
final String text = List<int>.generate(64, (int index) => index).join('\n');
|
||||||
final TextEditingController controller = TextEditingController(text: text);
|
final TextEditingController controller = TextEditingController(text: text);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user