From fbb3036b73a8aa6691aa5bca2ecce3aa90ea7824 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 27 Jul 2022 19:16:05 +0200 Subject: [PATCH] FloatingActionButton: add themeable mouse cursor (#103473) --- .../src/material/floating_action_button.dart | 23 +++++++++++++++- .../floating_action_button_theme.dart | 20 ++++++++++++-- .../floating_action_button_theme_test.dart | 27 +++++++++++++++++++ 3 files changed, 67 insertions(+), 3 deletions(-) diff --git a/packages/flutter/lib/src/material/floating_action_button.dart b/packages/flutter/lib/src/material/floating_action_button.dart index d359a5338a2..376acda063b 100644 --- a/packages/flutter/lib/src/material/floating_action_button.dart +++ b/packages/flutter/lib/src/material/floating_action_button.dart @@ -11,6 +11,7 @@ import 'package:flutter/widgets.dart'; import 'button.dart'; import 'color_scheme.dart'; import 'floating_action_button_theme.dart'; +import 'material_state.dart'; import 'scaffold.dart'; import 'text_theme.dart'; import 'theme.dart'; @@ -603,7 +604,7 @@ class FloatingActionButton extends StatelessWidget { Widget result = RawMaterialButton( onPressed: onPressed, - mouseCursor: mouseCursor, + mouseCursor: _EffectiveMouseCursor(mouseCursor, floatingActionButtonTheme.mouseCursor), elevation: elevation, focusElevation: focusElevation, hoverElevation: hoverElevation, @@ -664,6 +665,26 @@ class FloatingActionButton extends StatelessWidget { } } +// This MaterialStateProperty is passed along to RawMaterialButton which +// resolves the property against MaterialState.pressed, MaterialState.hovered, +// MaterialState.focused, MaterialState.disabled. +class _EffectiveMouseCursor extends MaterialStateMouseCursor { + const _EffectiveMouseCursor(this.widgetCursor, this.themeCursor); + + final MouseCursor? widgetCursor; + final MaterialStateProperty? themeCursor; + + @override + MouseCursor resolve(Set states) { + return MaterialStateProperty.resolveAs(widgetCursor, states) + ?? themeCursor?.resolve(states) + ?? MaterialStateMouseCursor.clickable.resolve(states); + } + + @override + String get debugDescription => 'MaterialStateMouseCursor(FloatActionButton)'; +} + // This widget's size matches its child's size unless its constraints // force it to be larger or smaller. The child is centered. // diff --git a/packages/flutter/lib/src/material/floating_action_button_theme.dart b/packages/flutter/lib/src/material/floating_action_button_theme.dart index fdc8b2abfa4..6450f77e174 100644 --- a/packages/flutter/lib/src/material/floating_action_button_theme.dart +++ b/packages/flutter/lib/src/material/floating_action_button_theme.dart @@ -7,6 +7,8 @@ import 'dart:ui' show lerpDouble; import 'package:flutter/foundation.dart'; import 'package:flutter/rendering.dart'; +import 'material_state.dart'; + /// Defines default property values for descendant [FloatingActionButton] /// widgets. /// @@ -51,6 +53,7 @@ class FloatingActionButtonThemeData with Diagnosticable { this.extendedIconLabelSpacing, this.extendedPadding, this.extendedTextStyle, + this.mouseCursor, }); /// Color to be used for the unselected, enabled [FloatingActionButton]'s @@ -129,6 +132,11 @@ class FloatingActionButtonThemeData with Diagnosticable { /// The text style for an extended [FloatingActionButton]'s label. final TextStyle? extendedTextStyle; + /// {@macro flutter.material.RawMaterialButton.mouseCursor} + /// + /// If specified, overrides the default value of [FloatingActionButton.mouseCursor]. + final MaterialStateProperty? mouseCursor; + /// Creates a copy of this object with the given fields replaced with the /// new values. FloatingActionButtonThemeData copyWith({ @@ -152,6 +160,7 @@ class FloatingActionButtonThemeData with Diagnosticable { double? extendedIconLabelSpacing, EdgeInsetsGeometry? extendedPadding, TextStyle? extendedTextStyle, + MaterialStateProperty? mouseCursor, }) { return FloatingActionButtonThemeData( foregroundColor: foregroundColor ?? this.foregroundColor, @@ -174,6 +183,7 @@ class FloatingActionButtonThemeData with Diagnosticable { extendedIconLabelSpacing: extendedIconLabelSpacing ?? this.extendedIconLabelSpacing, extendedPadding: extendedPadding ?? this.extendedPadding, extendedTextStyle: extendedTextStyle ?? this.extendedTextStyle, + mouseCursor: mouseCursor ?? this.mouseCursor, ); } @@ -208,6 +218,7 @@ class FloatingActionButtonThemeData with Diagnosticable { extendedIconLabelSpacing: lerpDouble(a?.extendedIconLabelSpacing, b?.extendedIconLabelSpacing, t), extendedPadding: EdgeInsetsGeometry.lerp(a?.extendedPadding, b?.extendedPadding, t), extendedTextStyle: TextStyle.lerp(a?.extendedTextStyle, b?.extendedTextStyle, t), + mouseCursor: t < 0.5 ? a?.mouseCursor : b?.mouseCursor, ); } @@ -232,7 +243,10 @@ class FloatingActionButtonThemeData with Diagnosticable { extendedSizeConstraints, extendedIconLabelSpacing, extendedPadding, - extendedTextStyle, + Object.hash( + extendedTextStyle, + mouseCursor, + ), ); @override @@ -263,7 +277,8 @@ class FloatingActionButtonThemeData with Diagnosticable { && other.extendedSizeConstraints == extendedSizeConstraints && other.extendedIconLabelSpacing == extendedIconLabelSpacing && other.extendedPadding == extendedPadding - && other.extendedTextStyle == extendedTextStyle; + && other.extendedTextStyle == extendedTextStyle + && other.mouseCursor == mouseCursor; } @override @@ -290,5 +305,6 @@ class FloatingActionButtonThemeData with Diagnosticable { properties.add(DoubleProperty('extendedIconLabelSpacing', extendedIconLabelSpacing, defaultValue: null)); properties.add(DiagnosticsProperty('extendedPadding', extendedPadding, defaultValue: null)); properties.add(DiagnosticsProperty('extendedTextStyle', extendedTextStyle, defaultValue: null)); + properties.add(DiagnosticsProperty>('mouseCursor', mouseCursor, defaultValue: null)); } } diff --git a/packages/flutter/test/material/floating_action_button_theme_test.dart b/packages/flutter/test/material/floating_action_button_theme_test.dart index 5cd5d27c375..1eca03272b3 100644 --- a/packages/flutter/test/material/floating_action_button_theme_test.dart +++ b/packages/flutter/test/material/floating_action_button_theme_test.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -287,6 +288,7 @@ void main() { extendedIconLabelSpacing: 12, extendedPadding: EdgeInsetsDirectional.only(start: 7.0, end: 8.0), extendedTextStyle: TextStyle(letterSpacing: 2.0), + mouseCursor: MaterialStateMouseCursor.clickable, ).debugFillProperties(builder); final List description = builder.properties @@ -315,8 +317,33 @@ void main() { 'extendedIconLabelSpacing: 12.0', 'extendedPadding: EdgeInsetsDirectional(7.0, 0.0, 8.0, 0.0)', 'extendedTextStyle: TextStyle(inherit: true, letterSpacing: 2.0)', + 'mouseCursor: MaterialStateMouseCursor(clickable)', ]); }); + + testWidgets('FloatingActionButton.mouseCursor uses FloatingActionButtonThemeData.mouseCursor when specified.', (WidgetTester tester) async { + await tester.pumpWidget(MaterialApp( + theme: ThemeData().copyWith( + floatingActionButtonTheme: FloatingActionButtonThemeData( + mouseCursor: MaterialStateProperty.all(SystemMouseCursors.text), + ), + ), + home: Scaffold( + floatingActionButton: FloatingActionButton( + onPressed: () { }, + child: const Icon(Icons.add), + ), + ), + )); + + await tester.pumpAndSettle(); + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(); + addTearDown(gesture.removePointer); + await gesture.moveTo(tester.getCenter(find.byType(FloatingActionButton))); + await tester.pumpAndSettle(); + expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text); + }); } RawMaterialButton _getRawMaterialButton(WidgetTester tester) {