Skip to content

Commit 6781576

Browse files
authored
Reland iOS 16 context menu (#117234)
Updates the iOS text selection toolbar to look like iOS 16 (reland)
1 parent abd5217 commit 6781576

File tree

5 files changed

+134
-28
lines changed

5 files changed

+134
-28
lines changed

packages/flutter/lib/src/cupertino/colors.dart

+1-1
Original file line numberDiff line numberDiff line change
@@ -990,7 +990,7 @@ class CupertinoDynamicColor extends Color with Diagnosticable {
990990
CupertinoDynamicColor resolveFrom(BuildContext context) {
991991
Brightness brightness = Brightness.light;
992992
if (_isPlatformBrightnessDependent) {
993-
brightness = CupertinoTheme.maybeBrightnessOf(context) ?? Brightness.light;
993+
brightness = CupertinoTheme.maybeBrightnessOf(context) ?? Brightness.light;
994994
}
995995
bool isHighContrastEnabled = false;
996996
if (_isHighContrastDependent) {

packages/flutter/lib/src/cupertino/text_selection_toolbar.dart

+36-8
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55
import 'dart:collection';
66
import 'dart:ui' as ui;
77

8-
import 'package:flutter/foundation.dart' show clampDouble;
8+
import 'package:flutter/foundation.dart' show Brightness, clampDouble;
99
import 'package:flutter/rendering.dart';
1010
import 'package:flutter/widgets.dart';
1111

12+
import 'colors.dart';
1213
import 'text_selection_toolbar_button.dart';
14+
import 'theme.dart';
1315

1416
// Values extracted from https://developer.apple.com/design/resources/.
1517
// The height of the toolbar, including the arrow.
@@ -29,9 +31,27 @@ const double _kArrowScreenPadding = 26.0;
2931
// Values extracted from https://developer.apple.com/design/resources/.
3032
const Radius _kToolbarBorderRadius = Radius.circular(8);
3133

32-
// Colors extracted from https://developer.apple.com/design/resources/.
33-
// TODO(LongCatIsLooong): https://github.com/flutter/flutter/issues/41507.
34-
const Color _kToolbarDividerColor = Color(0xFF808080);
34+
const CupertinoDynamicColor _kToolbarDividerColor = CupertinoDynamicColor.withBrightness(
35+
// This value was extracted from a screenshot of iOS 16.0.3, as light mode
36+
// didn't appear in the Apple design resources assets linked below.
37+
color: Color(0xFFB6B6B6),
38+
// Color extracted from https://developer.apple.com/design/resources/.
39+
// TODO(LongCatIsLooong): https://github.com/flutter/flutter/issues/41507.
40+
darkColor: Color(0xFF808080),
41+
);
42+
43+
// These values were extracted from a screenshot of iOS 16.0.3, as light mode
44+
// didn't appear in the Apple design resources assets linked above.
45+
final BoxDecoration _kToolbarShadow = BoxDecoration(
46+
borderRadius: const BorderRadius.all(_kToolbarBorderRadius),
47+
boxShadow: <BoxShadow>[
48+
BoxShadow(
49+
color: CupertinoColors.black.withOpacity(0.1),
50+
blurRadius: 16.0,
51+
offset: Offset(0, _kToolbarArrowSize.height / 2),
52+
),
53+
],
54+
);
3555

3656
/// The type for a Function that builds a toolbar's container with the given
3757
/// child.
@@ -119,14 +139,23 @@ class CupertinoTextSelectionToolbar extends StatelessWidget {
119139
// Builds a toolbar just like the default iOS toolbar, with the right color
120140
// background and a rounded cutout with an arrow.
121141
static Widget _defaultToolbarBuilder(BuildContext context, Offset anchor, bool isAbove, Widget child) {
122-
return _CupertinoTextSelectionToolbarShape(
142+
final Widget outputChild = _CupertinoTextSelectionToolbarShape(
123143
anchor: anchor,
124144
isAbove: isAbove,
125145
child: DecoratedBox(
126-
decoration: const BoxDecoration(color: _kToolbarDividerColor),
146+
decoration: BoxDecoration(
147+
color: _kToolbarDividerColor.resolveFrom(context),
148+
),
127149
child: child,
128150
),
129151
);
152+
if (CupertinoTheme.brightnessOf(context) == Brightness.dark) {
153+
return outputChild;
154+
}
155+
return DecoratedBox(
156+
decoration: _kToolbarShadow,
157+
child: outputChild,
158+
);
130159
}
131160

132161
@override
@@ -226,7 +255,6 @@ class _RenderCupertinoTextSelectionToolbarShape extends RenderShiftedBox {
226255
super.child,
227256
);
228257

229-
230258
@override
231259
bool get isRepaintBoundary => true;
232260

@@ -485,7 +513,7 @@ class _CupertinoTextSelectionToolbarContentState extends State<_CupertinoTextSel
485513
onPressed: _handleNextPage,
486514
text: '▶',
487515
),
488-
nextButtonDisabled: CupertinoTextSelectionToolbarButton.text(
516+
nextButtonDisabled: const CupertinoTextSelectionToolbarButton.text(
489517
text: '▶',
490518
),
491519
children: widget.children,

packages/flutter/lib/src/cupertino/text_selection_toolbar_button.dart

+29-16
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,17 @@ const TextStyle _kToolbarButtonFontStyle = TextStyle(
1818

1919
// Colors extracted from https://developer.apple.com/design/resources/.
2020
// TODO(LongCatIsLooong): https://github.com/flutter/flutter/issues/41507.
21-
const Color _kToolbarBackgroundColor = Color(0xEB202020);
21+
const CupertinoDynamicColor _kToolbarBackgroundColor = CupertinoDynamicColor.withBrightness(
22+
// This value was extracted from a screenshot of iOS 16.0.3, as light mode
23+
// didn't appear in the Apple design resources assets linked above.
24+
color: Color(0xEBF7F7F7),
25+
darkColor: Color(0xEB202020),
26+
);
27+
28+
const CupertinoDynamicColor _kToolbarTextColor = CupertinoDynamicColor.withBrightness(
29+
color: CupertinoColors.black,
30+
darkColor: CupertinoColors.white,
31+
);
2232

2333
// Eyeballed value.
2434
const EdgeInsets _kToolbarButtonPadding = EdgeInsets.symmetric(vertical: 16.0, horizontal: 18.0);
@@ -33,22 +43,17 @@ class CupertinoTextSelectionToolbarButton extends StatelessWidget {
3343
this.onPressed,
3444
required Widget this.child,
3545
}) : assert(child != null),
46+
text = null,
3647
buttonItem = null;
3748

3849
/// Create an instance of [CupertinoTextSelectionToolbarButton] whose child is
3950
/// a [Text] widget styled like the default iOS text selection toolbar button.
40-
CupertinoTextSelectionToolbarButton.text({
51+
const CupertinoTextSelectionToolbarButton.text({
4152
super.key,
4253
this.onPressed,
43-
required String text,
54+
required this.text,
4455
}) : buttonItem = null,
45-
child = Text(
46-
text,
47-
overflow: TextOverflow.ellipsis,
48-
style: _kToolbarButtonFontStyle.copyWith(
49-
color: onPressed != null ? CupertinoColors.white : CupertinoColors.inactiveGray,
50-
),
51-
);
56+
child = null;
5257

5358
/// Create an instance of [CupertinoTextSelectionToolbarButton] from the given
5459
/// [ContextMenuButtonItem].
@@ -59,6 +64,7 @@ class CupertinoTextSelectionToolbarButton extends StatelessWidget {
5964
required ContextMenuButtonItem this.buttonItem,
6065
}) : assert(buttonItem != null),
6166
child = null,
67+
text = null,
6268
onPressed = buttonItem.onPressed;
6369

6470
/// {@template flutter.cupertino.CupertinoTextSelectionToolbarButton.child}
@@ -79,6 +85,10 @@ class CupertinoTextSelectionToolbarButton extends StatelessWidget {
7985
/// {@endtemplate}
8086
final ContextMenuButtonItem? buttonItem;
8187

88+
/// The text used in the button's label when using
89+
/// [CupertinoTextSelectionToolbarButton.text].
90+
final String? text;
91+
8292
/// Returns the default button label String for the button of the given
8393
/// [ContextMenuButtonItem]'s [ContextMenuButtonType].
8494
static String getButtonLabel(BuildContext context, ContextMenuButtonItem buttonItem) {
@@ -106,12 +116,15 @@ class CupertinoTextSelectionToolbarButton extends StatelessWidget {
106116
@override
107117
Widget build(BuildContext context) {
108118
final Widget child = this.child ?? Text(
109-
getButtonLabel(context, buttonItem!),
110-
overflow: TextOverflow.ellipsis,
111-
style: _kToolbarButtonFontStyle.copyWith(
112-
color: onPressed != null ? CupertinoColors.white : CupertinoColors.inactiveGray,
113-
),
114-
);
119+
text ?? getButtonLabel(context, buttonItem!),
120+
overflow: TextOverflow.ellipsis,
121+
style: _kToolbarButtonFontStyle.copyWith(
122+
color: onPressed != null
123+
? _kToolbarTextColor.resolveFrom(context)
124+
: CupertinoColors.inactiveGray,
125+
),
126+
);
127+
115128
return CupertinoButton(
116129
borderRadius: null,
117130
color: _kToolbarBackgroundColor,

packages/flutter/test/cupertino/text_field_test.dart

+3-3
Original file line numberDiff line numberDiff line change
@@ -1500,7 +1500,7 @@ void main() {
15001500
expect(controller.text, 'abcdef');
15011501
});
15021502

1503-
testWidgets('toolbar has the same visual regardless of theming', (WidgetTester tester) async {
1503+
testWidgets('toolbar colors change with theme brightness, but nothing else', (WidgetTester tester) async {
15041504
final TextEditingController controller = TextEditingController(
15051505
text: "j'aime la poutine",
15061506
);
@@ -1524,7 +1524,7 @@ void main() {
15241524
await tester.pump(const Duration(milliseconds: 200));
15251525

15261526
Text text = tester.widget<Text>(find.text('Paste'));
1527-
expect(text.style!.color, CupertinoColors.white);
1527+
expect(text.style!.color!.value, CupertinoColors.black.value);
15281528
expect(text.style!.fontSize, 14);
15291529
expect(text.style!.letterSpacing, -0.15);
15301530
expect(text.style!.fontWeight, FontWeight.w400);
@@ -1556,7 +1556,7 @@ void main() {
15561556

15571557
text = tester.widget<Text>(find.text('Paste'));
15581558
// The toolbar buttons' text are still the same style.
1559-
expect(text.style!.color, CupertinoColors.white);
1559+
expect(text.style!.color!.value, CupertinoColors.white.value);
15601560
expect(text.style!.fontSize, 14);
15611561
expect(text.style!.letterSpacing, -0.15);
15621562
expect(text.style!.fontWeight, FontWeight.w400);

packages/flutter/test/cupertino/text_selection_toolbar_test.dart

+65
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ class TestBox extends SizedBox {
6060
static const double itemWidth = 100.0;
6161
}
6262

63+
const CupertinoDynamicColor _kToolbarBackgroundColor = CupertinoDynamicColor.withBrightness(
64+
color: Color(0xEBF7F7F7),
65+
darkColor: Color(0xEB202020),
66+
);
67+
6368
void main() {
6469
TestWidgetsFlutterBinding.ensureInitialized();
6570

@@ -289,4 +294,64 @@ void main() {
289294
expect(find.text('Paste'), findsNothing);
290295
expect(find.text('Select all'), findsNothing);
291296
}, skip: kIsWeb); // [intended] We do not use Flutter-rendered context menu on the Web.
297+
298+
for (final Brightness? themeBrightness in <Brightness?>[...Brightness.values, null]) {
299+
for (final Brightness? mediaBrightness in <Brightness?>[...Brightness.values, null]) {
300+
testWidgets('draws dark buttons in dark mode and light button in light mode when theme is $themeBrightness and MediaQuery is $mediaBrightness', (WidgetTester tester) async {
301+
await tester.pumpWidget(
302+
CupertinoApp(
303+
theme: CupertinoThemeData(
304+
brightness: themeBrightness,
305+
),
306+
home: Center(
307+
child: Builder(
308+
builder: (BuildContext context) {
309+
return MediaQuery(
310+
data: MediaQuery.of(context).copyWith(platformBrightness: mediaBrightness),
311+
child: CupertinoTextSelectionToolbar(
312+
anchorAbove: const Offset(100.0, 0.0),
313+
anchorBelow: const Offset(100.0, 0.0),
314+
children: <Widget>[
315+
CupertinoTextSelectionToolbarButton.text(
316+
onPressed: () {},
317+
text: 'Button',
318+
),
319+
],
320+
),
321+
);
322+
},
323+
),
324+
),
325+
),
326+
);
327+
328+
final Finder buttonFinder = find.byType(CupertinoButton);
329+
expect(buttonFinder, findsOneWidget);
330+
331+
final Finder decorationFinder = find.descendant(
332+
of: find.byType(CupertinoButton),
333+
matching: find.byType(DecoratedBox)
334+
);
335+
expect(decorationFinder, findsOneWidget);
336+
final DecoratedBox decoratedBox = tester.widget(decorationFinder);
337+
final BoxDecoration boxDecoration = decoratedBox.decoration as BoxDecoration;
338+
339+
// Theme brightness is preferred, otherwise MediaQuery brightness is
340+
// used. If both are null, defaults to light.
341+
late final Brightness effectiveBrightness;
342+
if (themeBrightness != null) {
343+
effectiveBrightness = themeBrightness;
344+
} else {
345+
effectiveBrightness = mediaBrightness ?? Brightness.light;
346+
}
347+
348+
expect(
349+
boxDecoration.color!.value,
350+
effectiveBrightness == Brightness.dark
351+
? _kToolbarBackgroundColor.darkColor.value
352+
: _kToolbarBackgroundColor.color.value,
353+
);
354+
}, skip: kIsWeb); // [intended] We do not use Flutter-rendered context menu on the Web.
355+
}
356+
}
292357
}

0 commit comments

Comments
 (0)