Skip to content

Commit c7b10df

Browse files
authored
Fix initial value for highlight mode on desktop platforms. (flutter#54306)
This fixes the initial value of FocusManager.highlightMode so that it gets initialized correctly on desktop platforms. My recent update of this code (flutter#52990) broke things so that the highlight mode never changed from the initial default of touch, which meant that focus highlights didn't show unless you set FocusManager.highlightStrategy to something (even automatic, the default: setting it caused the mode to be updated).
1 parent a9b5504 commit c7b10df

File tree

4 files changed

+98
-43
lines changed

4 files changed

+98
-43
lines changed

packages/flutter/lib/src/widgets/focus_manager.dart

Lines changed: 54 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1367,35 +1367,6 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier {
13671367
/// the [WidgetsBinding] instance.
13681368
static FocusManager get instance => WidgetsBinding.instance.focusManager;
13691369

1370-
bool get _lastInteractionWasTouch {
1371-
// Assume that if we're on one of these mobile platforms, or if there's no
1372-
// mouse connected, that the initial interaction will be touch-based, and
1373-
// that it's traditional mouse and keyboard on all others.
1374-
//
1375-
// This only affects the initial value: the ongoing value is updated to a
1376-
// known correct value as soon as any pointer events are received.
1377-
if (_lastInteractionWasTouchValue == null) {
1378-
switch (defaultTargetPlatform) {
1379-
case TargetPlatform.android:
1380-
case TargetPlatform.fuchsia:
1381-
case TargetPlatform.iOS:
1382-
_lastInteractionWasTouchValue = !WidgetsBinding.instance.mouseTracker.mouseIsConnected;
1383-
break;
1384-
case TargetPlatform.linux:
1385-
case TargetPlatform.macOS:
1386-
case TargetPlatform.windows:
1387-
_lastInteractionWasTouchValue = false;
1388-
break;
1389-
}
1390-
}
1391-
return _lastInteractionWasTouchValue;
1392-
}
1393-
bool _lastInteractionWasTouchValue;
1394-
set _lastInteractionWasTouch(bool value) {
1395-
_lastInteractionWasTouchValue = value;
1396-
}
1397-
1398-
13991370
/// Sets the strategy by which [highlightMode] is determined.
14001371
///
14011372
/// If set to [FocusHighlightStrategy.automatic], then the highlight mode will
@@ -1427,6 +1398,30 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier {
14271398
_updateHighlightMode();
14281399
}
14291400

1401+
static FocusHighlightMode get _defaultModeForPlatform {
1402+
// Assume that if we're on one of the mobile platforms, and there's no mouse
1403+
// connected, that the initial interaction will be touch-based, and that
1404+
// it's traditional mouse and keyboard on all other platforms.
1405+
//
1406+
// This only affects the initial value: the ongoing value is updated to a
1407+
// known correct value as soon as any pointer/keyboard events are received.
1408+
switch (defaultTargetPlatform) {
1409+
case TargetPlatform.android:
1410+
case TargetPlatform.fuchsia:
1411+
case TargetPlatform.iOS:
1412+
if (WidgetsBinding.instance.mouseTracker.mouseIsConnected) {
1413+
return FocusHighlightMode.traditional;
1414+
}
1415+
return FocusHighlightMode.touch;
1416+
case TargetPlatform.linux:
1417+
case TargetPlatform.macOS:
1418+
case TargetPlatform.windows:
1419+
return FocusHighlightMode.traditional;
1420+
}
1421+
assert(false, 'Unhandled target platform $defaultTargetPlatform');
1422+
return null;
1423+
}
1424+
14301425
/// Indicates the current interaction mode for focus highlights.
14311426
///
14321427
/// The value returned depends upon the [highlightStrategy] used, and possibly
@@ -1438,15 +1433,28 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier {
14381433
///
14391434
/// If [highlightMode] returns [FocusHighlightMode.traditional], then widgets should
14401435
/// draw their focus highlight whenever they are focused.
1441-
FocusHighlightMode get highlightMode => _highlightMode;
1442-
FocusHighlightMode _highlightMode = FocusHighlightMode.touch;
1436+
// Don't want to set _highlightMode here, since it's possible for the target
1437+
// platform to change (especially in tests).
1438+
FocusHighlightMode get highlightMode => _highlightMode ?? _defaultModeForPlatform;
1439+
FocusHighlightMode _highlightMode;
1440+
1441+
// If set, indicates if the last interaction detected was touch or not.
1442+
// If null, no interactions have occurred yet.
1443+
bool _lastInteractionWasTouch;
14431444

14441445
// Update function to be called whenever the state relating to highlightMode
14451446
// changes.
14461447
void _updateHighlightMode() {
14471448
FocusHighlightMode newMode;
14481449
switch (highlightStrategy) {
14491450
case FocusHighlightStrategy.automatic:
1451+
if (_lastInteractionWasTouch == null) {
1452+
// If we don't have any information about the last interaction yet,
1453+
// then just rely on the default value for the platform, which will be
1454+
// determined based on the target platform if _highlightMode is not
1455+
// set.
1456+
return;
1457+
}
14501458
if (_lastInteractionWasTouch) {
14511459
newMode = FocusHighlightMode.touch;
14521460
} else {
@@ -1460,8 +1468,12 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier {
14601468
newMode = FocusHighlightMode.traditional;
14611469
break;
14621470
}
1463-
if (newMode != _highlightMode) {
1464-
_highlightMode = newMode;
1471+
// We can't just compare newMode with _highlightMode here, since
1472+
// _highlightMode could be null, so we want to compare with the return value
1473+
// for the getter, since that's what clients will be looking at.
1474+
final FocusHighlightMode oldMode = highlightMode;
1475+
_highlightMode = newMode;
1476+
if (highlightMode != oldMode) {
14651477
_notifyHighlightModeListeners();
14661478
}
14671479
}
@@ -1485,7 +1497,7 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier {
14851497
for (final ValueChanged<FocusHighlightMode> listener in localListeners) {
14861498
try {
14871499
if (_listeners.contains(listener)) {
1488-
listener(_highlightMode);
1500+
listener(highlightMode);
14891501
}
14901502
} catch (exception, stack) {
14911503
InformationCollector collector;
@@ -1517,31 +1529,30 @@ class FocusManager with DiagnosticableTreeMixin, ChangeNotifier {
15171529
final FocusScopeNode rootScope = FocusScopeNode(debugLabel: 'Root Focus Scope');
15181530

15191531
void _handlePointerEvent(PointerEvent event) {
1520-
bool currentInteractionIsTouch;
1532+
FocusHighlightMode expectedMode;
15211533
switch (event.kind) {
15221534
case PointerDeviceKind.touch:
15231535
case PointerDeviceKind.stylus:
15241536
case PointerDeviceKind.invertedStylus:
1525-
currentInteractionIsTouch = true;
1537+
_lastInteractionWasTouch = true;
1538+
expectedMode = FocusHighlightMode.touch;
15261539
break;
15271540
case PointerDeviceKind.mouse:
15281541
case PointerDeviceKind.unknown:
1529-
currentInteractionIsTouch = false;
1542+
_lastInteractionWasTouch = false;
1543+
expectedMode = FocusHighlightMode.traditional;
15301544
break;
15311545
}
1532-
if (_lastInteractionWasTouch != currentInteractionIsTouch) {
1533-
_lastInteractionWasTouch = currentInteractionIsTouch;
1546+
if (expectedMode != highlightMode) {
15341547
_updateHighlightMode();
15351548
}
15361549
}
15371550

15381551
void _handleRawKeyEvent(RawKeyEvent event) {
15391552
// Update highlightMode first, since things responding to the keys might
15401553
// look at the highlight mode, and it should be accurate.
1541-
if (_lastInteractionWasTouch) {
1542-
_lastInteractionWasTouch = false;
1543-
_updateHighlightMode();
1544-
}
1554+
_lastInteractionWasTouch = false;
1555+
_updateHighlightMode();
15451556

15461557
assert(_focusDebug('Received key event ${event.logicalKey}'));
15471558
// Walk the current focus from the leaf to the root, calling each one's

packages/flutter/test/widgets/focus_manager_test.dart

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import 'package:flutter/foundation.dart';
66
import 'package:flutter/gestures.dart';
77
import 'package:flutter/material.dart';
8+
import 'package:flutter/rendering.dart';
89
import 'package:flutter/services.dart';
910
import 'package:flutter/widgets.dart';
1011
import 'package:flutter_test/flutter_test.dart';
@@ -843,6 +844,26 @@ void main() {
843844
break;
844845
}
845846
}, variant: TargetPlatformVariant.all());
847+
testWidgets('Mouse events change initial focus highlight mode on mobile.', (WidgetTester tester) async {
848+
expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.touch));
849+
RendererBinding.instance.initMouseTracker(); // Clear out the mouse state.
850+
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 0);
851+
addTearDown(gesture.removePointer);
852+
await gesture.moveTo(Offset.zero);
853+
expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.traditional));
854+
}, variant: TargetPlatformVariant.mobile());
855+
testWidgets('Mouse events change initial focus highlight mode on desktop.', (WidgetTester tester) async {
856+
expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.traditional));
857+
RendererBinding.instance.initMouseTracker(); // Clear out the mouse state.
858+
final TestGesture gesture = await tester.createGesture(kind: PointerDeviceKind.mouse, pointer: 0);
859+
addTearDown(gesture.removePointer);
860+
await gesture.moveTo(Offset.zero);
861+
expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.traditional));
862+
}, variant: TargetPlatformVariant.desktop());
863+
testWidgets('Keyboard events change initial focus highlight mode.', (WidgetTester tester) async {
864+
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
865+
expect(FocusManager.instance.highlightMode, equals(FocusHighlightMode.traditional));
866+
}, variant: TargetPlatformVariant.all());
846867
testWidgets('Events change focus highlight mode.', (WidgetTester tester) async {
847868
await setupWidget(tester);
848869
int callCount = 0;

packages/flutter_test/lib/src/widget_tester.dart

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,22 @@ class TargetPlatformVariant extends TestVariant<TargetPlatform> {
225225
/// the [TargetPlatform] enum.
226226
TargetPlatformVariant.all() : values = TargetPlatform.values.toSet();
227227

228+
/// Creates a [TargetPlatformVariant] that includes platforms that are
229+
/// considered desktop platforms.
230+
TargetPlatformVariant.desktop() : values = <TargetPlatform>{
231+
TargetPlatform.linux,
232+
TargetPlatform.macOS,
233+
TargetPlatform.windows,
234+
};
235+
236+
/// Creates a [TargetPlatformVariant] that includes platforms that are
237+
/// considered mobile platforms.
238+
TargetPlatformVariant.mobile() : values = <TargetPlatform>{
239+
TargetPlatform.android,
240+
TargetPlatform.iOS,
241+
TargetPlatform.fuchsia,
242+
};
243+
228244
/// Creates a [TargetPlatformVariant] that tests only the given value of
229245
/// [TargetPlatform].
230246
TargetPlatformVariant.only(TargetPlatform platform) : values = <TargetPlatform>{platform};

packages/flutter_test/test/widget_tester_test.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -743,6 +743,13 @@ void main() {
743743
numberOfVariationsRun += 1;
744744
}
745745
}, variant: TargetPlatformVariant.all());
746+
747+
testWidgets('TargetPlatformVariant.desktop + mobile contains all TargetPlatform values', (WidgetTester tester) async {
748+
final TargetPlatformVariant all = TargetPlatformVariant.all();
749+
final TargetPlatformVariant desktop = TargetPlatformVariant.all();
750+
final TargetPlatformVariant mobile = TargetPlatformVariant.all();
751+
expect(desktop.values.union(mobile.values), equals(all.values));
752+
});
746753
});
747754
}
748755

0 commit comments

Comments
 (0)