Expose selectionHeightStyle and selectionWidthStyle on TextFields (#48917)

This commit is contained in:
Gary Qian 2020-02-05 20:38:03 -05:00 committed by GitHub
parent 3aa7a80053
commit 3e1a124e0e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 335 additions and 27 deletions

View file

@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle;
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
@ -196,10 +198,15 @@ class CupertinoTextField extends StatefulWidget {
///
/// If specified, the [maxLength] property must be greater than zero.
///
/// The [selectionHeightStyle] and [selectionWidthStyle] properties allow
/// changing the shape of the selection highlighting. These properties default
/// to [ui.BoxHeightStyle.tight] and [ui.BoxWidthStyle.tight] respectively and
/// must not be null.
///
/// The [autocorrect], [autofocus], [clearButtonMode], [dragStartBehavior],
/// [expands], [maxLengthEnforced], [obscureText], [prefixMode], [readOnly],
/// [scrollPadding], [suffixMode], [textAlign], and [enableSuggestions]
/// properties must not be null.
/// [scrollPadding], [suffixMode], [textAlign], [selectionHeightStyle],
/// [selectionWidthStyle], and [enableSuggestions] properties must not be null.
///
/// See also:
///
@ -252,6 +259,8 @@ class CupertinoTextField extends StatefulWidget {
this.cursorWidth = 2.0,
this.cursorRadius = const Radius.circular(2.0),
this.cursorColor,
this.selectionHeightStyle = ui.BoxHeightStyle.tight,
this.selectionWidthStyle = ui.BoxWidthStyle.tight,
this.keyboardAppearance,
this.scrollPadding = const EdgeInsets.all(20.0),
this.dragStartBehavior = DragStartBehavior.start,
@ -270,6 +279,8 @@ class CupertinoTextField extends StatefulWidget {
assert(maxLengthEnforced != null),
assert(scrollPadding != null),
assert(dragStartBehavior != null),
assert(selectionHeightStyle != null),
assert(selectionWidthStyle != null),
assert(maxLines == null || maxLines > 0),
assert(minLines == null || minLines > 0),
assert(
@ -530,6 +541,16 @@ class CupertinoTextField extends StatefulWidget {
/// and [CupertinoColors.activeOrange] in the dark theme.
final Color cursorColor;
/// Controls how tall the selection highlight boxes are computed to be.
///
/// See [ui.BoxHeightStyle] for details on available styles.
final ui.BoxHeightStyle selectionHeightStyle;
/// Controls how wide the selection highlight boxes are computed to be.
///
/// See [ui.BoxWidthStyle] for details on available styles.
final ui.BoxWidthStyle selectionWidthStyle;
/// The appearance of the keyboard.
///
/// This setting is only honored on iOS devices.
@ -918,6 +939,8 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with AutomaticK
cursorOffset: cursorOffset,
paintCursorAboveText: true,
backgroundCursorColor: CupertinoDynamicColor.resolve(CupertinoColors.inactiveGray, context),
selectionHeightStyle: widget.selectionHeightStyle,
selectionWidthStyle: widget.selectionWidthStyle,
scrollPadding: widget.scrollPadding,
keyboardAppearance: keyboardAppearance,
dragStartBehavior: widget.dragStartBehavior,

View file

@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle;
import 'package:flutter/cupertino.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
@ -279,9 +281,15 @@ class TextField extends StatefulWidget {
/// The text cursor is not shown if [showCursor] is false or if [showCursor]
/// is null (the default) and [readOnly] is true.
///
/// The [selectionHeightStyle] and [selectionWidthStyle] properties allow
/// changing the shape of the selection highlighting. These properties default
/// to [ui.BoxHeightStyle.tight] and [ui.BoxWidthStyle.tight] respectively and
/// must not be null.
///
/// The [textAlign], [autofocus], [obscureText], [readOnly], [autocorrect],
/// [maxLengthEnforced], [scrollPadding], [maxLines], [maxLength],
/// and [enableSuggestions] arguments must not be null.
/// [selectionHeightStyle], [selectionWidthStyle], and [enableSuggestions]
/// arguments must not be null.
///
/// See also:
///
@ -322,6 +330,8 @@ class TextField extends StatefulWidget {
this.cursorWidth = 2.0,
this.cursorRadius,
this.cursorColor,
this.selectionHeightStyle = ui.BoxHeightStyle.tight,
this.selectionWidthStyle = ui.BoxWidthStyle.tight,
this.keyboardAppearance,
this.scrollPadding = const EdgeInsets.all(20.0),
this.dragStartBehavior = DragStartBehavior.start,
@ -342,6 +352,8 @@ class TextField extends StatefulWidget {
assert(maxLengthEnforced != null),
assert(scrollPadding != null),
assert(dragStartBehavior != null),
assert(selectionHeightStyle != null),
assert(selectionWidthStyle != null),
assert(maxLines == null || maxLines > 0),
assert(minLines == null || minLines > 0),
assert(
@ -603,6 +615,16 @@ class TextField extends StatefulWidget {
/// depending on [ThemeData.platform].
final Color cursorColor;
/// Controls how tall the selection highlight boxes are computed to be.
///
/// See [ui.BoxHeightStyle] for details on available styles.
final ui.BoxHeightStyle selectionHeightStyle;
/// Controls how wide the selection highlight boxes are computed to be.
///
/// See [ui.BoxWidthStyle] for details on available styles.
final ui.BoxWidthStyle selectionWidthStyle;
/// The appearance of the keyboard.
///
/// This setting is only honored on iOS devices.
@ -1003,6 +1025,8 @@ class _TextFieldState extends State<TextField> implements TextSelectionGestureDe
cursorWidth: widget.cursorWidth,
cursorRadius: cursorRadius,
cursorColor: cursorColor,
selectionHeightStyle: widget.selectionHeightStyle,
selectionWidthStyle: widget.selectionWidthStyle,
cursorOpacityAnimates: cursorOpacityAnimates,
cursorOffset: cursorOffset,
paintCursorAboveText: paintCursorAboveText,

View file

@ -3,7 +3,7 @@
// found in the LICENSE file.
import 'dart:math' show min, max;
import 'dart:ui' as ui show Paragraph, ParagraphBuilder, ParagraphConstraints, ParagraphStyle, PlaceholderAlignment, LineMetrics, TextHeightBehavior;
import 'dart:ui' as ui show Paragraph, ParagraphBuilder, ParagraphConstraints, ParagraphStyle, PlaceholderAlignment, LineMetrics, TextHeightBehavior, BoxHeightStyle, BoxWidthStyle;
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
@ -808,12 +808,28 @@ class TextPainter {
/// Returns a list of rects that bound the given selection.
///
/// The [boxHeightStyle] and [boxWidthStyle] arguments may be used to select
/// the shape of the [TextBox]s. These properties default to
/// [ui.BoxHeightStyle.tight] and [ui.BoxWidthStyle.tight] respectively and
/// must not be null.
///
/// A given selection might have more than one rect if this text painter
/// contains bidirectional text because logically contiguous text might not be
/// visually contiguous.
List<TextBox> getBoxesForSelection(TextSelection selection) {
List<TextBox> getBoxesForSelection(
TextSelection selection, {
ui.BoxHeightStyle boxHeightStyle = ui.BoxHeightStyle.tight,
ui.BoxWidthStyle boxWidthStyle = ui.BoxWidthStyle.tight,
}) {
assert(!_needsLayout);
return _paragraph.getBoxesForRange(selection.start, selection.end);
assert(boxHeightStyle != null);
assert(boxWidthStyle != null);
return _paragraph.getBoxesForRange(
selection.start,
selection.end,
boxHeightStyle: boxHeightStyle,
boxWidthStyle: boxWidthStyle
);
}
/// Returns the position within the text for the given pixel offset.

View file

@ -3,7 +3,7 @@
// found in the LICENSE file.
import 'dart:math' as math;
import 'dart:ui' as ui show TextBox, lerpDouble;
import 'dart:ui' as ui show TextBox, lerpDouble, BoxHeightStyle, BoxWidthStyle;
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
@ -208,6 +208,8 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
bool paintCursorAboveText = false,
Offset cursorOffset,
double devicePixelRatio = 1.0,
ui.BoxHeightStyle selectionHeightStyle = ui.BoxHeightStyle.tight,
ui.BoxWidthStyle selectionWidthStyle = ui.BoxWidthStyle.tight,
bool enableInteractiveSelection,
EdgeInsets floatingCursorAddedMargin = const EdgeInsets.fromLTRB(4, 4, 4, 5),
@required this.textSelectionDelegate,
@ -237,6 +239,8 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
assert(readOnly != null),
assert(forceLine != null),
assert(devicePixelRatio != null),
assert(selectionHeightStyle != null),
assert(selectionWidthStyle != null),
_textPainter = TextPainter(
text: text,
textAlign: textAlign,
@ -262,6 +266,8 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
_floatingCursorAddedMargin = floatingCursorAddedMargin,
_enableInteractiveSelection = enableInteractiveSelection,
_devicePixelRatio = devicePixelRatio,
_selectionHeightStyle = selectionHeightStyle,
_selectionWidthStyle = selectionWidthStyle,
_startHandleLayerLink = startHandleLayerLink,
_endHandleLayerLink = endHandleLayerLink,
_obscureText = obscureText,
@ -1094,6 +1100,32 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
Offset _floatingCursorOffset;
TextPosition _floatingCursorTextPosition;
/// Controls how tall the selection highlight boxes are computed to be.
///
/// See [ui.BoxHeightStyle] for details on available styles.
ui.BoxHeightStyle get selectionHeightStyle => _selectionHeightStyle;
ui.BoxHeightStyle _selectionHeightStyle;
set selectionHeightStyle(ui.BoxHeightStyle value) {
assert(value != null);
if (_selectionHeightStyle == value)
return;
_selectionHeightStyle = value;
markNeedsPaint();
}
/// Controls how wide the selection highlight boxes are computed to be.
///
/// See [ui.BoxWidthStyle] for details on available styles.
ui.BoxWidthStyle get selectionWidthStyle => _selectionWidthStyle;
ui.BoxWidthStyle _selectionWidthStyle;
set selectionWidthStyle(ui.BoxWidthStyle value) {
assert(value != null);
if (_selectionWidthStyle == value)
return;
_selectionWidthStyle = value;
markNeedsPaint();
}
/// If false, [describeSemanticsConfiguration] will not set the
/// configuration's cursor motion or set selection callbacks.
///
@ -1941,7 +1973,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
}
if (showSelection) {
_selectionRects ??= _textPainter.getBoxesForSelection(_selection);
_selectionRects ??= _textPainter.getBoxesForSelection(_selection, boxHeightStyle: _selectionHeightStyle, boxWidthStyle: _selectionWidthStyle);
_paintSelection(context.canvas, effectiveOffset);
}

View file

@ -342,10 +342,10 @@ class EditableText extends StatefulWidget {
/// The [controller], [focusNode], [obscureText], [autocorrect], [autofocus],
/// [showSelectionHandles], [enableInteractiveSelection], [forceLine],
/// [style], [cursorColor], [cursorOpacityAnimates],[backgroundCursorColor],
/// [enableSuggestions], [paintCursorAboveText], [textAlign],
/// [dragStartBehavior], [scrollPadding], [dragStartBehavior],
/// [toolbarOptions], [rendererIgnoresPointer], and [readOnly] arguments must
/// not be null.
/// [enableSuggestions], [paintCursorAboveText], [selectionHeightStyle],
/// [selectionWidthStyle], [textAlign], [dragStartBehavior], [scrollPadding],
/// [dragStartBehavior], [toolbarOptions], [rendererIgnoresPointer], and
/// [readOnly] arguments must not be null.
EditableText({
Key key,
@required this.controller,
@ -389,6 +389,8 @@ class EditableText extends StatefulWidget {
this.cursorOpacityAnimates = false,
this.cursorOffset,
this.paintCursorAboveText = false,
this.selectionHeightStyle = ui.BoxHeightStyle.tight,
this.selectionWidthStyle = ui.BoxWidthStyle.tight,
this.scrollPadding = const EdgeInsets.all(20.0),
this.keyboardAppearance = Brightness.light,
this.dragStartBehavior = DragStartBehavior.start,
@ -417,6 +419,8 @@ class EditableText extends StatefulWidget {
assert(cursorOpacityAnimates != null),
assert(paintCursorAboveText != null),
assert(backgroundCursorColor != null),
assert(selectionHeightStyle != null),
assert(selectionWidthStyle != null),
assert(textAlign != null),
assert(maxLines == null || maxLines > 0),
assert(minLines == null || minLines > 0),
@ -979,6 +983,16 @@ class EditableText extends StatefulWidget {
///{@macro flutter.rendering.editable.paintCursorOnTop}
final bool paintCursorAboveText;
/// Controls how tall the selection highlight boxes are computed to be.
///
/// See [ui.BoxHeightStyle] for details on available styles.
final ui.BoxHeightStyle selectionHeightStyle;
/// Controls how wide the selection highlight boxes are computed to be.
///
/// See [ui.BoxWidthStyle] for details on available styles.
final ui.BoxWidthStyle selectionWidthStyle;
/// The appearance of the keyboard.
///
/// This setting is only honored on iOS devices.
@ -1894,6 +1908,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
cursorWidth: widget.cursorWidth,
cursorRadius: widget.cursorRadius,
cursorOffset: widget.cursorOffset,
selectionHeightStyle: widget.selectionHeightStyle,
selectionWidthStyle: widget.selectionWidthStyle,
paintCursorAboveText: widget.paintCursorAboveText,
enableInteractiveSelection: widget.enableInteractiveSelection,
textSelectionDelegate: this,
@ -1962,9 +1978,11 @@ class _Editable extends LeafRenderObjectWidget {
this.cursorWidth,
this.cursorRadius,
this.cursorOffset,
this.paintCursorAboveText,
this.selectionHeightStyle = ui.BoxHeightStyle.tight,
this.selectionWidthStyle = ui.BoxWidthStyle.tight,
this.enableInteractiveSelection = true,
this.textSelectionDelegate,
this.paintCursorAboveText,
this.devicePixelRatio,
}) : assert(textDirection != null),
assert(rendererIgnoresPointer != null),
@ -2002,10 +2020,12 @@ class _Editable extends LeafRenderObjectWidget {
final double cursorWidth;
final Radius cursorRadius;
final Offset cursorOffset;
final bool paintCursorAboveText;
final ui.BoxHeightStyle selectionHeightStyle;
final ui.BoxWidthStyle selectionWidthStyle;
final bool enableInteractiveSelection;
final TextSelectionDelegate textSelectionDelegate;
final double devicePixelRatio;
final bool paintCursorAboveText;
@override
RenderEditable createRenderObject(BuildContext context) {
@ -2039,6 +2059,8 @@ class _Editable extends LeafRenderObjectWidget {
cursorRadius: cursorRadius,
cursorOffset: cursorOffset,
paintCursorAboveText: paintCursorAboveText,
selectionHeightStyle: selectionHeightStyle,
selectionWidthStyle: selectionWidthStyle,
enableInteractiveSelection: enableInteractiveSelection,
textSelectionDelegate: textSelectionDelegate,
devicePixelRatio: devicePixelRatio,
@ -2075,6 +2097,8 @@ class _Editable extends LeafRenderObjectWidget {
..cursorWidth = cursorWidth
..cursorRadius = cursorRadius
..cursorOffset = cursorOffset
..selectionHeightStyle = selectionHeightStyle
..selectionWidthStyle = selectionWidthStyle
..textSelectionDelegate = textSelectionDelegate
..devicePixelRatio = devicePixelRatio
..paintCursorAboveText = paintCursorAboveText;

View file

@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'dart:async';
import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle, Color;
import 'package:flutter/cupertino.dart';
import 'package:flutter/rendering.dart';
@ -3989,4 +3990,96 @@ void main() {
),
);
});
testWidgets('text selection style 1', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure\nhi\nwassssup!',
);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: RepaintBoundary(
child: Container(
width: 650.0,
height: 600.0,
decoration: const BoxDecoration(
color: Color(0xff00ff00),
),
child: Column(
children: <Widget>[
CupertinoTextField(
key: const Key('field0'),
controller: controller,
style: const TextStyle(height: 4, color: ui.Color.fromARGB(100, 0, 0, 0)),
toolbarOptions: const ToolbarOptions(selectAll: true),
selectionHeightStyle: ui.BoxHeightStyle.includeLineSpacingTop,
selectionWidthStyle: ui.BoxWidthStyle.max,
maxLines: 3,
),
],
),
),
),
),
),
);
final Offset textfieldStart = tester.getTopLeft(find.byKey(const Key('field0')));
await tester.longPressAt(textfieldStart + const Offset(50.0, 2.0));
await tester.pump(const Duration(milliseconds: 150));
await tester.tapAt(textfieldStart + const Offset(20.0, 146.0));
await tester.pump(const Duration(milliseconds: 300));
await expectLater(
find.byType(CupertinoApp),
matchesGoldenFile('text_field_golden.TextSelectionStyle.1.png'),
);
});
testWidgets('text selection style 2', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure\nhi\nwassssup!',
);
await tester.pumpWidget(
CupertinoApp(
home: Center(
child: RepaintBoundary(
child: Container(
width: 650.0,
height: 600.0,
decoration: const BoxDecoration(
color: Color(0xff00ff00),
),
child: Column(
children: <Widget>[
CupertinoTextField(
key: const Key('field0'),
controller: controller,
style: const TextStyle(height: 4, color: ui.Color.fromARGB(100, 0, 0, 0)),
toolbarOptions: const ToolbarOptions(selectAll: true),
selectionHeightStyle: ui.BoxHeightStyle.includeLineSpacingBottom,
selectionWidthStyle: ui.BoxWidthStyle.tight,
maxLines: 3,
),
],
),
),
),
),
),
);
final Offset textfieldStart = tester.getTopLeft(find.byKey(const Key('field0')));
await tester.longPressAt(textfieldStart + const Offset(50.0, 2.0));
await tester.pump(const Duration(milliseconds: 150));
await tester.tapAt(textfieldStart + const Offset(20.0, 146.0));
await tester.pump(const Duration(milliseconds: 300));
await expectLater(
find.byType(CupertinoApp),
matchesGoldenFile('text_field_golden.TextSelectionStyle.2.png'),
);
});
}

View file

@ -5,7 +5,7 @@
@TestOn('!chrome') // This whole test suite needs triage.
import 'dart:async';
import 'dart:math' as math;
import 'dart:ui' as ui show window;
import 'dart:ui' as ui show window, BoxHeightStyle, BoxWidthStyle;
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
@ -489,21 +489,21 @@ void main() {
testWidgets('text field toolbar options correctly changes options',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
controller: controller,
toolbarOptions: const ToolbarOptions(copy: true),
),
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: TextField(
controller: controller,
toolbarOptions: const ToolbarOptions(copy: true),
),
),
),
);
),
);
final Offset textfieldStart = tester.getTopLeft(find.byType(TextField));
@ -536,6 +536,102 @@ void main() {
expect(find.text('Select All'), findsNothing);
}, skip: isBrowser, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('text selection style 1', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure\nhi\nwasssup!',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: RepaintBoundary(
child: Container(
width: 650.0,
height: 600.0,
decoration: const BoxDecoration(
color: Color(0xff00ff00),
),
child: Column(
children: <Widget>[
TextField(
key: const Key('field0'),
controller: controller,
style: const TextStyle(height: 4, color: Colors.black45),
toolbarOptions: const ToolbarOptions(copy: true, selectAll: true),
selectionHeightStyle: ui.BoxHeightStyle.includeLineSpacingTop,
selectionWidthStyle: ui.BoxWidthStyle.max,
maxLines: 3,
),
],
),
),
),
),
),
),
);
final Offset textfieldStart = tester.getTopLeft(find.byKey(const Key('field0')));
await tester.longPressAt(textfieldStart + const Offset(50.0, 2.0));
await tester.pump(const Duration(milliseconds: 50));
await tester.tapAt(textfieldStart + const Offset(100.0, 107.0));
await tester.pump(const Duration(milliseconds: 300));
await expectLater(
find.byType(MaterialApp),
matchesGoldenFile('text_field_golden.TextSelectionStyle.1.png'),
);
});
testWidgets('text selection style 2', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'Atwater Peel Sherbrooke Bonaventure\nhi\nwasssup!',
);
await tester.pumpWidget(
MaterialApp(
home: Material(
child: Center(
child: RepaintBoundary(
child: Container(
width: 650.0,
height: 600.0,
decoration: const BoxDecoration(
color: Color(0xff00ff00),
),
child: Column(
children: <Widget>[
TextField(
key: const Key('field0'),
controller: controller,
style: const TextStyle(height: 4, color: Colors.black45),
toolbarOptions: const ToolbarOptions(copy: true, selectAll: true),
selectionHeightStyle: ui.BoxHeightStyle.includeLineSpacingBottom,
selectionWidthStyle: ui.BoxWidthStyle.tight,
maxLines: 3,
),
],
),
),
),
),
),
),
);
final Offset textfieldStart = tester.getTopLeft(find.byKey(const Key('field0')));
await tester.longPressAt(textfieldStart + const Offset(50.0, 2.0));
await tester.pump(const Duration(milliseconds: 50));
await tester.tapAt(textfieldStart + const Offset(100.0, 107.0));
await tester.pump(const Duration(milliseconds: 300));
await expectLater(
find.byType(MaterialApp),
matchesGoldenFile('text_field_golden.TextSelectionStyle.2.png'),
);
});
testWidgets('text field toolbar options correctly changes options',
(WidgetTester tester) async {
final TextEditingController controller = TextEditingController(