From f86b9220a38b159bd0db6de88bbb8c8110fae589 Mon Sep 17 00:00:00 2001 From: Luccas Clezar Date: Fri, 21 Apr 2023 20:03:34 -0300 Subject: [PATCH] Update Cupertino desktop text selection toolbar (#121829) Visual fidelity of the right-click context menu on MacOS. --- .../src/cupertino/desktop_text_selection.dart | 1 - .../desktop_text_selection_toolbar.dart | 104 ++++++++++++---- ...desktop_text_selection_toolbar_button.dart | 81 +++++++------ .../text_selection_toolbar_button.dart | 2 + .../adaptive_text_selection_toolbar.dart | 1 - ...op_text_selection_toolbar_button_test.dart | 44 ++++++- .../desktop_text_selection_toolbar_test.dart | 111 +++++++++++++++++- 7 files changed, 278 insertions(+), 66 deletions(-) diff --git a/packages/flutter/lib/src/cupertino/desktop_text_selection.dart b/packages/flutter/lib/src/cupertino/desktop_text_selection.dart index 40ff6c20a6..7b586efd97 100644 --- a/packages/flutter/lib/src/cupertino/desktop_text_selection.dart +++ b/packages/flutter/lib/src/cupertino/desktop_text_selection.dart @@ -182,7 +182,6 @@ class _CupertinoDesktopTextSelectionControlsToolbarState extends State<_Cupertin } items.add(CupertinoDesktopTextSelectionToolbarButton.text( - context: context, onPressed: onPressed, text: text, )); diff --git a/packages/flutter/lib/src/cupertino/desktop_text_selection_toolbar.dart b/packages/flutter/lib/src/cupertino/desktop_text_selection_toolbar.dart index 5a586c80b2..a72f2ae6aa 100644 --- a/packages/flutter/lib/src/cupertino/desktop_text_selection_toolbar.dart +++ b/packages/flutter/lib/src/cupertino/desktop_text_selection_toolbar.dart @@ -2,6 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'colors.dart'; @@ -10,23 +13,33 @@ import 'colors.dart'; // the screen. const double _kToolbarScreenPadding = 8.0; -// These values were measured from a screenshot of TextEdit on macOS 10.15.7 on -// a Macbook Pro. +// These values were measured from a screenshot of the native context menu on +// macOS 13.2 on a Macbook Pro. +const double _kToolbarSaturationBoost = 3; +const double _kToolbarBlurSigma = 20; const double _kToolbarWidth = 222.0; -const Radius _kToolbarBorderRadius = Radius.circular(4.0); -const EdgeInsets _kToolbarPadding = EdgeInsets.symmetric( - vertical: 3.0, -); +const Radius _kToolbarBorderRadius = Radius.circular(8.0); +const EdgeInsets _kToolbarPadding = EdgeInsets.all(6.0); +const List _kToolbarShadow = [ + BoxShadow( + color: Color.fromARGB(60, 0, 0, 0), + blurRadius: 10.0, + spreadRadius: 0.5, + offset: Offset(0.0, 4.0), + ), +]; -// These values were measured from a screenshot of TextEdit on macOS 10.16 on a -// Macbook Pro. -const CupertinoDynamicColor _kToolbarBorderColor = CupertinoDynamicColor.withBrightness( - color: Color(0xFFBBBBBB), - darkColor: Color(0xFF505152), +// These values were measured from a screenshot of the native context menu on +// macOS 13.2 on a Macbook Pro. +const CupertinoDynamicColor _kToolbarBorderColor = + CupertinoDynamicColor.withBrightness( + color: Color(0xFFB8B8B8), + darkColor: Color(0xFF5B5B5B), ); -const CupertinoDynamicColor _kToolbarBackgroundColor = CupertinoDynamicColor.withBrightness( - color: Color(0xffECE8E6), - darkColor: Color(0xff302928), +const CupertinoDynamicColor _kToolbarBackgroundColor = + CupertinoDynamicColor.withBrightness( + color: Color(0xB2FFFFFF), + darkColor: Color(0xB2303030), ); /// A macOS-style text selection toolbar. @@ -53,6 +66,23 @@ class CupertinoDesktopTextSelectionToolbar extends StatelessWidget { required this.children, }) : assert(children.length > 0); + /// Creates a 5x5 matrix that increases saturation when used with [ColorFilter.matrix]. + /// + /// The numbers were taken from this comment: + /// [Cupertino blurs should boost saturation](https://github.com/flutter/flutter/issues/29483#issuecomment-477334981). + static List _matrixWithSaturation(double saturation) { + final double r = 0.213 * (1 - saturation); + final double g = 0.715 * (1 - saturation); + final double b = 0.072 * (1 - saturation); + + return [ + r + saturation, g, b, 0, 0, // + r, g + saturation, b, 0, 0, // + r, g, b + saturation, 0, 0, // + 0, 0, 0, 1, 0, // + ]; + } + /// {@macro flutter.material.DesktopTextSelectionToolbar.anchor} final Offset anchor; @@ -68,16 +98,41 @@ class CupertinoDesktopTextSelectionToolbar extends StatelessWidget { static Widget _defaultToolbarBuilder(BuildContext context, Widget child) { return Container( width: _kToolbarWidth, - decoration: BoxDecoration( - color: _kToolbarBackgroundColor.resolveFrom(context), - border: Border.all( - color: _kToolbarBorderColor.resolveFrom(context), - ), - borderRadius: const BorderRadius.all(_kToolbarBorderRadius), + clipBehavior: Clip.hardEdge, + decoration: const BoxDecoration( + boxShadow: _kToolbarShadow, + borderRadius: BorderRadius.all(_kToolbarBorderRadius), ), - child: Padding( - padding: _kToolbarPadding, - child: child, + child: BackdropFilter( + // Flutter web doesn't support ImageFilter.compose on CanvasKit yet + // (https://github.com/flutter/flutter/issues/120123). + filter: kIsWeb + ? ImageFilter.blur( + sigmaX: _kToolbarBlurSigma, + sigmaY: _kToolbarBlurSigma, + ) + : ImageFilter.compose( + outer: ColorFilter.matrix( + _matrixWithSaturation(_kToolbarSaturationBoost), + ), + inner: ImageFilter.blur( + sigmaX: _kToolbarBlurSigma, + sigmaY: _kToolbarBlurSigma, + ), + ), + child: DecoratedBox( + decoration: BoxDecoration( + color: _kToolbarBackgroundColor.resolveFrom(context), + border: Border.all( + color: _kToolbarBorderColor.resolveFrom(context), + ), + borderRadius: const BorderRadius.all(_kToolbarBorderRadius), + ), + child: Padding( + padding: _kToolbarPadding, + child: child, + ), + ), ), ); } @@ -86,7 +141,8 @@ class CupertinoDesktopTextSelectionToolbar extends StatelessWidget { Widget build(BuildContext context) { assert(debugCheckHasMediaQuery(context)); - final double paddingAbove = MediaQuery.paddingOf(context).top + _kToolbarScreenPadding; + final double paddingAbove = + MediaQuery.paddingOf(context).top + _kToolbarScreenPadding; final Offset localAdjustment = Offset(_kToolbarScreenPadding, paddingAbove); return Padding( diff --git a/packages/flutter/lib/src/cupertino/desktop_text_selection_toolbar_button.dart b/packages/flutter/lib/src/cupertino/desktop_text_selection_toolbar_button.dart index 60e046d7a0..20f6d3a8eb 100644 --- a/packages/flutter/lib/src/cupertino/desktop_text_selection_toolbar_button.dart +++ b/packages/flutter/lib/src/cupertino/desktop_text_selection_toolbar_button.dart @@ -10,8 +10,8 @@ import 'colors.dart'; import 'text_selection_toolbar_button.dart'; import 'theme.dart'; -// These values were measured from a screenshot of TextEdit on MacOS 10.15.7 on -// a Macbook Pro. +// These values were measured from a screenshot of the native context menu on +// macOS 13.2 on a Macbook Pro. const TextStyle _kToolbarButtonFontStyle = TextStyle( inherit: false, fontSize: 14.0, @@ -19,13 +19,13 @@ const TextStyle _kToolbarButtonFontStyle = TextStyle( fontWeight: FontWeight.w400, ); -// This value was measured from a screenshot of TextEdit on MacOS 10.15.7 on a -// Macbook Pro. +// This value was measured from a screenshot of the native context menu on +// macOS 13.2 on a Macbook Pro. const EdgeInsets _kToolbarButtonPadding = EdgeInsets.fromLTRB( - 20.0, - 0.0, - 20.0, - 3.0, + 8.0, + 2.0, + 8.0, + 5.0, ); /// A button in the style of the Mac context menu buttons. @@ -37,26 +37,17 @@ class CupertinoDesktopTextSelectionToolbarButton extends StatefulWidget { super.key, required this.onPressed, required Widget this.child, - }) : buttonItem = null; + }) : buttonItem = null, + text = null; /// Create an instance of [CupertinoDesktopTextSelectionToolbarButton] whose child is /// a [Text] widget styled like the default Mac context menu button. - CupertinoDesktopTextSelectionToolbarButton.text({ + const CupertinoDesktopTextSelectionToolbarButton.text({ super.key, - required BuildContext context, required this.onPressed, - required String text, - }) : buttonItem = null, - child = Text( - text, - overflow: TextOverflow.ellipsis, - style: _kToolbarButtonFontStyle.copyWith( - color: const CupertinoDynamicColor.withBrightness( - color: CupertinoColors.black, - darkColor: CupertinoColors.white, - ).resolveFrom(context), - ), - ); + required this.text, + }) : buttonItem = null, + child = null; /// Create an instance of [CupertinoDesktopTextSelectionToolbarButton] from /// the given [ContextMenuButtonItem]. @@ -65,8 +56,9 @@ class CupertinoDesktopTextSelectionToolbarButton extends StatefulWidget { CupertinoDesktopTextSelectionToolbarButton.buttonItem({ super.key, required ContextMenuButtonItem this.buttonItem, - }) : onPressed = buttonItem.onPressed, - child = null; + }) : onPressed = buttonItem.onPressed, + text = null, + child = null; /// {@macro flutter.cupertino.CupertinoTextSelectionToolbarButton.onPressed} final VoidCallback? onPressed; @@ -77,11 +69,16 @@ class CupertinoDesktopTextSelectionToolbarButton extends StatefulWidget { /// {@macro flutter.cupertino.CupertinoTextSelectionToolbarButton.onPressed} final ContextMenuButtonItem? buttonItem; + /// {@macro flutter.cupertino.CupertinoTextSelectionToolbarButton.text} + final String? text; + @override - State createState() => _CupertinoDesktopTextSelectionToolbarButtonState(); + State createState() => + _CupertinoDesktopTextSelectionToolbarButtonState(); } -class _CupertinoDesktopTextSelectionToolbarButtonState extends State { +class _CupertinoDesktopTextSelectionToolbarButtonState + extends State { bool _isHovered = false; void _onEnter(PointerEnterEvent event) { @@ -98,16 +95,24 @@ class _CupertinoDesktopTextSelectionToolbarButtonState extends State[ + CupertinoDesktopTextSelectionToolbarButton( + child: const Text('Tap me'), + onPressed: () {}, + ), + ], + ), + ), + ), + ); + + final BackdropFilter toolbarFilter = tester.firstWidget( + find.descendant( + of: find.byType(CupertinoDesktopTextSelectionToolbar), + matching: find.byType(BackdropFilter), + ), + ); + + expect( + toolbarFilter.filter.runtimeType, + // _ComposeImageFilter is internal so we can't test if its filters are + // for blur and saturation, but checking if it's a _ComposeImageFilter + // should be enough. Outer and inner parameters don't matter, we just need + // a new _ComposeImageFilter to get its runtimeType. + // + // As web doesn't support ImageFilter.compose, we use just blur when + // kIsWeb. + kIsWeb + ? ImageFilter.blur().runtimeType + : ImageFilter.compose( + outer: ImageFilter.blur(), + inner: ImageFilter.blur(), + ).runtimeType, + ); + }); + + testWidgets('has shadow', (WidgetTester tester) async { + await tester.pumpWidget( + CupertinoApp( + home: Center( + child: CupertinoDesktopTextSelectionToolbar( + anchor: Offset.zero, + children: [ + CupertinoDesktopTextSelectionToolbarButton( + child: const Text('Tap me'), + onPressed: () {}, + ), + ], + ), + ), + ), + ); + + final DecoratedBox decoratedBox = tester.firstWidget( + find.descendant( + of: find.byType(CupertinoDesktopTextSelectionToolbar), + matching: find.byType(DecoratedBox), + ), + ); + + expect( + (decoratedBox.decoration as BoxDecoration).boxShadow, + isNotNull, + ); + }); + + testWidgets('is translucent', (WidgetTester tester) async { + await tester.pumpWidget( + CupertinoApp( + home: Center( + child: CupertinoDesktopTextSelectionToolbar( + anchor: Offset.zero, + children: [ + CupertinoDesktopTextSelectionToolbarButton( + child: const Text('Tap me'), + onPressed: () {}, + ), + ], + ), + ), + ), + ); + + final DecoratedBox decoratedBox = tester + .widgetList( + find.descendant( + of: find.byType(CupertinoDesktopTextSelectionToolbar), + matching: find.byType(DecoratedBox), + ), + ) + // The second DecoratedBox should be the one with color. + .elementAt(1); + + expect( + (decoratedBox.decoration as BoxDecoration).color!.opacity, + lessThan(1.0), + ); + }); + testWidgets('positions itself at the anchor', (WidgetTester tester) async { // An arbitrary point on the screen to position at. const Offset anchor = Offset(30.0, 40.0); @@ -29,7 +137,8 @@ void main() { ); expect( - tester.getTopLeft(find.byType(CupertinoDesktopTextSelectionToolbarButton)), + tester + .getTopLeft(find.byType(CupertinoDesktopTextSelectionToolbarButton)), // Greater than due to padding internal to the toolbar. greaterThan(anchor), );