Unverified Commit f014c1e6 authored by Anthony Oleinik's avatar Anthony Oleinik Committed by GitHub

Loupe Android + iOS (#107477)

* added Magnifier for iOS and Android
parent 5bf6ac18
...@@ -41,6 +41,7 @@ export 'src/cupertino/interface_level.dart'; ...@@ -41,6 +41,7 @@ export 'src/cupertino/interface_level.dart';
export 'src/cupertino/list_section.dart'; export 'src/cupertino/list_section.dart';
export 'src/cupertino/list_tile.dart'; export 'src/cupertino/list_tile.dart';
export 'src/cupertino/localizations.dart'; export 'src/cupertino/localizations.dart';
export 'src/cupertino/magnifier.dart';
export 'src/cupertino/nav_bar.dart'; export 'src/cupertino/nav_bar.dart';
export 'src/cupertino/page_scaffold.dart'; export 'src/cupertino/page_scaffold.dart';
export 'src/cupertino/picker.dart'; export 'src/cupertino/picker.dart';
......
...@@ -102,6 +102,7 @@ export 'src/material/input_date_picker_form_field.dart'; ...@@ -102,6 +102,7 @@ export 'src/material/input_date_picker_form_field.dart';
export 'src/material/input_decorator.dart'; export 'src/material/input_decorator.dart';
export 'src/material/list_tile.dart'; export 'src/material/list_tile.dart';
export 'src/material/list_tile_theme.dart'; export 'src/material/list_tile_theme.dart';
export 'src/material/magnifier.dart';
export 'src/material/material.dart'; export 'src/material/material.dart';
export 'src/material/material_button.dart'; export 'src/material/material_button.dart';
export 'src/material/material_localizations.dart'; export 'src/material/material_localizations.dart';
......
This diff is collapsed.
...@@ -13,6 +13,7 @@ import 'package:flutter/widgets.dart'; ...@@ -13,6 +13,7 @@ import 'package:flutter/widgets.dart';
import 'colors.dart'; import 'colors.dart';
import 'desktop_text_selection.dart'; import 'desktop_text_selection.dart';
import 'icons.dart'; import 'icons.dart';
import 'magnifier.dart';
import 'text_selection.dart'; import 'text_selection.dart';
import 'theme.dart'; import 'theme.dart';
...@@ -273,6 +274,7 @@ class CupertinoTextField extends StatefulWidget { ...@@ -273,6 +274,7 @@ class CupertinoTextField extends StatefulWidget {
this.restorationId, this.restorationId,
this.scribbleEnabled = true, this.scribbleEnabled = true,
this.enableIMEPersonalizedLearning = true, this.enableIMEPersonalizedLearning = true,
this.magnifierConfiguration,
}) : assert(textAlign != null), }) : assert(textAlign != null),
assert(readOnly != null), assert(readOnly != null),
assert(autofocus != null), assert(autofocus != null),
...@@ -434,6 +436,7 @@ class CupertinoTextField extends StatefulWidget { ...@@ -434,6 +436,7 @@ class CupertinoTextField extends StatefulWidget {
this.restorationId, this.restorationId,
this.scribbleEnabled = true, this.scribbleEnabled = true,
this.enableIMEPersonalizedLearning = true, this.enableIMEPersonalizedLearning = true,
this.magnifierConfiguration,
}) : assert(textAlign != null), }) : assert(textAlign != null),
assert(readOnly != null), assert(readOnly != null),
assert(autofocus != null), assert(autofocus != null),
...@@ -783,6 +786,21 @@ class CupertinoTextField extends StatefulWidget { ...@@ -783,6 +786,21 @@ class CupertinoTextField extends StatefulWidget {
/// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning} /// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning}
final bool enableIMEPersonalizedLearning; final bool enableIMEPersonalizedLearning;
/// {@macro flutter.widgets.text_selection.TextMagnifierConfiguration.intro}
///
/// {@macro flutter.widgets.magnifier.intro}
///
/// {@macro flutter.widgets.text_selection.TextMagnifierConfiguration.details}
///
/// By default, builds a [CupertinoTextMagnifier] on iOS and Android nothing on all other
/// platforms. If it is desired to supress the magnifier, consider passing
/// [TextMagnifierConfiguration.disabled].
///
// TODO(antholeole): https://github.com/flutter/flutter/issues/108041
// once the magnifier PR lands, I should enrich this area of the
// docs with images of what a magnifier is.
final TextMagnifierConfiguration? magnifierConfiguration;
@override @override
State<CupertinoTextField> createState() => _CupertinoTextFieldState(); State<CupertinoTextField> createState() => _CupertinoTextFieldState();
...@@ -827,6 +845,27 @@ class CupertinoTextField extends StatefulWidget { ...@@ -827,6 +845,27 @@ class CupertinoTextField extends StatefulWidget {
properties.add(DiagnosticsProperty<bool>('scribbleEnabled', scribbleEnabled, defaultValue: true)); properties.add(DiagnosticsProperty<bool>('scribbleEnabled', scribbleEnabled, defaultValue: true));
properties.add(DiagnosticsProperty<bool>('enableIMEPersonalizedLearning', enableIMEPersonalizedLearning, defaultValue: true)); properties.add(DiagnosticsProperty<bool>('enableIMEPersonalizedLearning', enableIMEPersonalizedLearning, defaultValue: true));
} }
static final TextMagnifierConfiguration _iosMagnifierConfiguration = TextMagnifierConfiguration(
magnifierBuilder: (
BuildContext context,
MagnifierController controller,
ValueNotifier<MagnifierOverlayInfoBearer> magnifierOverlayInfoBearer
) {
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.iOS:
return CupertinoTextMagnifier(
controller: controller,
magnifierOverlayInfoBearer: magnifierOverlayInfoBearer,
);
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
return null;
}
});
} }
class _CupertinoTextFieldState extends State<CupertinoTextField> with RestorationMixin, AutomaticKeepAliveClientMixin<CupertinoTextField> implements TextSelectionGestureDetectorBuilderDelegate, AutofillClient { class _CupertinoTextFieldState extends State<CupertinoTextField> with RestorationMixin, AutomaticKeepAliveClientMixin<CupertinoTextField> implements TextSelectionGestureDetectorBuilderDelegate, AutofillClient {
...@@ -1274,6 +1313,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio ...@@ -1274,6 +1313,7 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
maxLines: widget.maxLines, maxLines: widget.maxLines,
minLines: widget.minLines, minLines: widget.minLines,
expands: widget.expands, expands: widget.expands,
magnifierConfiguration: widget.magnifierConfiguration ?? CupertinoTextField._iosMagnifierConfiguration,
// Only show the selection highlight when the text field is focused. // Only show the selection highlight when the text field is focused.
selectionColor: _effectiveFocusNode.hasFocus ? selectionColor : null, selectionColor: _effectiveFocusNode.hasFocus ? selectionColor : null,
selectionControls: widget.selectionEnabled selectionControls: widget.selectionEnabled
......
This diff is collapsed.
...@@ -11,6 +11,7 @@ import 'package:flutter/rendering.dart'; ...@@ -11,6 +11,7 @@ import 'package:flutter/rendering.dart';
import 'desktop_text_selection.dart'; import 'desktop_text_selection.dart';
import 'feedback.dart'; import 'feedback.dart';
import 'magnifier.dart';
import 'text_selection.dart'; import 'text_selection.dart';
import 'theme.dart'; import 'theme.dart';
...@@ -203,6 +204,7 @@ class SelectableText extends StatefulWidget { ...@@ -203,6 +204,7 @@ class SelectableText extends StatefulWidget {
this.textHeightBehavior, this.textHeightBehavior,
this.textWidthBasis, this.textWidthBasis,
this.onSelectionChanged, this.onSelectionChanged,
this.magnifierConfiguration,
}) : assert(showCursor != null), }) : assert(showCursor != null),
assert(autofocus != null), assert(autofocus != null),
assert(dragStartBehavior != null), assert(dragStartBehavior != null),
...@@ -260,6 +262,7 @@ class SelectableText extends StatefulWidget { ...@@ -260,6 +262,7 @@ class SelectableText extends StatefulWidget {
this.textHeightBehavior, this.textHeightBehavior,
this.textWidthBasis, this.textWidthBasis,
this.onSelectionChanged, this.onSelectionChanged,
this.magnifierConfiguration,
}) : assert(showCursor != null), }) : assert(showCursor != null),
assert(autofocus != null), assert(autofocus != null),
assert(dragStartBehavior != null), assert(dragStartBehavior != null),
...@@ -427,6 +430,17 @@ class SelectableText extends StatefulWidget { ...@@ -427,6 +430,17 @@ class SelectableText extends StatefulWidget {
/// {@macro flutter.widgets.editableText.onSelectionChanged} /// {@macro flutter.widgets.editableText.onSelectionChanged}
final SelectionChangedCallback? onSelectionChanged; final SelectionChangedCallback? onSelectionChanged;
/// {@macro flutter.widgets.text_selection.TextMagnifierConfiguration.intro}
///
/// {@macro flutter.widgets.magnifier.intro}
///
/// {@macro flutter.widgets.text_selection.TextMagnifierConfiguration.details}
///
/// By default, builds a [CupertinoTextMagnifier] on iOS and [TextMagnifier] on
/// Android, and builds nothing on all other platforms. If it is desired to supress
/// the magnifier, consider passing [TextMagnifierConfiguration.disabled].
final TextMagnifierConfiguration? magnifierConfiguration;
@override @override
State<SelectableText> createState() => _SelectableTextState(); State<SelectableText> createState() => _SelectableTextState();
...@@ -705,6 +719,7 @@ class _SelectableTextState extends State<SelectableText> implements TextSelectio ...@@ -705,6 +719,7 @@ class _SelectableTextState extends State<SelectableText> implements TextSelectio
paintCursorAboveText: paintCursorAboveText, paintCursorAboveText: paintCursorAboveText,
backgroundCursorColor: CupertinoColors.inactiveGray, backgroundCursorColor: CupertinoColors.inactiveGray,
enableInteractiveSelection: widget.enableInteractiveSelection, enableInteractiveSelection: widget.enableInteractiveSelection,
magnifierConfiguration: widget.magnifierConfiguration ?? TextMagnifier.adaptiveMagnifierConfiguration,
dragStartBehavior: widget.dragStartBehavior, dragStartBehavior: widget.dragStartBehavior,
scrollPhysics: widget.scrollPhysics, scrollPhysics: widget.scrollPhysics,
autofillHints: null, autofillHints: null,
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'desktop_text_selection.dart'; import 'desktop_text_selection.dart';
import 'magnifier.dart';
import 'text_selection.dart'; import 'text_selection.dart';
import 'theme.dart'; import 'theme.dart';
...@@ -34,9 +35,21 @@ class SelectionArea extends StatefulWidget { ...@@ -34,9 +35,21 @@ class SelectionArea extends StatefulWidget {
super.key, super.key,
this.focusNode, this.focusNode,
this.selectionControls, this.selectionControls,
this.magnifierConfiguration,
required this.child, required this.child,
}); });
/// {@macro flutter.widgets.text_selection.TextMagnifierConfiguration.intro}
///
/// {@macro flutter.widgets.magnifier.intro}
///
/// {@macro flutter.widgets.text_selection.TextMagnifierConfiguration.details}
///
/// By default, builds a [CupertinoTextMagnifier] on iOS and [TextMagnifier] on
/// Android, and builds nothing on all other platforms. If it is desired to supress
/// the magnifier, consider passing [TextMagnifierConfiguration.disabled].
final TextMagnifierConfiguration? magnifierConfiguration;
/// {@macro flutter.widgets.Focus.focusNode} /// {@macro flutter.widgets.Focus.focusNode}
final FocusNode? focusNode; final FocusNode? focusNode;
...@@ -92,6 +105,7 @@ class _SelectionAreaState extends State<SelectionArea> { ...@@ -92,6 +105,7 @@ class _SelectionAreaState extends State<SelectionArea> {
return SelectableRegion( return SelectableRegion(
focusNode: _effectiveFocusNode, focusNode: _effectiveFocusNode,
selectionControls: controls, selectionControls: controls,
magnifierConfiguration: widget.magnifierConfiguration ?? TextMagnifier.adaptiveMagnifierConfiguration,
child: widget.child, child: widget.child,
); );
} }
......
...@@ -14,7 +14,7 @@ import 'debug.dart'; ...@@ -14,7 +14,7 @@ import 'debug.dart';
import 'desktop_text_selection.dart'; import 'desktop_text_selection.dart';
import 'feedback.dart'; import 'feedback.dart';
import 'input_decorator.dart'; import 'input_decorator.dart';
import 'material.dart'; import 'magnifier.dart';
import 'material_localizations.dart'; import 'material_localizations.dart';
import 'material_state.dart'; import 'material_state.dart';
import 'selectable_text.dart' show iOSHorizontalOffset; import 'selectable_text.dart' show iOSHorizontalOffset;
...@@ -330,6 +330,7 @@ class TextField extends StatefulWidget { ...@@ -330,6 +330,7 @@ class TextField extends StatefulWidget {
this.restorationId, this.restorationId,
this.scribbleEnabled = true, this.scribbleEnabled = true,
this.enableIMEPersonalizedLearning = true, this.enableIMEPersonalizedLearning = true,
this.magnifierConfiguration,
}) : assert(textAlign != null), }) : assert(textAlign != null),
assert(readOnly != null), assert(readOnly != null),
assert(autofocus != null), assert(autofocus != null),
...@@ -392,6 +393,17 @@ class TextField extends StatefulWidget { ...@@ -392,6 +393,17 @@ class TextField extends StatefulWidget {
paste: true, paste: true,
))); )));
/// {@macro flutter.widgets.text_selection.TextMagnifierConfiguration.intro}
///
/// {@macro flutter.widgets.magnifier.intro}
///
/// {@macro flutter.widgets.text_selection.TextMagnifierConfiguration.details}
///
/// By default, builds a [CupertinoTextMagnifier] on iOS and [TextMagnifier] on
/// Android, and builds nothing on all other platforms. If it is desired to supress
/// the magnifier, consider passing [TextMagnifierConfiguration.disabled].
final TextMagnifierConfiguration? magnifierConfiguration;
/// Controls the text being edited. /// Controls the text being edited.
/// ///
/// If null, this widget will create its own [TextEditingController]. /// If null, this widget will create its own [TextEditingController].
...@@ -1312,6 +1324,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements ...@@ -1312,6 +1324,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
restorationId: 'editable', restorationId: 'editable',
scribbleEnabled: widget.scribbleEnabled, scribbleEnabled: widget.scribbleEnabled,
enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning,
magnifierConfiguration: widget.magnifierConfiguration ?? TextMagnifier.adaptiveMagnifierConfiguration,
), ),
), ),
); );
......
...@@ -636,6 +636,7 @@ class EditableText extends StatefulWidget { ...@@ -636,6 +636,7 @@ class EditableText extends StatefulWidget {
this.scrollBehavior, this.scrollBehavior,
this.scribbleEnabled = true, this.scribbleEnabled = true,
this.enableIMEPersonalizedLearning = true, this.enableIMEPersonalizedLearning = true,
this.magnifierConfiguration = TextMagnifierConfiguration.disabled,
}) : assert(controller != null), }) : assert(controller != null),
assert(focusNode != null), assert(focusNode != null),
assert(obscuringCharacter != null && obscuringCharacter.length == 1), assert(obscuringCharacter != null && obscuringCharacter.length == 1),
...@@ -1547,6 +1548,13 @@ class EditableText extends StatefulWidget { ...@@ -1547,6 +1548,13 @@ class EditableText extends StatefulWidget {
/// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning} /// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning}
final bool enableIMEPersonalizedLearning; final bool enableIMEPersonalizedLearning;
/// {@macro flutter.widgets.text_selection.TextMagnifierConfiguration.intro}
///
/// {@macro flutter.widgets.magnifier.intro}
///
/// {@macro flutter.widgets.text_selection.TextMagnifierConfiguration.details}
final TextMagnifierConfiguration magnifierConfiguration;
bool get _userSelectionEnabled => enableInteractiveSelection && (!readOnly || !obscureText); bool get _userSelectionEnabled => enableInteractiveSelection && (!readOnly || !obscureText);
// Infer the keyboard type of an `EditableText` if it's not specified. // Infer the keyboard type of an `EditableText` if it's not specified.
...@@ -2629,6 +2637,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -2629,6 +2637,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
selectionDelegate: this, selectionDelegate: this,
dragStartBehavior: widget.dragStartBehavior, dragStartBehavior: widget.dragStartBehavior,
onSelectionHandleTapped: widget.onSelectionHandleTapped, onSelectionHandleTapped: widget.onSelectionHandleTapped,
magnifierConfiguration: widget.magnifierConfiguration,
); );
} }
......
This diff is collapsed.
...@@ -9,6 +9,7 @@ import 'package:flutter/gestures.dart'; ...@@ -9,6 +9,7 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:vector_math/vector_math_64.dart';
import 'actions.dart'; import 'actions.dart';
import 'basic.dart'; import 'basic.dart';
...@@ -179,8 +180,18 @@ class SelectableRegion extends StatefulWidget { ...@@ -179,8 +180,18 @@ class SelectableRegion extends StatefulWidget {
required this.focusNode, required this.focusNode,
required this.selectionControls, required this.selectionControls,
required this.child, required this.child,
this.magnifierConfiguration = TextMagnifierConfiguration.disabled,
}); });
/// {@macro flutter.widgets.text_selection.TextMagnifierConfiguration.intro}
///
/// {@macro flutter.widgets.magnifier.intro}
///
/// By default, [SelectableRegion]'s [TextMagnifierConfiguration] is disabled.
///
/// {@macro flutter.widgets.text_selection.TextMagnifierConfiguration.details}
final TextMagnifierConfiguration magnifierConfiguration;
/// {@macro flutter.widgets.Focus.focusNode} /// {@macro flutter.widgets.Focus.focusNode}
final FocusNode focusNode; final FocusNode focusNode;
...@@ -403,7 +414,12 @@ class _SelectableRegionState extends State<SelectableRegion> with TextSelectionD ...@@ -403,7 +414,12 @@ class _SelectableRegionState extends State<SelectableRegion> with TextSelectionD
}); });
return; return;
} }
} }
void _onAnyDragEnd(DragEndDetails details) {
_selectionOverlay!.hideMagnifier(shouldShowToolbar: true);
_stopSelectionEndEdgeUpdate();
}
void _stopSelectionEndEdgeUpdate() { void _stopSelectionEndEdgeUpdate() {
_scheduledSelectionEndEdgeUpdate = false; _scheduledSelectionEndEdgeUpdate = false;
...@@ -451,11 +467,19 @@ class _SelectableRegionState extends State<SelectableRegion> with TextSelectionD ...@@ -451,11 +467,19 @@ class _SelectableRegionState extends State<SelectableRegion> with TextSelectionD
late Offset _selectionStartHandleDragPosition; late Offset _selectionStartHandleDragPosition;
late Offset _selectionEndHandleDragPosition; late Offset _selectionEndHandleDragPosition;
late List<TextSelectionPoint> points;
void _handleSelectionStartHandleDragStart(DragStartDetails details) { void _handleSelectionStartHandleDragStart(DragStartDetails details) {
assert(_selectionDelegate.value.startSelectionPoint != null); assert(_selectionDelegate.value.startSelectionPoint != null);
final Offset localPosition = _selectionDelegate.value.startSelectionPoint!.localPosition; final Offset localPosition = _selectionDelegate.value.startSelectionPoint!.localPosition;
final Matrix4 globalTransform = _selectable!.getTransformTo(null); final Matrix4 globalTransform = _selectable!.getTransformTo(null);
_selectionStartHandleDragPosition = MatrixUtils.transformPoint(globalTransform, localPosition); _selectionStartHandleDragPosition = MatrixUtils.transformPoint(globalTransform, localPosition);
_selectionOverlay!.showMagnifier(_buildInfoForMagnifier(
details.globalPosition,
_selectionDelegate.value.startSelectionPoint!,
));
} }
void _handleSelectionStartHandleDragUpdate(DragUpdateDetails details) { void _handleSelectionStartHandleDragUpdate(DragUpdateDetails details) {
...@@ -464,6 +488,11 @@ class _SelectableRegionState extends State<SelectableRegion> with TextSelectionD ...@@ -464,6 +488,11 @@ class _SelectableRegionState extends State<SelectableRegion> with TextSelectionD
// Offset it to the center of the line to make it feel more natural. // Offset it to the center of the line to make it feel more natural.
_selectionStartPosition = _selectionStartHandleDragPosition - Offset(0, _selectionDelegate.value.startSelectionPoint!.lineHeight / 2); _selectionStartPosition = _selectionStartHandleDragPosition - Offset(0, _selectionDelegate.value.startSelectionPoint!.lineHeight / 2);
_triggerSelectionStartEdgeUpdate(); _triggerSelectionStartEdgeUpdate();
_selectionOverlay!.updateMagnifier(_buildInfoForMagnifier(
details.globalPosition,
_selectionDelegate.value.startSelectionPoint!,
));
} }
void _handleSelectionEndHandleDragStart(DragStartDetails details) { void _handleSelectionEndHandleDragStart(DragStartDetails details) {
...@@ -471,6 +500,11 @@ class _SelectableRegionState extends State<SelectableRegion> with TextSelectionD ...@@ -471,6 +500,11 @@ class _SelectableRegionState extends State<SelectableRegion> with TextSelectionD
final Offset localPosition = _selectionDelegate.value.endSelectionPoint!.localPosition; final Offset localPosition = _selectionDelegate.value.endSelectionPoint!.localPosition;
final Matrix4 globalTransform = _selectable!.getTransformTo(null); final Matrix4 globalTransform = _selectable!.getTransformTo(null);
_selectionEndHandleDragPosition = MatrixUtils.transformPoint(globalTransform, localPosition); _selectionEndHandleDragPosition = MatrixUtils.transformPoint(globalTransform, localPosition);
_selectionOverlay!.showMagnifier(_buildInfoForMagnifier(
details.globalPosition,
_selectionDelegate.value.endSelectionPoint!,
));
} }
void _handleSelectionEndHandleDragUpdate(DragUpdateDetails details) { void _handleSelectionEndHandleDragUpdate(DragUpdateDetails details) {
...@@ -479,6 +513,30 @@ class _SelectableRegionState extends State<SelectableRegion> with TextSelectionD ...@@ -479,6 +513,30 @@ class _SelectableRegionState extends State<SelectableRegion> with TextSelectionD
// Offset it to the center of the line to make it feel more natural. // Offset it to the center of the line to make it feel more natural.
_selectionEndPosition = _selectionEndHandleDragPosition - Offset(0, _selectionDelegate.value.endSelectionPoint!.lineHeight / 2); _selectionEndPosition = _selectionEndHandleDragPosition - Offset(0, _selectionDelegate.value.endSelectionPoint!.lineHeight / 2);
_triggerSelectionEndEdgeUpdate(); _triggerSelectionEndEdgeUpdate();
_selectionOverlay!.updateMagnifier(_buildInfoForMagnifier(
details.globalPosition,
_selectionDelegate.value.endSelectionPoint!,
));
}
MagnifierOverlayInfoBearer _buildInfoForMagnifier(Offset globalGesturePosition, SelectionPoint selectionPoint) {
final Vector3 globalTransform = _selectable!.getTransformTo(null).getTranslation();
final Offset globalTransformAsOffset = Offset(globalTransform.x, globalTransform.y);
final Offset globalSelectionPointPosition = selectionPoint.localPosition + globalTransformAsOffset;
final Rect caretRect = Rect.fromLTWH(
globalSelectionPointPosition.dx,
globalSelectionPointPosition.dy - selectionPoint.lineHeight,
0,
selectionPoint.lineHeight
);
return MagnifierOverlayInfoBearer(
globalGesturePosition: globalGesturePosition,
caretRect: caretRect,
fieldBounds: globalTransformAsOffset & _selectable!.size,
currentLineBoundries: globalTransformAsOffset & _selectable!.size,
);
} }
void _createSelectionOverlay() { void _createSelectionOverlay() {
...@@ -488,7 +546,6 @@ class _SelectableRegionState extends State<SelectableRegion> with TextSelectionD ...@@ -488,7 +546,6 @@ class _SelectableRegionState extends State<SelectableRegion> with TextSelectionD
} }
final SelectionPoint? start = _selectionDelegate.value.startSelectionPoint; final SelectionPoint? start = _selectionDelegate.value.startSelectionPoint;
final SelectionPoint? end = _selectionDelegate.value.endSelectionPoint; final SelectionPoint? end = _selectionDelegate.value.endSelectionPoint;
late List<TextSelectionPoint> points;
final Offset startLocalPosition = start?.localPosition ?? end!.localPosition; final Offset startLocalPosition = start?.localPosition ?? end!.localPosition;
final Offset endLocalPosition = end?.localPosition ?? start!.localPosition; final Offset endLocalPosition = end?.localPosition ?? start!.localPosition;
if (startLocalPosition.dy > endLocalPosition.dy) { if (startLocalPosition.dy > endLocalPosition.dy) {
...@@ -509,12 +566,12 @@ class _SelectableRegionState extends State<SelectableRegion> with TextSelectionD ...@@ -509,12 +566,12 @@ class _SelectableRegionState extends State<SelectableRegion> with TextSelectionD
lineHeightAtStart: start?.lineHeight ?? end!.lineHeight, lineHeightAtStart: start?.lineHeight ?? end!.lineHeight,
onStartHandleDragStart: _handleSelectionStartHandleDragStart, onStartHandleDragStart: _handleSelectionStartHandleDragStart,
onStartHandleDragUpdate: _handleSelectionStartHandleDragUpdate, onStartHandleDragUpdate: _handleSelectionStartHandleDragUpdate,
onStartHandleDragEnd: (DragEndDetails details) => _stopSelectionStartEdgeUpdate(), onStartHandleDragEnd: _onAnyDragEnd,
endHandleType: end?.handleType ?? TextSelectionHandleType.right, endHandleType: end?.handleType ?? TextSelectionHandleType.right,
lineHeightAtEnd: end?.lineHeight ?? start!.lineHeight, lineHeightAtEnd: end?.lineHeight ?? start!.lineHeight,
onEndHandleDragStart: _handleSelectionEndHandleDragStart, onEndHandleDragStart: _handleSelectionEndHandleDragStart,
onEndHandleDragUpdate: _handleSelectionEndHandleDragUpdate, onEndHandleDragUpdate: _handleSelectionEndHandleDragUpdate,
onEndHandleDragEnd: (DragEndDetails details) => _stopSelectionEndEdgeUpdate(), onEndHandleDragEnd: _onAnyDragEnd,
selectionEndpoints: points, selectionEndpoints: points,
selectionControls: widget.selectionControls, selectionControls: widget.selectionControls,
selectionDelegate: this, selectionDelegate: this,
...@@ -522,6 +579,7 @@ class _SelectableRegionState extends State<SelectableRegion> with TextSelectionD ...@@ -522,6 +579,7 @@ class _SelectableRegionState extends State<SelectableRegion> with TextSelectionD
startHandleLayerLink: _startHandleLayerLink, startHandleLayerLink: _startHandleLayerLink,
endHandleLayerLink: _endHandleLayerLink, endHandleLayerLink: _endHandleLayerLink,
toolbarLayerLink: _toolbarLayerLink, toolbarLayerLink: _toolbarLayerLink,
magnifierConfiguration: widget.magnifierConfiguration
); );
} }
...@@ -798,6 +856,9 @@ class _SelectableRegionState extends State<SelectableRegion> with TextSelectionD ...@@ -798,6 +856,9 @@ class _SelectableRegionState extends State<SelectableRegion> with TextSelectionD
_selectable?.removeListener(_updateSelectionStatus); _selectable?.removeListener(_updateSelectionStatus);
_selectable?.pushHandleLayers(null, null); _selectable?.pushHandleLayers(null, null);
_selectionDelegate.dispose(); _selectionDelegate.dispose();
// In case dispose was triggered before gesture end, remove the magnifier
// so it doesn't remain stuck in the overlay forever.
_selectionOverlay?.hideMagnifier(shouldShowToolbar: false);
_selectionOverlay?.dispose(); _selectionOverlay?.dispose();
_selectionOverlay = null; _selectionOverlay = null;
super.dispose(); super.dispose();
......
...@@ -70,6 +70,7 @@ export 'src/widgets/keyboard_listener.dart'; ...@@ -70,6 +70,7 @@ export 'src/widgets/keyboard_listener.dart';
export 'src/widgets/layout_builder.dart'; export 'src/widgets/layout_builder.dart';
export 'src/widgets/list_wheel_scroll_view.dart'; export 'src/widgets/list_wheel_scroll_view.dart';
export 'src/widgets/localizations.dart'; export 'src/widgets/localizations.dart';
export 'src/widgets/magnifier.dart';
export 'src/widgets/media_query.dart'; export 'src/widgets/media_query.dart';
export 'src/widgets/modal_barrier.dart'; export 'src/widgets/modal_barrier.dart';
export 'src/widgets/navigation_toolbar.dart'; export 'src/widgets/navigation_toolbar.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.
@Tags(<String>['reduced-test-set'])
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
final Offset basicOffset = Offset(CupertinoMagnifier.kDefaultSize.width / 2,
CupertinoMagnifier.kDefaultSize.height - CupertinoMagnifier.kMagnifierAboveFocalPoint);
const Rect reasonableTextField = Rect.fromLTRB(0, 100, 200, 200);
final MagnifierController magnifierController = MagnifierController();
// Make sure that your gesture in infoBearer is within the line in infoBearer,
// or else the magnifier status will stay hidden and this will not complete.
Future<void> showCupertinoMagnifier(
BuildContext context,
WidgetTester tester,
ValueNotifier<MagnifierOverlayInfoBearer> infoBearer,
) async {
final Future<void> magnifierShown = magnifierController.show(
context: context,
builder: (_) => CupertinoTextMagnifier(
controller: magnifierController,
magnifierOverlayInfoBearer: infoBearer,
));
WidgetsBinding.instance.scheduleFrame();
await tester.pumpAndSettle();
await magnifierShown;
}
tearDown(() async {
magnifierController.removeFromOverlay();
});
group('CupertinoTextEditingMagnifier', () {
group('position', () {
Offset getMagnifierPosition(WidgetTester tester) {
final AnimatedPositioned animatedPositioned =
tester.firstWidget(find.byType(AnimatedPositioned));
return Offset(
animatedPositioned.left ?? 0, animatedPositioned.top ?? 0);
}
testWidgets('should be at gesture position if does not violate any positioning rules', (WidgetTester tester) async {
final Key fakeTextFieldKey = UniqueKey();
final Key outerKey = UniqueKey();
await tester.pumpWidget(
Container(
key: outerKey,
color: const Color.fromARGB(255, 0, 255, 179),
child: MaterialApp(
home: Center(
child: Container(
key: fakeTextFieldKey,
width: 10,
height: 10,
color: Colors.red,
child: const Placeholder(),
),
),
),
),
);
final BuildContext context = tester.element(find.byType(Placeholder));
// Magnifier should be positioned directly over the red square.
final RenderBox tapPointRenderBox =
tester.firstRenderObject(find.byKey(fakeTextFieldKey)) as RenderBox;
final Rect fakeTextFieldRect =
tapPointRenderBox.localToGlobal(Offset.zero) & tapPointRenderBox.size;
final ValueNotifier<MagnifierOverlayInfoBearer> magnifier =
ValueNotifier<MagnifierOverlayInfoBearer>(
MagnifierOverlayInfoBearer(
currentLineBoundries: fakeTextFieldRect,
fieldBounds: fakeTextFieldRect,
caretRect: fakeTextFieldRect,
// The tap position is dragBelow units below the text field.
globalGesturePosition: fakeTextFieldRect.center,
),
);
await showCupertinoMagnifier(context, tester, magnifier);
// Should show two red squares; original, and one in the magnifier,
// directly ontop of one another.
await expectLater(
find.byKey(outerKey),
matchesGoldenFile('cupertino_magnifier.position.default.png'),
);
});
testWidgets('should never horizontally be outside of Screen Padding', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
color: Color.fromARGB(7, 0, 129, 90),
home: Placeholder(),
),
);
final BuildContext context = tester.firstElement(find.byType(Placeholder));
await showCupertinoMagnifier(
context,
tester,
ValueNotifier<MagnifierOverlayInfoBearer>(
MagnifierOverlayInfoBearer(
currentLineBoundries: reasonableTextField,
fieldBounds: reasonableTextField,
caretRect: reasonableTextField,
// The tap position is far out of the right side of the app.
globalGesturePosition:
Offset(MediaQuery.of(context).size.width + 100, 0),
),
),
);
// Should be less than the right edge, since we have padding.
expect(getMagnifierPosition(tester).dx,
lessThan(MediaQuery.of(context).size.width));
});
testWidgets('should have some vertical drag', (WidgetTester tester) async {
final double dragPositionBelowTextField = reasonableTextField.center.dy + 30;
await tester.pumpWidget(
const MaterialApp(
color: Color.fromARGB(7, 0, 129, 90),
home: Placeholder(),
),
);
final BuildContext context =
tester.firstElement(find.byType(Placeholder));
await showCupertinoMagnifier(
context,
tester,
ValueNotifier<MagnifierOverlayInfoBearer>(
MagnifierOverlayInfoBearer(
currentLineBoundries: reasonableTextField,
fieldBounds: reasonableTextField,
caretRect: reasonableTextField,
// The tap position is dragBelow units below the text field.
globalGesturePosition: Offset(
MediaQuery.of(context).size.width / 2,
dragPositionBelowTextField),
),
),
);
// The magnifier Y should be greater than the text field, since we "dragged" it down.
expect(getMagnifierPosition(tester).dy + basicOffset.dy,
greaterThan(reasonableTextField.center.dy));
expect(getMagnifierPosition(tester).dy + basicOffset.dy,
lessThan(dragPositionBelowTextField));
});
});
group('status', () {
testWidgets('should hide if gesture is far below the text field', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
color: Color.fromARGB(7, 0, 129, 90),
home: Placeholder(),
),
);
final BuildContext context =
tester.firstElement(find.byType(Placeholder));
final ValueNotifier<MagnifierOverlayInfoBearer> magnifierinfo =
ValueNotifier<MagnifierOverlayInfoBearer>(
MagnifierOverlayInfoBearer(
currentLineBoundries: reasonableTextField,
fieldBounds: reasonableTextField,
caretRect: reasonableTextField,
// The tap position is dragBelow units below the text field.
globalGesturePosition: Offset(
MediaQuery.of(context).size.width / 2, reasonableTextField.top),
),
);
// Show the magnifier initally, so that we get it in a not hidden state.
await showCupertinoMagnifier(context, tester, magnifierinfo);
// Move the gesture to one that should hide it.
magnifierinfo.value = MagnifierOverlayInfoBearer(
currentLineBoundries: reasonableTextField,
fieldBounds: reasonableTextField,
caretRect: reasonableTextField,
globalGesturePosition: magnifierinfo.value.globalGesturePosition + const Offset(0, 100),
);
await tester.pumpAndSettle();
expect(magnifierController.shown, false);
expect(magnifierController.overlayEntry, isNotNull);
});
testWidgets('should re-show if gesture moves back up',
(WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
color: Color.fromARGB(7, 0, 129, 90),
home: Placeholder(),
),
);
final BuildContext context =
tester.firstElement(find.byType(Placeholder));
final ValueNotifier<MagnifierOverlayInfoBearer> magnifierInfo =
ValueNotifier<MagnifierOverlayInfoBearer>(
MagnifierOverlayInfoBearer(
currentLineBoundries: reasonableTextField,
fieldBounds: reasonableTextField,
caretRect: reasonableTextField,
// The tap position is dragBelow units below the text field.
globalGesturePosition: Offset(MediaQuery.of(context).size.width / 2, reasonableTextField.top),
),
);
// Show the magnifier initally, so that we get it in a not hidden state.
await showCupertinoMagnifier(context, tester, magnifierInfo);
// Move the gesture to one that should hide it.
magnifierInfo.value = MagnifierOverlayInfoBearer(
currentLineBoundries: reasonableTextField,
fieldBounds: reasonableTextField,
caretRect: reasonableTextField,
globalGesturePosition:
magnifierInfo.value.globalGesturePosition + const Offset(0, 100));
await tester.pumpAndSettle();
expect(magnifierController.shown, false);
expect(magnifierController.overlayEntry, isNotNull);
// Return the gesture to one that shows it.
magnifierInfo.value = MagnifierOverlayInfoBearer(
currentLineBoundries: reasonableTextField,
fieldBounds: reasonableTextField,
caretRect: reasonableTextField,
globalGesturePosition: Offset(MediaQuery.of(context).size.width / 2,
reasonableTextField.top));
await tester.pumpAndSettle();
expect(magnifierController.shown, true);
expect(magnifierController.overlayEntry, isNotNull);
});
});
});
}
...@@ -5961,6 +5961,148 @@ void main() { ...@@ -5961,6 +5961,148 @@ void main() {
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
}); });
group('magnifier', () {
late ValueNotifier<MagnifierOverlayInfoBearer> infoBearer;
final Widget fakeMagnifier = Container(key: UniqueKey());
group('magnifier builder', () {
testWidgets('should build custom magnifier if given', (WidgetTester tester) async {
final Widget customMagnifier = Container(
key: UniqueKey(),
);
final CupertinoTextField defaultCupertinoTextField = CupertinoTextField(
magnifierConfiguration: TextMagnifierConfiguration(magnifierBuilder: (_, __, ___) => customMagnifier),
);
await tester.pumpWidget(const CupertinoApp(
home: Placeholder(),
));
final BuildContext context =
tester.firstElement(find.byType(Placeholder));
expect(
defaultCupertinoTextField.magnifierConfiguration!.magnifierBuilder(
context,
MagnifierController(),
ValueNotifier<MagnifierOverlayInfoBearer>(
const MagnifierOverlayInfoBearer.empty(),
)),
isA<Widget>().having(
(Widget widget) => widget.key, 'key', equals(customMagnifier.key)));
});
group('defaults', () {
testWidgets('should build CupertinoMagnifier on iOS and Android', (WidgetTester tester) async {
await tester.pumpWidget(const CupertinoApp(
home: CupertinoTextField(),
));
final BuildContext context = tester.firstElement(find.byType(CupertinoTextField));
final EditableText editableText = tester.widget(find.byType(EditableText));
expect(
editableText.magnifierConfiguration.magnifierBuilder(
context,
MagnifierController(),
ValueNotifier<MagnifierOverlayInfoBearer>(
const MagnifierOverlayInfoBearer.empty(),
)),
isA<CupertinoTextMagnifier>());
},
variant: const TargetPlatformVariant(
<TargetPlatform>{TargetPlatform.iOS, TargetPlatform.android}));
});
testWidgets('should build nothing on all platforms but iOS and Android', (WidgetTester tester) async {
await tester.pumpWidget(const CupertinoApp(
home: CupertinoTextField(),
));
final BuildContext context = tester.firstElement(find.byType(CupertinoTextField));
final EditableText editableText = tester.widget(find.byType(EditableText));
expect(
editableText.magnifierConfiguration.magnifierBuilder(
context,
MagnifierController(),
ValueNotifier<MagnifierOverlayInfoBearer>(
const MagnifierOverlayInfoBearer.empty(),
)),
isNull);
},
variant: TargetPlatformVariant.all(
excluding: <TargetPlatform>{TargetPlatform.iOS, TargetPlatform.android}));
});
testWidgets('Can drag handles to show, unshow, and update magnifier',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
CupertinoApp(
home: CupertinoPageScaffold(
child: Builder(
builder: (BuildContext context) => CupertinoTextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
magnifierConfiguration: TextMagnifierConfiguration(
magnifierBuilder: (_,
MagnifierController controller,
ValueNotifier<MagnifierOverlayInfoBearer>
localInfoBearer) {
infoBearer = localInfoBearer;
return fakeMagnifier;
}),
),
),
),
),
);
const String testValue = 'abc def ghi';
await tester.enterText(find.byType(CupertinoTextField), testValue);
// Double tap the 'e' to select 'def'.
await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
await tester.pump(const Duration(milliseconds: 30));
await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
await tester.pump(const Duration(milliseconds: 30));
final TextSelection selection = controller.selection;
final RenderEditable renderEditable = findRenderEditable(tester);
final List<TextSelectionPoint> endpoints = globalize(
renderEditable.getEndpointsForSelection(selection),
renderEditable,
);
// Drag the right handle 2 letters to the right.
final Offset handlePos = endpoints.last.point + const Offset(1.0, 1.0);
final TestGesture gesture =
await tester.startGesture(handlePos, pointer: 7);
Offset? firstDragGesturePosition;
await gesture.moveTo(textOffsetToPosition(tester, testValue.length - 2));
await tester.pump();
expect(find.byKey(fakeMagnifier.key!), findsOneWidget);
firstDragGesturePosition = infoBearer.value.globalGesturePosition;
await gesture.moveTo(textOffsetToPosition(tester, testValue.length));
await tester.pump();
// Expect the position the magnifier gets to have moved.
expect(firstDragGesturePosition,
isNot(infoBearer.value.globalGesturePosition));
await gesture.up();
await tester.pump();
expect(find.byKey(fakeMagnifier.key!), findsNothing);
}, variant: TargetPlatformVariant.only(TargetPlatform.iOS));
group('TapRegion integration', () { group('TapRegion integration', () {
testWidgets('Tapping outside loses focus on desktop', (WidgetTester tester) async { testWidgets('Tapping outside loses focus on desktop', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(debugLabel: 'Test Node'); final FocusNode focusNode = FocusNode(debugLabel: 'Test Node');
...@@ -6073,31 +6215,34 @@ void main() { ...@@ -6073,31 +6215,34 @@ void main() {
variant: TargetPlatformVariant.all(), variant: TargetPlatformVariant.all(),
skip: kIsWeb, // [intended] The toolbar isn't rendered by Flutter on the web, it's rendered by the browser. skip: kIsWeb, // [intended] The toolbar isn't rendered by Flutter on the web, it's rendered by the browser.
); );
testWidgets("Tapping on border doesn't lose focus", (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(debugLabel: 'Test Node'); testWidgets("Tapping on border doesn't lose focus",
await tester.pumpWidget( (WidgetTester tester) async {
CupertinoApp( final FocusNode focusNode = FocusNode(debugLabel: 'Test Node');
home: Center( await tester.pumpWidget(
child: SizedBox( CupertinoApp(
width: 100, home: Center(
height: 100, child: SizedBox(
child: CupertinoTextField( width: 100,
autofocus: true, height: 100,
focusNode: focusNode, child: CupertinoTextField(
autofocus: true,
focusNode: focusNode,
),
), ),
), ),
), ),
), );
); await tester.pump();
await tester.pump(); expect(focusNode.hasPrimaryFocus, isTrue);
expect(focusNode.hasPrimaryFocus, isTrue);
final Rect borderBox = tester.getRect(find.byType(CupertinoTextField)); final Rect borderBox = tester.getRect(find.byType(CupertinoTextField));
// Tap just inside the border, but not inside the EditableText. // Tap just inside the border, but not inside the EditableText.
await tester.tapAt(borderBox.topLeft + const Offset(1, 1)); await tester.tapAt(borderBox.topLeft + const Offset(1, 1));
await tester.pump(); await tester.pump();
expect(focusNode.hasPrimaryFocus, isTrue); expect(focusNode.hasPrimaryFocus, isTrue);
}, variant: TargetPlatformVariant.all()); }, variant: TargetPlatformVariant.all());
});
}); });
} }
This diff is collapsed.
...@@ -11785,6 +11785,170 @@ void main() { ...@@ -11785,6 +11785,170 @@ void main() {
expect(controller.selection.extentOffset, 5); expect(controller.selection.extentOffset, 5);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
}); });
group('magnifier builder', () {
testWidgets('should build custom magnifier if given',
(WidgetTester tester) async {
final Widget customMagnifier = Container(
key: UniqueKey(),
);
final TextField textField = TextField(
magnifierConfiguration: TextMagnifierConfiguration(
magnifierBuilder: (_, __, ___) => customMagnifier,
),
);
await tester.pumpWidget(const MaterialApp(
home: Placeholder(),
));
final BuildContext context =
tester.firstElement(find.byType(Placeholder));
expect(
textField.magnifierConfiguration!.magnifierBuilder(
context,
MagnifierController(),
ValueNotifier<MagnifierOverlayInfoBearer>(
const MagnifierOverlayInfoBearer.empty(),
)),
isA<Widget>().having(
(Widget widget) => widget.key,
'built magnifier key equal to passed in magnifier key',
equals(customMagnifier.key)));
});
group('defaults', () {
testWidgets('should build Magnifier on Android', (WidgetTester tester) async {
await tester.pumpWidget(const MaterialApp(
home: Scaffold(body: TextField()))
);
final BuildContext context = tester.firstElement(find.byType(TextField));
final EditableText editableText = tester.widget(find.byType(EditableText));
expect(
editableText.magnifierConfiguration.magnifierBuilder(
context,
MagnifierController(),
ValueNotifier<MagnifierOverlayInfoBearer>(
const MagnifierOverlayInfoBearer.empty(),
)),
isA<TextMagnifier>());
}, variant: TargetPlatformVariant.only(TargetPlatform.android));
testWidgets('should build CupertinoMagnifier on iOS',
(WidgetTester tester) async {
await tester.pumpWidget(const MaterialApp(
home: Scaffold(body: TextField()))
);
final BuildContext context = tester.firstElement(find.byType(TextField));
final EditableText editableText = tester.widget(find.byType(EditableText));
expect(
editableText.magnifierConfiguration.magnifierBuilder(
context,
MagnifierController(),
ValueNotifier<MagnifierOverlayInfoBearer>(
const MagnifierOverlayInfoBearer.empty(),
)),
isA<CupertinoTextMagnifier>());
}, variant: TargetPlatformVariant.only(TargetPlatform.iOS));
testWidgets('should build nothing on Android and iOS',
(WidgetTester tester) async {
await tester.pumpWidget(const MaterialApp(
home: Scaffold(body: TextField()))
);
final BuildContext context = tester.firstElement(find.byType(TextField));
final EditableText editableText = tester.widget(find.byType(EditableText));
expect(
editableText.magnifierConfiguration.magnifierBuilder(
context,
MagnifierController(),
ValueNotifier<MagnifierOverlayInfoBearer>(
const MagnifierOverlayInfoBearer.empty(),
)),
isNull);
},
variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{
TargetPlatform.iOS,
TargetPlatform.android
}));
});
});
group('magnifier', () {
late ValueNotifier<MagnifierOverlayInfoBearer> infoBearer;
final Widget fakeMagnifier = Container(key: UniqueKey());
testWidgets(
'Can drag handles to show, unshow, and update magnifier',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController();
await tester.pumpWidget(
overlay(
child: TextField(
dragStartBehavior: DragStartBehavior.down,
controller: controller,
magnifierConfiguration: TextMagnifierConfiguration(
magnifierBuilder: (
_,
MagnifierController controller,
ValueNotifier<MagnifierOverlayInfoBearer> localInfoBearer
) {
infoBearer = localInfoBearer;
return fakeMagnifier;
},
),
),
),
);
const String testValue = 'abc def ghi';
await tester.enterText(find.byType(TextField), testValue);
await skipPastScrollingAnimation(tester);
// Double tap the 'e' to select 'def'.
await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
await tester.pump(const Duration(milliseconds: 30));
await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
await tester.pump(const Duration(milliseconds: 30));
final TextSelection selection = controller.selection;
final RenderEditable renderEditable = findRenderEditable(tester);
final List<TextSelectionPoint> endpoints = globalize(
renderEditable.getEndpointsForSelection(selection),
renderEditable,
);
// Drag the right handle 2 letters to the right.
final Offset handlePos = endpoints.last.point + const Offset(1.0, 1.0);
final TestGesture gesture = await tester.startGesture(handlePos);
await gesture.moveTo(textOffsetToPosition(tester, testValue.length - 2));
await tester.pump();
expect(find.byKey(fakeMagnifier.key!), findsOneWidget);
final Offset firstDragGesturePosition = infoBearer.value.globalGesturePosition;
await gesture.moveTo(textOffsetToPosition(tester, testValue.length));
await tester.pump();
// Expect the position the magnifier gets to have moved.
expect(firstDragGesturePosition,
isNot(infoBearer.value.globalGesturePosition));
await gesture.up();
await tester.pump();
expect(find.byKey(fakeMagnifier.key!), findsNothing);
});
group('TapRegion integration', () { group('TapRegion integration', () {
testWidgets('Tapping outside loses focus on desktop', (WidgetTester tester) async { testWidgets('Tapping outside loses focus on desktop', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(debugLabel: 'Test Node'); final FocusNode focusNode = FocusNode(debugLabel: 'Test Node');
...@@ -12001,8 +12165,9 @@ void main() { ...@@ -12001,8 +12165,9 @@ void main() {
case PointerDeviceKind.unknown: case PointerDeviceKind.unknown:
expect(focusNode.hasPrimaryFocus, isFalse); expect(focusNode.hasPrimaryFocus, isFalse);
break; break;
} }
}, variant: TargetPlatformVariant.all()); }, variant: TargetPlatformVariant.all());
} }
});
}); });
} }
...@@ -12563,6 +12563,40 @@ void main() { ...@@ -12563,6 +12563,40 @@ void main() {
); );
}); });
}); });
group('magnifier', () {
testWidgets('should build nothing by default', (WidgetTester tester) async {
final EditableText editableText = EditableText(
controller: controller,
showSelectionHandles: true,
autofocus: true,
focusNode: FocusNode(),
style: Typography.material2018().black.subtitle1!,
cursorColor: Colors.blue,
backgroundCursorColor: Colors.grey,
selectionControls: materialTextSelectionControls,
keyboardType: TextInputType.text,
textAlign: TextAlign.right,
);
await tester.pumpWidget(
MaterialApp(
home: editableText,
),
);
final BuildContext context = tester.firstElement(find.byType(EditableText));
expect(
editableText.magnifierConfiguration.magnifierBuilder(
context,
MagnifierController(),
ValueNotifier<MagnifierOverlayInfoBearer>(const MagnifierOverlayInfoBearer.empty())
),
isNull
);
});
});
} }
class UnsettableController extends TextEditingController { class UnsettableController extends TextEditingController {
......
This diff is collapsed.
...@@ -1099,6 +1099,74 @@ void main() { ...@@ -1099,6 +1099,74 @@ void main() {
final Map<String, dynamic> clipboardData = mockClipboard.clipboardData as Map<String, dynamic>; final Map<String, dynamic> clipboardData = mockClipboard.clipboardData as Map<String, dynamic>;
expect(clipboardData['text'], 'thank'); expect(clipboardData['text'], 'thank');
}, skip: kIsWeb); // [intended] Web uses its native context menu. }, skip: kIsWeb); // [intended] Web uses its native context menu.
group('magnifier', () {
late ValueNotifier<MagnifierOverlayInfoBearer> infoBearer;
final Widget fakeMagnifier = Container(key: UniqueKey());
testWidgets('Can drag handles to show, unshow, and update magnifier',
(WidgetTester tester) async {
const String text = 'Monkies and rabbits in my soup';
await tester.pumpWidget(
MaterialApp(
home: SelectableRegion(
magnifierConfiguration: TextMagnifierConfiguration(
magnifierBuilder: (_,
MagnifierController controller,
ValueNotifier<MagnifierOverlayInfoBearer>
localInfoBearer) {
infoBearer = localInfoBearer;
return fakeMagnifier;
},
),
focusNode: FocusNode(),
selectionControls: materialTextSelectionControls,
child: const Text(text),
),
),
);
final RenderParagraph paragraph = tester.renderObject<RenderParagraph>(
find.descendant(
of: find.text(text), matching: find.byType(RichText)));
// Show the selection handles.
final TestGesture activateSelectionGesture = await tester
.startGesture(textOffsetToPosition(paragraph, text.length ~/ 2));
addTearDown(activateSelectionGesture.removePointer);
await tester.pump(const Duration(milliseconds: 500));
await activateSelectionGesture.up();
await tester.pump(const Duration(milliseconds: 500));
// Drag the handle around so that the magnifier shows.
final TextBox selectionBox =
paragraph.getBoxesForSelection(paragraph.selections.first).first;
final Offset leftHandlePos =
globalize(selectionBox.toRect().bottomLeft, paragraph);
final TestGesture gesture = await tester.startGesture(leftHandlePos);
await gesture.moveTo(textOffsetToPosition(paragraph, text.length - 2));
await tester.pump();
// Expect the magnifier to show and then store it's position.
expect(find.byKey(fakeMagnifier.key!), findsOneWidget);
final Offset firstDragGesturePosition =
infoBearer.value.globalGesturePosition;
await gesture.moveTo(textOffsetToPosition(paragraph, text.length));
await tester.pump();
// Expect the position the magnifier gets to have moved.
expect(firstDragGesturePosition,
isNot(infoBearer.value.globalGesturePosition));
// Lift the pointer and expect the magnifier to disappear.
await gesture.up();
await tester.pump();
expect(find.byKey(fakeMagnifier.key!), findsNothing);
});
});
}); });
testWidgets('toolbar is hidden on mobile when orientation changes', (WidgetTester tester) async { testWidgets('toolbar is hidden on mobile when orientation changes', (WidgetTester tester) async {
......
...@@ -5152,6 +5152,79 @@ void main() { ...@@ -5152,6 +5152,79 @@ void main() {
expect(find.byType(SelectableText, skipOffstage: false), findsOneWidget); expect(find.byType(SelectableText, skipOffstage: false), findsOneWidget);
}); });
group('magnifier', () {
late ValueNotifier<MagnifierOverlayInfoBearer> infoBearer;
final Widget fakeMagnifier = Container(key: UniqueKey());
testWidgets(
'Can drag handles to show, unshow, and update magnifier',
(WidgetTester tester) async {
const String testValue = 'abc def ghi';
final SelectableText selectableText = SelectableText(
testValue,
magnifierConfiguration: TextMagnifierConfiguration(
magnifierBuilder: (
_,
MagnifierController controller,
ValueNotifier<MagnifierOverlayInfoBearer> localInfoBearer
) {
infoBearer = localInfoBearer;
return fakeMagnifier;
},
)
);
await tester.pumpWidget(
overlay(
child: selectableText,
),
);
await skipPastScrollingAnimation(tester);
// Double tap the 'e' to select 'def'.
await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
await tester.pump(const Duration(milliseconds: 30));
await tester.tapAt(textOffsetToPosition(tester, testValue.indexOf('e')));
await tester.pump(const Duration(milliseconds: 30));
final TextSelection selection = TextSelection(
baseOffset: testValue.indexOf('d'),
extentOffset: testValue.indexOf('f')
);
final RenderEditable renderEditable = findRenderEditable(tester);
final List<TextSelectionPoint> endpoints = globalize(
renderEditable.getEndpointsForSelection(selection),
renderEditable,
);
// Drag the right handle 2 letters to the right.
final Offset handlePos = endpoints.last.point + const Offset(1.0, 1.0);
final TestGesture gesture = await tester.startGesture(handlePos, pointer: 7);
Offset? firstDragGesturePosition;
await gesture.moveTo(textOffsetToPosition(tester, testValue.length - 2));
await tester.pump();
expect(find.byKey(fakeMagnifier.key!), findsOneWidget);
firstDragGesturePosition = infoBearer.value.globalGesturePosition;
await gesture.moveTo(textOffsetToPosition(tester, testValue.length));
await tester.pump();
// Expect the position the magnifier gets to have moved.
expect(firstDragGesturePosition,
isNot(infoBearer.value.globalGesturePosition));
await gesture.up();
await tester.pump();
expect(find.byKey(fakeMagnifier.key!), findsNothing);
});
});
testWidgets('SelectableText text span style is merged with default text style', (WidgetTester tester) async { testWidgets('SelectableText text span style is merged with default text style', (WidgetTester tester) async {
// This is a regression test for https://github.com/flutter/flutter/issues/71389 // This is a regression test for https://github.com/flutter/flutter/issues/71389
......
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