diff --git a/packages/flutter/lib/src/material/switch.dart b/packages/flutter/lib/src/material/switch.dart index 38a5eddc5f..e823a8d870 100644 --- a/packages/flutter/lib/src/material/switch.dart +++ b/packages/flutter/lib/src/material/switch.dart @@ -197,7 +197,9 @@ class Switch extends StatelessWidget { /// ``` final ValueChanged? onChanged; + /// {@template flutter.material.switch.activeColor} /// The color to use when this switch is on. + /// {@endtemplate} /// /// Defaults to [ColorScheme.secondary]. /// @@ -205,7 +207,9 @@ class Switch extends StatelessWidget { /// state, it will be used instead of this color. final Color? activeColor; + /// {@template flutter.material.switch.activeTrackColor} /// The color to use on the track when this switch is on. + /// {@endtemplate} /// /// Defaults to [ColorScheme.secondary] with the opacity set at 50%. /// @@ -215,7 +219,9 @@ class Switch extends StatelessWidget { /// state, it will be used instead of this color. final Color? activeTrackColor; + /// {@template flutter.material.switch.inactiveThumbColor} /// The color to use on the thumb when this switch is off. + /// {@endtemplate} /// /// Defaults to the colors described in the Material design specification. /// @@ -225,7 +231,9 @@ class Switch extends StatelessWidget { /// used instead of this color. final Color? inactiveThumbColor; + /// {@template flutter.material.switch.inactiveTrackColor} /// The color to use on the track when this switch is off. + /// {@endtemplate} /// /// Defaults to the colors described in the Material design specification. /// @@ -235,22 +243,30 @@ class Switch extends StatelessWidget { /// used instead of this color. final Color? inactiveTrackColor; + /// {@template flutter.material.switch.activeThumbImage} /// An image to use on the thumb of this switch when the switch is on. + /// {@endtemplate} /// /// Ignored if this switch is created with [Switch.adaptive]. final ImageProvider? activeThumbImage; + /// {@template flutter.material.switch.onActiveThumbImageError} /// An optional error callback for errors emitted when loading /// [activeThumbImage]. + /// {@endtemplate} final ImageErrorListener? onActiveThumbImageError; + /// {@template flutter.material.switch.inactiveThumbImage} /// An image to use on the thumb of this switch when the switch is off. + /// {@endtemplate} /// /// Ignored if this switch is created with [Switch.adaptive]. final ImageProvider? inactiveThumbImage; + /// {@template flutter.material.switch.onInactiveThumbImageError} /// An optional error callback for errors emitted when loading /// [inactiveThumbImage]. + /// {@endtemplate} final ImageErrorListener? onInactiveThumbImageError; /// {@template flutter.material.switch.thumbColor} diff --git a/packages/flutter/lib/src/material/switch_list_tile.dart b/packages/flutter/lib/src/material/switch_list_tile.dart index 70a7ebb3fa..328de9c3ab 100644 --- a/packages/flutter/lib/src/material/switch_list_tile.dart +++ b/packages/flutter/lib/src/material/switch_list_tile.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/widgets.dart'; import 'list_tile.dart'; @@ -163,13 +164,27 @@ class SwitchListTile extends StatelessWidget { super.key, required this.value, required this.onChanged, - this.tileColor, this.activeColor, this.activeTrackColor, this.inactiveThumbColor, this.inactiveTrackColor, this.activeThumbImage, + this.onActiveThumbImageError, this.inactiveThumbImage, + this.onInactiveThumbImageError, + this.thumbColor, + this.trackColor, + this.trackOutlineColor, + this.thumbIcon, + this.materialTapTargetSize, + this.dragStartBehavior = DragStartBehavior.start, + this.mouseCursor, + this.overlayColor, + this.splashRadius, + this.focusNode, + this.onFocusChange, + this.autofocus = false, + this.tileColor, this.title, this.subtitle, this.isThreeLine = false, @@ -177,17 +192,16 @@ class SwitchListTile extends StatelessWidget { this.contentPadding, this.secondary, this.selected = false, - this.autofocus = false, this.controlAffinity = ListTileControlAffinity.platform, this.shape, this.selectedTileColor, this.visualDensity, - this.focusNode, - this.onFocusChange, this.enableFeedback, this.hoverColor, - this.trackOutlineColor, }) : _switchListTileType = _SwitchListTileType.material, + applyCupertinoTheme = false, + assert(activeThumbImage != null || onActiveThumbImageError == null), + assert(inactiveThumbImage != null || onInactiveThumbImageError == null), assert(!isThreeLine || subtitle != null); /// Creates a Material [ListTile] with an adaptive [Switch], following @@ -206,13 +220,28 @@ class SwitchListTile extends StatelessWidget { super.key, required this.value, required this.onChanged, - this.tileColor, this.activeColor, this.activeTrackColor, this.inactiveThumbColor, this.inactiveTrackColor, this.activeThumbImage, + this.onActiveThumbImageError, this.inactiveThumbImage, + this.onInactiveThumbImageError, + this.thumbColor, + this.trackColor, + this.trackOutlineColor, + this.thumbIcon, + this.materialTapTargetSize, + this.dragStartBehavior = DragStartBehavior.start, + this.mouseCursor, + this.overlayColor, + this.splashRadius, + this.focusNode, + this.onFocusChange, + this.autofocus = false, + this.applyCupertinoTheme, + this.tileColor, this.title, this.subtitle, this.isThreeLine = false, @@ -220,18 +249,16 @@ class SwitchListTile extends StatelessWidget { this.contentPadding, this.secondary, this.selected = false, - this.autofocus = false, this.controlAffinity = ListTileControlAffinity.platform, this.shape, this.selectedTileColor, this.visualDensity, - this.focusNode, - this.onFocusChange, this.enableFeedback, this.hoverColor, - this.trackOutlineColor, }) : _switchListTileType = _SwitchListTileType.adaptive, - assert(!isThreeLine || subtitle != null); + assert(!isThreeLine || subtitle != null), + assert(activeThumbImage != null || onActiveThumbImageError == null), + assert(inactiveThumbImage != null || onInactiveThumbImageError == null); /// Whether this switch is checked. /// @@ -265,43 +292,146 @@ class SwitchListTile extends StatelessWidget { /// {@end-tool} final ValueChanged? onChanged; - /// The color to use when this switch is on. + /// {@macro flutter.material.switch.activeColor} /// /// Defaults to [ColorScheme.secondary] of the current [Theme]. final Color? activeColor; - /// The color to use on the track when this switch is on. + /// {@macro flutter.material.switch.activeTrackColor} /// /// Defaults to [ThemeData.toggleableActiveColor] with the opacity set at 50%. /// /// Ignored if created with [SwitchListTile.adaptive]. final Color? activeTrackColor; - /// The color to use on the thumb when this switch is off. + /// {@macro flutter.material.switch.inactiveThumbColor} /// /// Defaults to the colors described in the Material design specification. /// /// Ignored if created with [SwitchListTile.adaptive]. final Color? inactiveThumbColor; - /// The color to use on the track when this switch is off. + /// {@macro flutter.material.switch.inactiveTrackColor} /// /// Defaults to the colors described in the Material design specification. /// /// Ignored if created with [SwitchListTile.adaptive]. final Color? inactiveTrackColor; - /// {@macro flutter.material.ListTile.tileColor} - final Color? tileColor; - - /// An image to use on the thumb of this switch when the switch is on. + /// {@macro flutter.material.switch.activeThumbImage} final ImageProvider? activeThumbImage; - /// An image to use on the thumb of this switch when the switch is off. + /// {@macro flutter.material.switch.onActiveThumbImageError} + final ImageErrorListener? onActiveThumbImageError; + + /// {@macro flutter.material.switch.inactiveThumbImage} /// /// Ignored if created with [SwitchListTile.adaptive]. final ImageProvider? inactiveThumbImage; + /// {@macro flutter.material.switch.onInactiveThumbImageError} + final ImageErrorListener? onInactiveThumbImageError; + + /// The color of this switch's thumb. + /// + /// Resolved in the following states: + /// * [MaterialState.selected]. + /// * [MaterialState.hovered]. + /// * [MaterialState.disabled]. + /// + /// If null, then the value of [activeColor] is used in the selected state + /// and [inactiveThumbColor] in the default state. If that is also null, then + /// the value of [SwitchThemeData.thumbColor] is used. If that is also null, + /// The default value is used. + final MaterialStateProperty? thumbColor; + + /// The color of this switch's track. + /// + /// Resolved in the following states: + /// * [MaterialState.selected]. + /// * [MaterialState.hovered]. + /// * [MaterialState.disabled]. + /// + /// If null, then the value of [activeTrackColor] is used in the selected + /// state and [inactiveTrackColor] in the default state. If that is also null, + /// then the value of [SwitchThemeData.trackColor] is used. If that is also + /// null, then the default value is used. + final MaterialStateProperty? trackColor; + + /// {@macro flutter.material.switch.trackOutlineColor} + /// + /// The [ListTile] will be focused when this [SwitchListTile] requests focus, + /// so the focused outline color of the switch will be ignored. + /// + /// In Material 3, the outline color defaults to transparent in the selected + /// state and [ColorScheme.outline] in the unselected state. In Material 2, + /// the [Switch] track has no outline. + final MaterialStateProperty? trackOutlineColor; + + /// The icon to use on the thumb of this switch + /// + /// Resolved in the following states: + /// * [MaterialState.selected]. + /// * [MaterialState.hovered]. + /// * [MaterialState.disabled]. + /// + /// If null, then the value of [SwitchThemeData.thumbIcon] is used. If this is + /// also null, then the [Switch] does not have any icons on the thumb. + final MaterialStateProperty? thumbIcon; + + /// {@macro flutter.material.switch.materialTapTargetSize} + /// + /// defaults to [MaterialTapTargetSize.shrinkWrap]. + final MaterialTapTargetSize? materialTapTargetSize; + + /// {@macro flutter.cupertino.CupertinoSwitch.dragStartBehavior} + final DragStartBehavior dragStartBehavior; + + /// The cursor for a mouse pointer when it enters or is hovering over the + /// widget. + /// + /// If [mouseCursor] is a [MaterialStateProperty], + /// [MaterialStateProperty.resolve] is used for the following [MaterialState]s: + /// + /// * [MaterialState.selected]. + /// * [MaterialState.hovered]. + /// * [MaterialState.disabled]. + /// + /// If null, then the value of [SwitchThemeData.mouseCursor] is used. If that + /// is also null, then [MaterialStateMouseCursor.clickable] is used. + final MouseCursor? mouseCursor; + + /// The color for the switch's [Material]. + /// + /// Resolves in the following states: + /// * [MaterialState.pressed]. + /// * [MaterialState.selected]. + /// * [MaterialState.hovered]. + /// + /// If null, then the value of [activeColor] with alpha [kRadialReactionAlpha] + /// and [hoverColor] is used in the pressed and hovered state. If that is also + /// null, the value of [SwitchThemeData.overlayColor] is used. If that is + /// also null, then the default value is used in the pressed and hovered state. + final MaterialStateProperty? overlayColor; + + /// {@macro flutter.material.switch.splashRadius} + /// + /// If null, then the value of [SwitchThemeData.splashRadius] is used. If that + /// is also null, then [kRadialReactionRadius] is used. + final double? splashRadius; + + /// {@macro flutter.widgets.Focus.focusNode} + final FocusNode? focusNode; + + /// {@macro flutter.material.inkwell.onFocusChange} + final ValueChanged? onFocusChange; + + /// {@macro flutter.widgets.Focus.autofocus} + final bool autofocus; + + /// {@macro flutter.material.ListTile.tileColor} + final Color? tileColor; + /// The primary content of the list tile. /// /// Typically a [Text] widget. @@ -346,9 +476,6 @@ class SwitchListTile extends StatelessWidget { /// Normally, this property is left to its default value, false. final bool selected; - /// {@macro flutter.widgets.Focus.autofocus} - final bool autofocus; - /// If adaptive, creates the switch with [Switch.adaptive]. final _SwitchListTileType _switchListTileType; @@ -368,12 +495,6 @@ class SwitchListTile extends StatelessWidget { /// {@macro flutter.material.themedata.visualDensity} final VisualDensity? visualDensity; - /// {@macro flutter.widgets.Focus.focusNode} - final FocusNode? focusNode; - - /// {@macro flutter.material.inkwell.onFocusChange} - final ValueChanged? onFocusChange; - /// {@macro flutter.material.ListTile.enableFeedback} /// /// See also: @@ -384,15 +505,8 @@ class SwitchListTile extends StatelessWidget { /// The color for the tile's [Material] when a pointer is hovering over it. final Color? hoverColor; - /// {@macro flutter.material.switch.trackOutlineColor} - /// - /// The [ListTile] will be focused when this [SwitchListTile] requests focus, - /// so the focused outline color of the switch will be ignored. - /// - /// In Material 3, the outline color defaults to transparent in the selected - /// state and [ColorScheme.outline] in the unselected state. In Material 2, - /// the [Switch] track has no outline. - final MaterialStateProperty? trackOutlineColor; + /// {@macro flutter.cupertino.CupertinoSwitch.applyTheme} + final bool? applyCupertinoTheme; @override Widget build(BuildContext context) { @@ -405,13 +519,23 @@ class SwitchListTile extends StatelessWidget { activeColor: activeColor, activeThumbImage: activeThumbImage, inactiveThumbImage: inactiveThumbImage, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap, activeTrackColor: activeTrackColor, inactiveTrackColor: inactiveTrackColor, inactiveThumbColor: inactiveThumbColor, autofocus: autofocus, onFocusChange: onFocusChange, + onActiveThumbImageError: onActiveThumbImageError, + onInactiveThumbImageError: onInactiveThumbImageError, + thumbColor: thumbColor, + trackColor: trackColor, trackOutlineColor: trackOutlineColor, + thumbIcon: thumbIcon, + applyCupertinoTheme: applyCupertinoTheme, + dragStartBehavior: dragStartBehavior, + mouseCursor: mouseCursor, + splashRadius: splashRadius, + overlayColor: overlayColor, ); break; @@ -422,13 +546,22 @@ class SwitchListTile extends StatelessWidget { activeColor: activeColor, activeThumbImage: activeThumbImage, inactiveThumbImage: inactiveThumbImage, - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap, activeTrackColor: activeTrackColor, inactiveTrackColor: inactiveTrackColor, inactiveThumbColor: inactiveThumbColor, autofocus: autofocus, onFocusChange: onFocusChange, + onActiveThumbImageError: onActiveThumbImageError, + onInactiveThumbImageError: onInactiveThumbImageError, + thumbColor: thumbColor, + trackColor: trackColor, trackOutlineColor: trackOutlineColor, + thumbIcon: thumbIcon, + dragStartBehavior: dragStartBehavior, + mouseCursor: mouseCursor, + splashRadius: splashRadius, + overlayColor: overlayColor, ); } diff --git a/packages/flutter/test/material/switch_list_tile_test.dart b/packages/flutter/test/material/switch_list_tile_test.dart index 1e0c343f5c..ca4adb4fff 100644 --- a/packages/flutter/test/material/switch_list_tile_test.dart +++ b/packages/flutter/test/material/switch_list_tile_test.dart @@ -603,19 +603,660 @@ void main() { ); // Start hovering - final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + final TestGesture gesture = await tester.createGesture( + kind: PointerDeviceKind.mouse); await gesture.moveTo(tester.getCenter(find.byKey(key))); await tester.pump(); await tester.pumpAndSettle(); expect( Material.of(tester.element(find.byKey(key))), + paints..rect()..rect( + color: Colors.orange[500], + rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0), + ) + ); + }); + + testWidgets('SwitchListTile respects thumbColor in active/enabled states', (WidgetTester tester) async { + const Color activeEnabledThumbColor = Color(0xFF000001); + const Color activeDisabledThumbColor = Color(0xFF000002); + const Color inactiveEnabledThumbColor = Color(0xFF000003); + const Color inactiveDisabledThumbColor = Color(0xFF000004); + + Color getThumbColor(Set states) { + if (states.contains(MaterialState.disabled)) { + if (states.contains(MaterialState.selected)) { + return activeDisabledThumbColor; + } + return inactiveDisabledThumbColor; + } + if (states.contains(MaterialState.selected)) { + return activeEnabledThumbColor; + } + return inactiveEnabledThumbColor; + } + + final MaterialStateProperty thumbColor = MaterialStateColor.resolveWith(getThumbColor); + + Widget buildSwitchListTile({required bool enabled, required bool selected}) { + return wrap( + child: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return SwitchListTile( + value: selected, + thumbColor: thumbColor, + onChanged: enabled ? (_) { } : null, + ); + }), + ); + } + + await tester.pumpWidget(buildSwitchListTile(enabled: false, selected: false)); + await tester.pumpAndSettle(); + expect( + Material.of(tester.element(find.byType(Switch))), + paints + ..rrect()..rrect()..rrect()..rrect() + ..rrect(color: inactiveDisabledThumbColor), + ); + + await tester.pumpWidget(buildSwitchListTile(enabled: false, selected: true)); + await tester.pumpAndSettle(); + expect( + Material.of(tester.element(find.byType(Switch))), paints - ..rect() - ..rect( - color: Colors.orange[500], - rect: const Rect.fromLTRB(350.0, 250.0, 450.0, 350.0), - ) + ..rrect()..rrect()..rrect()..rrect() + ..rrect(color: activeDisabledThumbColor), + ); + + await tester.pumpWidget(buildSwitchListTile(enabled: true, selected: false)); + await tester.pumpAndSettle(); + + expect( + Material.of(tester.element(find.byType(Switch))), + paints + ..rrect()..rrect()..rrect()..rrect() + ..rrect(color: inactiveEnabledThumbColor), + ); + + await tester.pumpWidget(buildSwitchListTile(enabled: true, selected: true)); + await tester.pumpAndSettle(); + + expect( + Material.of(tester.element(find.byType(Switch))), + paints + ..rrect()..rrect()..rrect()..rrect() + ..rrect(color: activeEnabledThumbColor), + ); + }); + + testWidgets('SwitchListTile respects thumbColor in hovered/pressed states', (WidgetTester tester) async { + tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; + const Color hoveredThumbColor = Color(0xFF4caf50); + const Color pressedThumbColor = Color(0xFFF44336); + + Color getThumbColor(Set states) { + if (states.contains(MaterialState.pressed)) { + return pressedThumbColor; + } + if (states.contains(MaterialState.hovered)) { + return hoveredThumbColor; + } + return Colors.transparent; + } + + final MaterialStateProperty thumbColor = MaterialStateColor.resolveWith(getThumbColor); + + Widget buildSwitchListTile() { + return MaterialApp( + theme: ThemeData(), + home: wrap( + child: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return SwitchListTile( + value: false, + thumbColor: thumbColor, + onChanged: (_) { }, + ); + }), + ), + ); + } + + await tester.pumpWidget(buildSwitchListTile()); + await tester.pumpAndSettle(); + + // Start hovering + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.moveTo(tester.getCenter(find.byType(Switch))); + + await tester.pumpAndSettle(); + + expect( + Material.of(tester.element(find.byType(Switch))), + paints + ..rrect()..rrect()..rrect()..rrect() + ..rrect(color: hoveredThumbColor), + ); + + // On pressed state + await tester.press(find.byType(Switch)); + await tester.pumpAndSettle(); + expect( + Material.of(tester.element(find.byType(Switch))), + paints + ..rrect()..rrect()..rrect()..rrect() + ..rrect(color: pressedThumbColor), + ); + }); + + testWidgets('SwitchListTile respects trackColor in active/enabled states', (WidgetTester tester) async { + const Color activeEnabledTrackColor = Color(0xFF000001); + const Color activeDisabledTrackColor = Color(0xFF000002); + const Color inactiveEnabledTrackColor = Color(0xFF000003); + const Color inactiveDisabledTrackColor = Color(0xFF000004); + + Color getTrackColor(Set states) { + if (states.contains(MaterialState.disabled)) { + if (states.contains(MaterialState.selected)) { + return activeDisabledTrackColor; + } + return inactiveDisabledTrackColor; + } + if (states.contains(MaterialState.selected)) { + return activeEnabledTrackColor; + } + return inactiveEnabledTrackColor; + } + + final MaterialStateProperty trackColor = MaterialStateColor.resolveWith(getTrackColor); + + Widget buildSwitchListTile({required bool enabled, required bool selected}) { + return wrap( + child: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return SwitchListTile( + value: selected, + trackColor: trackColor, + onChanged: enabled ? (_) { } : null, + ); + }), + ); + } + + await tester.pumpWidget(buildSwitchListTile(enabled: false, selected: false)); + + expect( + Material.of(tester.element(find.byType(Switch))), + paints..rrect(color: inactiveDisabledTrackColor), + ); + + await tester.pumpWidget(buildSwitchListTile(enabled: false, selected: true)); + await tester.pumpAndSettle(); + expect( + Material.of(tester.element(find.byType(Switch))), + paints..rrect(color: activeDisabledTrackColor), + ); + + await tester.pumpWidget(buildSwitchListTile(enabled: true, selected: false)); + await tester.pumpAndSettle(); + + expect( + Material.of(tester.element(find.byType(Switch))), + paints..rrect(color: inactiveEnabledTrackColor), + ); + + await tester.pumpWidget(buildSwitchListTile(enabled: true, selected: true)); + await tester.pumpAndSettle(); + + expect( + Material.of(tester.element(find.byType(Switch))), + paints..rrect(color: activeEnabledTrackColor), + ); + }); + + testWidgets('SwitchListTile respects trackColor in hovered states', (WidgetTester tester) async { + tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; + const Color hoveredTrackColor = Color(0xFF4caf50); + + Color getTrackColor(Set states) { + if (states.contains(MaterialState.hovered)) { + return hoveredTrackColor; + } + return Colors.transparent; + } + + final MaterialStateProperty trackColor = MaterialStateColor.resolveWith(getTrackColor); + + Widget buildSwitchListTile() { + return wrap( + child: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return SwitchListTile( + value: false, + trackColor: trackColor, + onChanged: (_) { }, + ); + }), + ); + } + + await tester.pumpWidget(buildSwitchListTile()); + await tester.pumpAndSettle(); + + // Start hovering + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.moveTo(tester.getCenter(find.byType(Switch))); + + await tester.pumpAndSettle(); + + expect( + Material.of(tester.element(find.byType(Switch))), + paints..rrect(color: hoveredTrackColor), + ); + }); + + testWidgets('SwitchListTile respects thumbIcon - M3', (WidgetTester tester) async { + const Icon activeIcon = Icon(Icons.check); + const Icon inactiveIcon = Icon(Icons.close); + + MaterialStateProperty thumbIcon(Icon? activeIcon, Icon? inactiveIcon) { + return MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.selected)) { + return activeIcon; + } + return inactiveIcon; + }); + } + + Widget buildSwitchListTile({required bool enabled, required bool active, Icon? activeIcon, Icon? inactiveIcon}) { + return MaterialApp( + theme: ThemeData(useMaterial3: true), + home: wrap( + child: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return SwitchListTile( + thumbIcon: thumbIcon(activeIcon, inactiveIcon), + value: active, + onChanged: enabled ? (_) {} : null, + ); + }), + ), + ); + } + + // active icon shows when switch is on. + await tester.pumpWidget(buildSwitchListTile(enabled: true, active: true, activeIcon: activeIcon)); + await tester.pumpAndSettle(); + final Switch switchWidget0 = tester.widget(find.byType(Switch)); + expect(switchWidget0.thumbIcon?.resolve({MaterialState.selected}), activeIcon); + expect( + Material.of(tester.element(find.byType(Switch))), + paints + ..rrect()..rrect() + ..paragraph(offset: const Offset(32.0, 12.0)), + ); + + // inactive icon shows when switch is off. + await tester.pumpWidget(buildSwitchListTile(enabled: true, active: false, inactiveIcon: inactiveIcon)); + await tester.pumpAndSettle(); + final Switch switchWidget1 = tester.widget(find.byType(Switch)); + expect(switchWidget1.thumbIcon?.resolve({}), inactiveIcon); + expect( + Material.of(tester.element(find.byType(Switch))), + paints + ..rrect()..rrect() + ..rrect() + ..paragraph(offset: const Offset(12.0, 12.0)), + ); + + // active icon doesn't show when switch is off. + await tester.pumpWidget(buildSwitchListTile(enabled: true, active: false, activeIcon: activeIcon)); + await tester.pumpAndSettle(); + final Switch switchWidget2 = tester.widget(find.byType(Switch)); + expect(switchWidget2.thumbIcon?.resolve({MaterialState.selected}), activeIcon); + expect( + Material.of(tester.element(find.byType(Switch))), + paints + ..rrect()..rrect()..rrect() + ); + + // inactive icon doesn't show when switch is on. + await tester.pumpWidget(buildSwitchListTile(enabled: true, active: true, inactiveIcon: inactiveIcon)); + await tester.pumpAndSettle(); + final Switch switchWidget3 = tester.widget(find.byType(Switch)); + expect(switchWidget3.thumbIcon?.resolve({}), inactiveIcon); + expect( + Material.of(tester.element(find.byType(Switch))), + paints + ..rrect()..rrect()..restore(), + ); + + // without icon + await tester.pumpWidget(buildSwitchListTile(enabled: true, active: false)); + expect( + Material.of(tester.element(find.byType(Switch))), + paints + ..rrect()..rrect()..rrect()..restore(), + ); + }); + + testWidgets('SwitchListTile respects materialTapTargetSize', (WidgetTester tester) async { + Widget buildSwitchListTile(MaterialTapTargetSize materialTapTargetSize) { + return wrap( + child: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return SwitchListTile( + materialTapTargetSize: materialTapTargetSize, + value: false, + onChanged: (_) {}, + ); + }), + ); + } + + await tester.pumpWidget(buildSwitchListTile(MaterialTapTargetSize.padded)); + final Switch switchWidget = tester.widget(find.byType(Switch)); + expect(switchWidget.materialTapTargetSize, MaterialTapTargetSize.padded); + expect(tester.getSize(find.byType(Switch)), const Size(59.0, 48.0)); + + await tester.pumpWidget(buildSwitchListTile(MaterialTapTargetSize.shrinkWrap)); + final Switch switchWidget1 = tester.widget(find.byType(Switch)); + expect(switchWidget1.materialTapTargetSize, MaterialTapTargetSize.shrinkWrap); + expect(tester.getSize(find.byType(Switch)), const Size(59.0, 40.0)); + }); + + testWidgets('SwitchListTile.adaptive respects applyCupertinoTheme', (WidgetTester tester) async { + final ThemeData theme = ThemeData(); + Widget buildSwitchListTile(bool applyCupertinoTheme, TargetPlatform platform) { + return MaterialApp( + theme: theme.copyWith(platform: platform), + home: wrap( + child: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return SwitchListTile.adaptive( + applyCupertinoTheme: applyCupertinoTheme, + value: true, + onChanged: (_) {}, + ); + }), + ), + ); + } + + for (final TargetPlatform platform in [ TargetPlatform.iOS, TargetPlatform.macOS ]) { + await tester.pumpWidget(buildSwitchListTile(true, platform)); + await tester.pumpAndSettle(); + expect(find.byType(CupertinoSwitch), findsOneWidget); + expect( + Material.of(tester.element(find.byType(Switch))), + paints..rrect(color: theme.useMaterial3 ? const Color(0xFF6750A4) : const Color(0xFF2196F3)), + ); + + await tester.pumpWidget(buildSwitchListTile(false, platform)); + await tester.pumpAndSettle(); + expect(find.byType(CupertinoSwitch), findsOneWidget); + expect( + Material.of(tester.element(find.byType(Switch))), + paints..rrect(color: const Color(0xFF34C759)), + ); + } + }); + + testWidgets('SwitchListTile respects materialTapTargetSize', (WidgetTester tester) async { + Widget buildSwitchListTile(MaterialTapTargetSize materialTapTargetSize) { + return wrap( + child: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return SwitchListTile( + materialTapTargetSize: materialTapTargetSize, + value: false, + onChanged: (_) {}, + ); + }), + ); + } + + await tester.pumpWidget(buildSwitchListTile(MaterialTapTargetSize.padded)); + final Switch switchWidget = tester.widget(find.byType(Switch)); + expect(switchWidget.materialTapTargetSize, MaterialTapTargetSize.padded); + expect(tester.getSize(find.byType(Switch)), const Size(59.0, 48.0)); + + await tester.pumpWidget(buildSwitchListTile(MaterialTapTargetSize.shrinkWrap)); + final Switch switchWidget1 = tester.widget(find.byType(Switch)); + expect(switchWidget1.materialTapTargetSize, MaterialTapTargetSize.shrinkWrap); + expect(tester.getSize(find.byType(Switch)), const Size(59.0, 40.0)); + }); + + testWidgets('SwitchListTile passes the value of dragStartBehavior to Switch', (WidgetTester tester) async { + Widget buildSwitchListTile(DragStartBehavior dragStartBehavior) { + return wrap( + child: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return SwitchListTile( + dragStartBehavior: dragStartBehavior, + value: false, + onChanged: (_) {}, + ); + }), + ); + } + + await tester.pumpWidget(buildSwitchListTile(DragStartBehavior.start)); + final Switch switchWidget = tester.widget(find.byType(Switch)); + expect(switchWidget.dragStartBehavior, DragStartBehavior.start); + + await tester.pumpWidget(buildSwitchListTile(DragStartBehavior.down)); + final Switch switchWidget1 = tester.widget(find.byType(Switch)); + expect(switchWidget1.dragStartBehavior, DragStartBehavior.down); + }); + + testWidgets('Switch on SwitchListTile changes mouse cursor when hovered', (WidgetTester tester) async { + // Test SwitchListTile.adaptive() constructor + await tester.pumpWidget(wrap( + child: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return SwitchListTile.adaptive( + mouseCursor: SystemMouseCursors.text, + value: false, + onChanged: (_) {}, + ); + }), + )); + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 1); + await gesture.addPointer(location: tester.getCenter(find.byType(Switch))); + + await tester.pump(); + + expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text); + + // Test SwitchListTile() constructor + await tester.pumpWidget(wrap( + child: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return SwitchListTile( + mouseCursor: SystemMouseCursors.forbidden, + value: false, + onChanged: (_) {}, + ); + }), + )); + + await gesture.moveTo(tester.getCenter(find.byType(Switch))); + expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.forbidden); + + // Test default cursor + await tester.pumpWidget(wrap( + child: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return SwitchListTile( + value: false, + onChanged: (_) {}, + ); + }), + )); + + expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.click); + + // Test default cursor when disabled + await tester.pumpWidget(wrap( + child: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return const SwitchListTile( + value: false, + onChanged: null, + ); + }), + )); + + expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic); + }); + + testWidgets('Switch with splash radius set', (WidgetTester tester) async { + tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; + const double splashRadius = 35; + await tester.pumpWidget(wrap( + child: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return SwitchListTile( + splashRadius: splashRadius, + value: false, + onChanged: (_) {}, + ); + }), + )); + + await tester.pumpAndSettle(); + + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.moveTo(tester.getCenter(find.byType(Switch))); + + await tester.pumpAndSettle(); + expect( + Material.of(tester.element(find.byType(Switch))), + paints..circle(radius: splashRadius), + ); + }); + + testWidgets('The overlay color for the thumb of the switch resolves in active/pressed/hovered states', (WidgetTester tester) async { + tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional; + const Color activeThumbColor = Color(0xFF000000); + const Color inactiveThumbColor = Color(0xFF000010); + const Color activePressedOverlayColor = Color(0xFF000001); + const Color inactivePressedOverlayColor = Color(0xFF000002); + const Color hoverOverlayColor = Color(0xFF000003); + const Color hoverColor = Color(0xFF000005); + + Color? getOverlayColor(Set states) { + if (states.contains(MaterialState.pressed)) { + if (states.contains(MaterialState.selected)) { + return activePressedOverlayColor; + } + return inactivePressedOverlayColor; + } + if (states.contains(MaterialState.hovered)) { + return hoverOverlayColor; + } + return null; + } + + Widget buildSwitch({bool active = false, bool focused = false, bool useOverlay = true}) { + return MaterialApp( + home: Scaffold( + body: SwitchListTile( + value: active, + onChanged: (_) { }, + thumbColor: MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.selected)) { + return activeThumbColor; + } + return inactiveThumbColor; + }), + overlayColor: useOverlay ? MaterialStateProperty.resolveWith(getOverlayColor) : null, + hoverColor: hoverColor, + ), + ), + ); + } + + // test inactive Switch, and overlayColor is set to null. + await tester.pumpWidget(buildSwitch(useOverlay: false)); + await tester.press(find.byType(Switch)); + await tester.pumpAndSettle(); + + expect( + Material.of(tester.element(find.byType(Switch))), + paints + ..rrect() + ..circle( + color: inactiveThumbColor.withAlpha(kRadialReactionAlpha), + ), + reason: 'Default inactive pressed Switch should have overlay color from thumbColor', + ); + + // test active Switch, and overlayColor is set to null. + await tester.pumpWidget(buildSwitch(active: true, useOverlay: false)); + await tester.press(find.byType(Switch)); + await tester.pumpAndSettle(); + + expect( + Material.of(tester.element(find.byType(Switch))), + paints + ..rrect() + ..circle( + color: activeThumbColor.withAlpha(kRadialReactionAlpha), + ), + reason: 'Default active pressed Switch should have overlay color from thumbColor', + ); + + // test inactive Switch with an overlayColor + await tester.pumpWidget(buildSwitch()); + await tester.press(find.byType(Switch)); + await tester.pumpAndSettle(); + + expect( + Material.of(tester.element(find.byType(Switch))), + paints + ..rrect() + ..circle( + color: inactivePressedOverlayColor, + ), + reason: 'Inactive pressed Switch should have overlay color: $inactivePressedOverlayColor', + ); + + // test active Switch with an overlayColor + await tester.pumpWidget(buildSwitch(active: true)); + await tester.press(find.byType(Switch)); + await tester.pumpAndSettle(); + + expect( + Material.of(tester.element(find.byType(Switch))), + paints + ..rrect() + ..circle( + color: activePressedOverlayColor, + ), + reason: 'Active pressed Switch should have overlay color: $activePressedOverlayColor', + ); + + await tester.pumpWidget(buildSwitch(focused: true)); + await tester.pumpAndSettle(); + + // Start hovering + final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse); + await gesture.addPointer(); + await gesture.moveTo(tester.getCenter(find.byType(Switch))); + await tester.pumpAndSettle(); + + expect( + Material.of(tester.element(find.byType(Switch))), + paints + ..rrect() + ..circle( + color: hoverOverlayColor, + ), + reason: 'Hovered Switch should use overlay color $hoverOverlayColor over $hoverColor', ); });