diff --git a/packages/flutter/lib/src/cupertino/theme.dart b/packages/flutter/lib/src/cupertino/theme.dart index b4daa961c1..b7c7631e2a 100644 --- a/packages/flutter/lib/src/cupertino/theme.dart +++ b/packages/flutter/lib/src/cupertino/theme.dart @@ -130,7 +130,7 @@ class CupertinoTheme extends StatelessWidget { } } -class _InheritedCupertinoTheme extends InheritedWidget { +class _InheritedCupertinoTheme extends InheritedTheme { const _InheritedCupertinoTheme({ required this.theme, required super.child, @@ -138,6 +138,11 @@ class _InheritedCupertinoTheme extends InheritedWidget { final CupertinoTheme theme; + @override + Widget wrap(BuildContext context, Widget child) { + return CupertinoTheme(data: theme.data, child: child); + } + @override bool updateShouldNotify(_InheritedCupertinoTheme old) => theme.data != old.theme.data; } diff --git a/packages/flutter/lib/src/widgets/text_selection.dart b/packages/flutter/lib/src/widgets/text_selection.dart index e83f266ce4..a7b6b75943 100644 --- a/packages/flutter/lib/src/widgets/text_selection.dart +++ b/packages/flutter/lib/src/widgets/text_selection.dart @@ -21,6 +21,7 @@ import 'debug.dart'; import 'editable_text.dart'; import 'framework.dart'; import 'gesture_detector.dart'; +import 'inherited_theme.dart'; import 'magnifier.dart'; import 'overlay.dart'; import 'scrollable.dart'; @@ -1375,12 +1376,22 @@ class SelectionOverlay { return; } - _handles = ( - start: OverlayEntry(builder: _buildStartHandle), - end: OverlayEntry(builder: _buildEndHandle), + final OverlayState overlay = Overlay.of(context, rootOverlay: true, debugRequiredFor: debugRequiredFor); + + final CapturedThemes capturedThemes = InheritedTheme.capture( + from: context, + to: overlay.context, ); - Overlay.of(context, rootOverlay: true, debugRequiredFor: debugRequiredFor) - .insertAll([_handles!.start, _handles!.end]); + + _handles = ( + start: OverlayEntry(builder: (BuildContext context) { + return capturedThemes.wrap(_buildStartHandle(context)); + }), + end: OverlayEntry(builder: (BuildContext context) { + return capturedThemes.wrap(_buildEndHandle(context)); + }), + ); + overlay.insertAll([_handles!.start, _handles!.end]); } /// {@template flutter.widgets.SelectionOverlay.hideHandles} diff --git a/packages/flutter/test/cupertino/text_field_test.dart b/packages/flutter/test/cupertino/text_field_test.dart index 98d73d235f..002c8a2afb 100644 --- a/packages/flutter/test/cupertino/text_field_test.dart +++ b/packages/flutter/test/cupertino/text_field_test.dart @@ -621,6 +621,47 @@ void main() { }, ); + testWidgets('selection handles color respects CupertinoTheme', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/74890. + const Color expectedSelectionHandleColor = Color.fromARGB(255, 10, 200, 255); + + final TextEditingController controller = TextEditingController(text: 'Some text.'); + + await tester.pumpWidget( + CupertinoApp( + theme: const CupertinoThemeData( + primaryColor: Colors.red, + ), + home: Center( + child: CupertinoTheme( + data: const CupertinoThemeData( + primaryColor: expectedSelectionHandleColor, + ), + child: CupertinoTextField(controller: controller), + ), + ), + ), + ); + + await tester.tapAt(textOffsetToPosition(tester, 0)); + await tester.pump(); + await tester.tapAt(textOffsetToPosition(tester, 0)); + await tester.pumpAndSettle(); + final Iterable boxes = tester.renderObjectList( + find.descendant( + of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_SelectionHandleOverlay'), + matching: find.byType(CustomPaint), + ), + ); + expect(boxes.length, 2); + + for (final RenderBox box in boxes) { + expect(box, paints..path(color: expectedSelectionHandleColor)); + } + }, + variant: TargetPlatformVariant.only(TargetPlatform.iOS), + ); + testWidgets( 'uses DefaultSelectionStyle for selection and cursor colors if provided', (WidgetTester tester) async { diff --git a/packages/flutter/test/material/text_field_test.dart b/packages/flutter/test/material/text_field_test.dart index 477da112ec..cea5f25ac0 100644 --- a/packages/flutter/test/material/text_field_test.dart +++ b/packages/flutter/test/material/text_field_test.dart @@ -9292,6 +9292,49 @@ void main() { expect(editableText.style.color, isNull); }); + testWidgets('selection handles color respects Theme', (WidgetTester tester) async { + // Regression test for https://github.com/flutter/flutter/issues/74890. + const Color expectedSelectionHandleColor = Color.fromARGB(255, 10, 200, 255); + + final TextEditingController controller = TextEditingController(text: 'Some text.'); + + await tester.pumpWidget( + MaterialApp( + theme: ThemeData( + textSelectionTheme: const TextSelectionThemeData( + selectionHandleColor: Colors.red, + ), + ), + home: Material( + child: Theme( + data: ThemeData( + textSelectionTheme: const TextSelectionThemeData( + selectionHandleColor: expectedSelectionHandleColor, + ), + ), + child: TextField(controller: controller), + ), + ), + ), + ); + + await tester.longPressAt(textOffsetToPosition(tester, 0)); + await tester.pumpAndSettle(); + final Iterable boxes = tester.renderObjectList( + find.descendant( + of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_SelectionHandleOverlay'), + matching: find.byType(CustomPaint), + ), + ); + expect(boxes.length, 2); + + for (final RenderBox box in boxes) { + expect(box, paints..path(color: expectedSelectionHandleColor)); + } + }, + variant: const TargetPlatformVariant({ TargetPlatform.android, TargetPlatform.fuchsia }), + ); + testWidgets('style enforces required fields', (WidgetTester tester) async { Widget buildFrame(TextStyle style) { return MaterialApp(