Unverified Commit c4b8046d authored by Callum Moffat's avatar Callum Moffat Committed by GitHub

Floating cursor cleanup (#116746)

* Floating cursor cleanup

* Use TextSelection.fromPosition
parent cbdc763c
......@@ -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);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment