Fix: selection handles do not inherit color from local Theme widget (#142476)

This change uses `CapturedTheme`s to capture the themes from the context the selection handles were built in and wraps the handles with them so they can correctly inherit `Theme`s from local `Theme` widgets.

`CapturedTheme`s only captures `InheritedTheme`s, so this change also makes `_InheritedCupertinoTheme` an `InheritedTheme`. This is so we can capture themes declared under a `CupertinoTheme`, for example `primaryColor` is used as the selection handle color.

Fixes #74890
This commit is contained in:
Renzo Olivares 2024-01-30 10:12:18 -08:00 committed by GitHub
parent bd715036b5
commit 1daac1b875
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 106 additions and 6 deletions

View file

@ -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;
}

View file

@ -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(<OverlayEntry>[_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(<OverlayEntry>[_handles!.start, _handles!.end]);
}
/// {@template flutter.widgets.SelectionOverlay.hideHandles}

View file

@ -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<RenderBox> boxes = tester.renderObjectList<RenderBox>(
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 {

View file

@ -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<RenderBox> boxes = tester.renderObjectList<RenderBox>(
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>{ TargetPlatform.android, TargetPlatform.fuchsia }),
);
testWidgets('style enforces required fields', (WidgetTester tester) async {
Widget buildFrame(TextStyle style) {
return MaterialApp(