mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00

The behavior largely remains the same, except: 1. The EOT cursor `(textLength, downstream)` for text ending in the opposite writing direction as the paragraph is now placed at the visual end of the last line. For example, in a LTR paragraph, the EOT cursor for `aA` (lowercase for LTR and uppercase for RTL) is placed to the right of the line: `aA|` (it was `a|A` before). This matches the behavior of most applications that do logical order arrow key navigation instead of visual order navigation. And it makes the navigation order consistent for `aA\naA`: ``` |aA => aA| => aA| => aA => aA => aA aA aA aA |aA aA| aA| (1) (2) (3) (4) (5) (6) ``` This is indeed still pretty confusing as (2) and (3), as well as (5) and (6) are hard to distinguish (when the I beam has a large width they are actually visually distinguishable -- they use the same anchor but one gets painted to the left and the other to the right. I noticed that emacs does the same). But logical order navigation will always be confusing in bidi text, in one way or another. Interestingly there are 3 different behaviors I've observed in chrome: - the chrome download dialog (which I think uses GTK text widgets but not sure which version) gives me 2 cursors when navigating bidi text, and - its HTML fields only show one, and presumably they place the I beam at the **trailing edge** of the character (which makes more sense for backspacing I guess). - On the other hand, its (new) omnibar seems to use visual order arrow navigation Side note: we may need to update the "tap to place the caret here" logic to handle the case where the tap lands outside of the text and the text ends in the opposite writing direction. 2. Removed the logarithmic search. The same could be done using the characters package but when glyphInfo tells you about the baseline location in the future we probably don't need the `getBoxesForRange` call. This should fix https://github.com/flutter/flutter/issues/123424. ## Internal Tests This is going to change the image output of some internal golden tests. I'm planning to merge https://github.com/flutter/flutter/pull/143281 before this to avoid updating the same golden files twice for invalid selections.
112 lines
3.8 KiB
Dart
112 lines
3.8 KiB
Dart
// Copyright 2014 The Flutter Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
import 'package:flutter_api_samples/widgets/text_magnifier/text_magnifier.0.dart'
|
|
as example;
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
List<TextSelectionPoint> _globalize(
|
|
Iterable<TextSelectionPoint> points, RenderBox box) {
|
|
return points.map<TextSelectionPoint>((TextSelectionPoint point) {
|
|
return TextSelectionPoint(
|
|
box.localToGlobal(point.point),
|
|
point.direction,
|
|
);
|
|
}).toList();
|
|
}
|
|
|
|
RenderEditable _findRenderEditable<T extends State<StatefulWidget>>(WidgetTester tester) {
|
|
return (tester.state(find.byType(TextField))
|
|
as TextSelectionGestureDetectorBuilderDelegate)
|
|
.editableTextKey
|
|
.currentState!
|
|
.renderEditable;
|
|
}
|
|
|
|
Offset _textOffsetToPosition<T extends State<StatefulWidget>>(WidgetTester tester, int offset) {
|
|
final RenderEditable renderEditable = _findRenderEditable(tester);
|
|
|
|
final List<TextSelectionPoint> endpoints = renderEditable
|
|
.getEndpointsForSelection(
|
|
TextSelection.collapsed(offset: offset),
|
|
)
|
|
.map<TextSelectionPoint>((TextSelectionPoint point) => TextSelectionPoint(
|
|
renderEditable.localToGlobal(point.point),
|
|
point.direction,
|
|
))
|
|
.toList();
|
|
|
|
return endpoints[0].point + const Offset(0.0, -2.0);
|
|
}
|
|
|
|
void main() {
|
|
const Duration durationBetweenActions = Duration(milliseconds: 20);
|
|
const String defaultText = 'I am a magnifier, fear me!';
|
|
|
|
Future<void> showMagnifier(WidgetTester tester, int textOffset) async {
|
|
assert(textOffset >= 0);
|
|
final Offset tapOffset = _textOffsetToPosition(tester, textOffset);
|
|
|
|
// Double tap 'Magnifier' word to show the selection handles.
|
|
final TestGesture testGesture = await tester.startGesture(tapOffset);
|
|
await tester.pump(durationBetweenActions);
|
|
await testGesture.up();
|
|
await tester.pump(durationBetweenActions);
|
|
await testGesture.down(tapOffset);
|
|
await tester.pump(durationBetweenActions);
|
|
await testGesture.up();
|
|
await tester.pumpAndSettle();
|
|
|
|
final TextEditingController controller = tester
|
|
.firstWidget<TextField>(find.byType(TextField))
|
|
.controller!;
|
|
|
|
final TextSelection selection = controller.selection;
|
|
final RenderEditable renderEditable = _findRenderEditable(tester);
|
|
final List<TextSelectionPoint> endpoints = _globalize(
|
|
renderEditable.getEndpointsForSelection(selection),
|
|
renderEditable,
|
|
);
|
|
|
|
final Offset handlePos = endpoints.last.point + const Offset(10.0, 10.0);
|
|
|
|
final TestGesture gesture = await tester.startGesture(handlePos);
|
|
|
|
await gesture.moveTo(
|
|
_textOffsetToPosition(
|
|
tester,
|
|
defaultText.length - 2,
|
|
),
|
|
);
|
|
await tester.pump();
|
|
}
|
|
|
|
testWidgets('should show custom magnifier on drag', (WidgetTester tester) async {
|
|
await tester.pumpWidget(const example.TextMagnifierExampleApp(text: defaultText));
|
|
|
|
await showMagnifier(tester, defaultText.indexOf('e'));
|
|
expect(find.byType(example.CustomMagnifier), findsOneWidget);
|
|
|
|
await expectLater(
|
|
find.byType(example.TextMagnifierExampleApp),
|
|
matchesGoldenFile('text_magnifier.0_test.png'),
|
|
);
|
|
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.android }));
|
|
|
|
|
|
testWidgets('should show custom magnifier in RTL', (WidgetTester tester) async {
|
|
const String text = 'أثارت زر';
|
|
const String textToTapOn = 'ت';
|
|
|
|
await tester.pumpWidget(const example.TextMagnifierExampleApp(textDirection: TextDirection.rtl, text: text));
|
|
|
|
await showMagnifier(tester, text.indexOf(textToTapOn));
|
|
|
|
expect(find.byType(example.CustomMagnifier), findsOneWidget);
|
|
});
|
|
|
|
}
|