1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'package:flutter/foundation.dart';
6
7import 'actions.dart';
8import 'basic.dart';
9import 'focus_manager.dart';
10import 'focus_scope.dart';
11import 'framework.dart';
12import 'scroll_position.dart';
13import 'scrollable.dart';
14
15// Examples can assume:
16// late BuildContext context;
17// FocusNode focusNode = FocusNode();
18
19// BuildContext/Element doesn't have a parent accessor, but it can be simulated
20// with visitAncestorElements. _getAncestor is needed because
21// context.getElementForInheritedWidgetOfExactType will return itself if it
22// happens to be of the correct type. _getAncestor should be O(count), since we
23// always return false at a specific ancestor. By default it returns the parent,
24// which is O(1).
25BuildContext? _getAncestor(BuildContext context, {int count = 1}) {
26 BuildContext? target;
27 context.visitAncestorElements((Element ancestor) {
28 count--;
29 if (count == 0) {
30 target = ancestor;
31 return false;
32 }
33 return true;
34 });
35 return target;
36}
37
38/// Signature for the callback that's called when a traversal policy
39/// requests focus.
40typedef TraversalRequestFocusCallback = void Function(
41 FocusNode node, {
42 ScrollPositionAlignmentPolicy? alignmentPolicy,
43 double? alignment,
44 Duration? duration,
45 Curve? curve,
46});
47
48// A class to temporarily hold information about FocusTraversalGroups when
49// sorting their contents.
50class _FocusTraversalGroupInfo {
51 _FocusTraversalGroupInfo(
52 _FocusTraversalGroupNode? group, {
53 FocusTraversalPolicy? defaultPolicy,
54 List<FocusNode>? members,
55 }) : groupNode = group,
56 policy = group?.policy ?? defaultPolicy ?? ReadingOrderTraversalPolicy(),
57 members = members ?? <FocusNode>[];
58
59 final FocusNode? groupNode;
60 final FocusTraversalPolicy policy;
61 final List<FocusNode> members;
62}
63
64/// A direction along either the horizontal or vertical axes.
65///
66/// This is used by the [DirectionalFocusTraversalPolicyMixin], and
67/// [FocusNode.focusInDirection] to indicate which direction to look in for the
68/// next focus.
69enum TraversalDirection {
70 /// Indicates a direction above the currently focused widget.
71 up,
72
73 /// Indicates a direction to the right of the currently focused widget.
74 ///
75 /// This direction is unaffected by the [Directionality] of the current
76 /// context.
77 right,
78
79 /// Indicates a direction below the currently focused widget.
80 down,
81
82 /// Indicates a direction to the left of the currently focused widget.
83 ///
84 /// This direction is unaffected by the [Directionality] of the current
85 /// context.
86 left,
87}
88
89/// Controls the transfer of focus beyond the first and the last items of a
90/// [FocusScopeNode].
91///
92/// This enumeration only controls the traversal behavior performed by
93/// [FocusTraversalPolicy]. Other methods of focus transfer, such as direct
94/// calls to [FocusNode.requestFocus] and [FocusNode.unfocus], are not affected
95/// by this enumeration.
96///
97/// See also:
98///
99/// * [FocusTraversalPolicy], which implements the logic behind this enum.
100/// * [FocusScopeNode], which is configured by this enum.
101enum TraversalEdgeBehavior {
102 /// Keeps the focus among the items of the focus scope.
103 ///
104 /// Requesting the next focus after the last focusable item will transfer the
105 /// focus to the first item, and requesting focus previous to the first item
106 /// will transfer the focus to the last item, thus forming a closed loop of
107 /// focusable items.
108 closedLoop,
109
110 /// Allows the focus to leave the [FlutterView].
111 ///
112 /// Requesting next focus after the last focusable item or previous to the
113 /// first item will unfocus any focused nodes. If the focus traversal action
114 /// was initiated by the embedder (e.g. the Flutter Engine) the embedder
115 /// receives a result indicating that the focus is no longer within the
116 /// current [FlutterView]. For example, [NextFocusAction] invoked via keyboard
117 /// (typically the TAB key) would receive [KeyEventResult.skipRemainingHandlers]
118 /// allowing the embedder handle the shortcut. On the web, typically the
119 /// control is transferred to the browser, allowing the user to reach the
120 /// address bar, escape an `iframe`, or focus on HTML elements other than
121 /// those managed by Flutter.
122 leaveFlutterView,
123
124 /// Allows focus to traverse up to parent scope.
125 ///
126 /// When reaching the edge of the current scope, requesting the next focus
127 /// will look up to the parent scope of the current scope and focus the focus
128 /// node next to the current scope.
129 ///
130 /// If there is no parent scope above the current scope, fallback to
131 /// [closedLoop] behavior.
132 parentScope,
133}
134
135/// Determines how focusable widgets are traversed within a [FocusTraversalGroup].
136///
137/// The focus traversal policy is what determines which widget is "next",
138/// "previous", or in a direction from the widget associated with the currently
139/// focused [FocusNode] (usually a [Focus] widget).
140///
141/// One of the pre-defined subclasses may be used, or define a custom policy to
142/// create a unique focus order.
143///
144/// When defining your own, your subclass should implement [sortDescendants] to
145/// provide the order in which you would like the descendants to be traversed.
146///
147/// See also:
148///
149/// * [FocusNode], for a description of the focus system.
150/// * [FocusTraversalGroup], a widget that groups together and imposes a
151/// traversal policy on the [Focus] nodes below it in the widget hierarchy.
152/// * [FocusNode], which is affected by the traversal policy.
153/// * [WidgetOrderTraversalPolicy], a policy that relies on the widget
154/// creation order to describe the order of traversal.
155/// * [ReadingOrderTraversalPolicy], a policy that describes the order as the
156/// natural "reading order" for the current [Directionality].
157/// * [OrderedTraversalPolicy], a policy that describes the order
158/// explicitly using [FocusTraversalOrder] widgets.
159/// * [DirectionalFocusTraversalPolicyMixin] a mixin class that implements
160/// focus traversal in a direction.
161@immutable
162abstract class FocusTraversalPolicy with Diagnosticable {
163 /// Abstract const constructor. This constructor enables subclasses to provide
164 /// const constructors so that they can be used in const expressions.
165 ///
166 /// {@template flutter.widgets.FocusTraversalPolicy.requestFocusCallback}
167 /// The `requestFocusCallback` can be used to override the default behavior
168 /// of the focus requests. If `requestFocusCallback`
169 /// is null, it defaults to [FocusTraversalPolicy.defaultTraversalRequestFocusCallback].
170 /// {@endtemplate}
171 const FocusTraversalPolicy({
172 TraversalRequestFocusCallback? requestFocusCallback
173 }) : requestFocusCallback = requestFocusCallback ?? defaultTraversalRequestFocusCallback;
174
175 /// The callback used to move the focus from one focus node to another when
176 /// traversing them using a keyboard. By default it requests focus on the next
177 /// node and ensures the node is visible if it's in a scrollable.
178 final TraversalRequestFocusCallback requestFocusCallback;
179
180 /// The default value for [requestFocusCallback].
181 /// Requests focus from `node` and ensures the node is visible
182 /// by calling [Scrollable.ensureVisible].
183 static void defaultTraversalRequestFocusCallback(
184 FocusNode node, {
185 ScrollPositionAlignmentPolicy? alignmentPolicy,
186 double? alignment,
187 Duration? duration,
188 Curve? curve,
189 }) {
190 node.requestFocus();
191 Scrollable.ensureVisible(
192 node.context!,
193 alignment: alignment ?? 1,
194 alignmentPolicy: alignmentPolicy ?? ScrollPositionAlignmentPolicy.explicit,
195 duration: duration ?? Duration.zero,
196 curve: curve ?? Curves.ease,
197 );
198 }
199
200 /// Request focus on a focus node as a result of a tab traversal.
201 ///
202 /// If the `node` is a [FocusScopeNode], this method will recursively find
203 /// the next focus from its descendants until it find a regular [FocusNode].
204 ///
205 /// Returns true if this method focused a new focus node.
206 bool _requestTabTraversalFocus(
207 FocusNode node, {
208 ScrollPositionAlignmentPolicy? alignmentPolicy,
209 double? alignment,
210 Duration? duration,
211 Curve? curve,
212 required bool forward,
213 }) {
214 if (node is FocusScopeNode) {
215 if (node.focusedChild != null) {
216 // Can't stop here as the `focusedChild` may be a focus scope node
217 // without a first focus. The first focus will be picked in the
218 // next iteration.
219 return _requestTabTraversalFocus(
220 node.focusedChild!,
221 alignmentPolicy: alignmentPolicy,
222 alignment: alignment,
223 duration: duration,
224 curve: curve,
225 forward: forward,
226 );
227 }
228 final List<FocusNode> sortedChildren = _sortAllDescendants(node, node);
229 if (sortedChildren.isNotEmpty) {
230 _requestTabTraversalFocus(
231 forward ? sortedChildren.first : sortedChildren.last,
232 alignmentPolicy: alignmentPolicy,
233 alignment: alignment,
234 duration: duration,
235 curve: curve,
236 forward: forward,
237 );
238 // Regardless if _requestTabTraversalFocus return true or false, a first
239 // focus has been picked.
240 return true;
241 }
242 }
243 final bool nodeHadPrimaryFocus = node.hasPrimaryFocus;
244 requestFocusCallback(
245 node,
246 alignmentPolicy: alignmentPolicy,
247 alignment: alignment,
248 duration: duration,
249 curve: curve,
250 );
251 return !nodeHadPrimaryFocus;
252 }
253
254 /// Returns the node that should receive focus if focus is traversing
255 /// forwards, and there is no current focus.
256 ///
257 /// The node returned is the node that should receive focus if focus is
258 /// traversing forwards (i.e. with [next]), and there is no current focus in
259 /// the nearest [FocusScopeNode] that `currentNode` belongs to.
260 ///
261 /// If `ignoreCurrentFocus` is false or not given, this function returns the
262 /// [FocusScopeNode.focusedChild], if set, on the nearest scope of the
263 /// `currentNode`, otherwise, returns the first node from [sortDescendants],
264 /// or the given `currentNode` if there are no descendants.
265 ///
266 /// If `ignoreCurrentFocus` is true, then the algorithm returns the first node
267 /// from [sortDescendants], or the given `currentNode` if there are no
268 /// descendants.
269 ///
270 /// See also:
271 ///
272 /// * [next], the function that is called to move the focus to the next node.
273 /// * [DirectionalFocusTraversalPolicyMixin.findFirstFocusInDirection], a
274 /// function that finds the first focusable widget in a particular
275 /// direction.
276 FocusNode? findFirstFocus(FocusNode currentNode, {bool ignoreCurrentFocus = false}) {
277 return _findInitialFocus(currentNode, ignoreCurrentFocus: ignoreCurrentFocus);
278 }
279
280 /// Returns the node that should receive focus if focus is traversing
281 /// backwards, and there is no current focus.
282 ///
283 /// The node returned is the one that should receive focus if focus is
284 /// traversing backwards (i.e. with [previous]), and there is no current focus
285 /// in the nearest [FocusScopeNode] that `currentNode` belongs to.
286 ///
287 /// If `ignoreCurrentFocus` is false or not given, this function returns the
288 /// [FocusScopeNode.focusedChild], if set, on the nearest scope of the
289 /// `currentNode`, otherwise, returns the last node from [sortDescendants],
290 /// or the given `currentNode` if there are no descendants.
291 ///
292 /// If `ignoreCurrentFocus` is true, then the algorithm returns the last node
293 /// from [sortDescendants], or the given `currentNode` if there are no
294 /// descendants.
295 ///
296 /// See also:
297 ///
298 /// * [previous], the function that is called to move the focus to the previous node.
299 /// * [DirectionalFocusTraversalPolicyMixin.findFirstFocusInDirection], a
300 /// function that finds the first focusable widget in a particular direction.
301 FocusNode findLastFocus(FocusNode currentNode, {bool ignoreCurrentFocus = false}) {
302 return _findInitialFocus(currentNode, fromEnd: true, ignoreCurrentFocus: ignoreCurrentFocus);
303 }
304
305 FocusNode _findInitialFocus(FocusNode currentNode, {bool fromEnd = false, bool ignoreCurrentFocus = false}) {
306 final FocusScopeNode scope = currentNode.nearestScope!;
307 FocusNode? candidate = scope.focusedChild;
308 if (ignoreCurrentFocus || candidate == null && scope.descendants.isNotEmpty) {
309 final Iterable<FocusNode> sorted = _sortAllDescendants(scope, currentNode).where((FocusNode node) => _canRequestTraversalFocus(node));
310 if (sorted.isEmpty) {
311 candidate = null;
312 } else {
313 candidate = fromEnd ? sorted.last : sorted.first;
314 }
315 }
316
317 // If we still didn't find any candidate, use the current node as a
318 // fallback.
319 candidate ??= currentNode;
320 return candidate;
321 }
322
323 /// Returns the first node in the given `direction` that should receive focus
324 /// if there is no current focus in the scope to which the `currentNode`
325 /// belongs.
326 ///
327 /// This is typically used by [inDirection] to determine which node to focus
328 /// if it is called when no node is currently focused.
329 FocusNode? findFirstFocusInDirection(FocusNode currentNode, TraversalDirection direction);
330
331 /// Clears the data associated with the given [FocusScopeNode] for this object.
332 ///
333 /// This is used to indicate that the focus policy has changed its mode, and
334 /// so any cached policy data should be invalidated. For example, changing the
335 /// direction in which focus is moving, or changing from directional to
336 /// next/previous navigation modes.
337 ///
338 /// The default implementation does nothing.
339 @mustCallSuper
340 void invalidateScopeData(FocusScopeNode node) {}
341
342 /// This is called whenever the given [node] is re-parented into a new scope,
343 /// so that the policy has a chance to update or invalidate any cached data
344 /// that it maintains per scope about the node.
345 ///
346 /// The [oldScope] is the previous scope that this node belonged to, if any.
347 ///
348 /// The default implementation does nothing.
349 @mustCallSuper
350 void changedScope({FocusNode? node, FocusScopeNode? oldScope}) {}
351
352 /// Focuses the next widget in the focus scope that contains the given
353 /// [currentNode].
354 ///
355 /// This should determine what the next node to receive focus should be by
356 /// inspecting the node tree, and then calling [FocusNode.requestFocus] on
357 /// the node that has been selected.
358 ///
359 /// Returns true if it successfully found a node and requested focus.
360 bool next(FocusNode currentNode) => _moveFocus(currentNode, forward: true);
361
362 /// Focuses the previous widget in the focus scope that contains the given
363 /// [currentNode].
364 ///
365 /// This should determine what the previous node to receive focus should be by
366 /// inspecting the node tree, and then calling [FocusNode.requestFocus] on
367 /// the node that has been selected.
368 ///
369 /// Returns true if it successfully found a node and requested focus.
370 bool previous(FocusNode currentNode) => _moveFocus(currentNode, forward: false);
371
372 /// Focuses the next widget in the given [direction] in the focus scope that
373 /// contains the given [currentNode].
374 ///
375 /// This should determine what the next node to receive focus in the given
376 /// [direction] should be by inspecting the node tree, and then calling
377 /// [FocusNode.requestFocus] on the node that has been selected.
378 ///
379 /// Returns true if it successfully found a node and requested focus.
380 bool inDirection(FocusNode currentNode, TraversalDirection direction);
381
382 /// Sorts the given `descendants` into focus order.
383 ///
384 /// Subclasses should override this to implement a different sort for [next]
385 /// and [previous] to use in their ordering. If the returned iterable omits a
386 /// node that is a descendant of the given scope, then the user will be unable
387 /// to use next/previous keyboard traversal to reach that node.
388 ///
389 /// The node used to initiate the traversal (the one passed to [next] or
390 /// [previous]) is passed as `currentNode`.
391 ///
392 /// Having the current node in the list is what allows the algorithm to
393 /// determine which nodes are adjacent to the current node. If the
394 /// `currentNode` is removed from the list, then the focus will be unchanged
395 /// when [next] or [previous] are called, and they will return false.
396 ///
397 /// This is not used for directional focus ([inDirection]), only for
398 /// determining the focus order for [next] and [previous].
399 ///
400 /// When implementing an override for this function, be sure to use
401 /// [mergeSort] instead of Dart's default list sorting algorithm when sorting
402 /// items, since the default algorithm is not stable (items deemed to be equal
403 /// can appear in arbitrary order, and change positions between sorts), whereas
404 /// [mergeSort] is stable.
405 @protected
406 Iterable<FocusNode> sortDescendants(Iterable<FocusNode> descendants, FocusNode currentNode);
407
408 static bool _canRequestTraversalFocus(FocusNode node) {
409 return node.canRequestFocus && !node.skipTraversal;
410 }
411
412 static Iterable<FocusNode> _getDescendantsWithoutExpandingScope(FocusNode node) {
413 final List<FocusNode> result = <FocusNode>[];
414 for (final FocusNode child in node.children) {
415 result.add(child);
416 if (child is! FocusScopeNode) {
417 result.addAll(_getDescendantsWithoutExpandingScope(child));
418 }
419 }
420 return result;
421 }
422
423 static Map<FocusNode?, _FocusTraversalGroupInfo> _findGroups(FocusScopeNode scope, _FocusTraversalGroupNode? scopeGroupNode, FocusNode currentNode) {
424 final FocusTraversalPolicy defaultPolicy = scopeGroupNode?.policy ?? ReadingOrderTraversalPolicy();
425 final Map<FocusNode?, _FocusTraversalGroupInfo> groups = <FocusNode?, _FocusTraversalGroupInfo>{};
426 for (final FocusNode node in _getDescendantsWithoutExpandingScope(scope)) {
427 final _FocusTraversalGroupNode? groupNode = FocusTraversalGroup._getGroupNode(node);
428 // Group nodes need to be added to their parent's node, or to the "null"
429 // node if no parent is found. This creates the hierarchy of group nodes
430 // and makes it so the entire group is sorted along with the other members
431 // of the parent group.
432 if (node == groupNode) {
433 // To find the parent of the group node, we need to skip over the parent
434 // of the Focus node added in _FocusTraversalGroupState.build, and start
435 // looking with that node's parent, since _getGroupNode will return the
436 // node it was called on if it matches the type.
437 final _FocusTraversalGroupNode? parentGroup = FocusTraversalGroup._getGroupNode(groupNode!.parent!);
438 groups[parentGroup] ??= _FocusTraversalGroupInfo(parentGroup, members: <FocusNode>[], defaultPolicy: defaultPolicy);
439 assert(!groups[parentGroup]!.members.contains(node));
440 groups[parentGroup]!.members.add(groupNode);
441 continue;
442 }
443 // Skip non-focusable and non-traversable nodes in the same way that
444 // FocusScopeNode.traversalDescendants would.
445 //
446 // Current focused node needs to be in the group so that the caller can
447 // find the next traversable node from the current focused node.
448 if (node == currentNode || (node.canRequestFocus && !node.skipTraversal)) {
449 groups[groupNode] ??= _FocusTraversalGroupInfo(groupNode, members: <FocusNode>[], defaultPolicy: defaultPolicy);
450 assert(!groups[groupNode]!.members.contains(node));
451 groups[groupNode]!.members.add(node);
452 }
453 }
454 return groups;
455 }
456
457 // Sort all descendants, taking into account the FocusTraversalGroup
458 // that they are each in, and filtering out non-traversable/focusable nodes.
459 static List<FocusNode> _sortAllDescendants(FocusScopeNode scope, FocusNode currentNode) {
460 final _FocusTraversalGroupNode? scopeGroupNode = FocusTraversalGroup._getGroupNode(scope);
461 // Build the sorting data structure, separating descendants into groups.
462 final Map<FocusNode?, _FocusTraversalGroupInfo> groups = _findGroups(scope, scopeGroupNode, currentNode);
463
464 // Sort the member lists using the individual policy sorts.
465 for (final FocusNode? key in groups.keys) {
466 final List<FocusNode> sortedMembers = groups[key]!.policy.sortDescendants(groups[key]!.members, currentNode).toList();
467 groups[key]!.members.clear();
468 groups[key]!.members.addAll(sortedMembers);
469 }
470
471 // Traverse the group tree, adding the children of members in the order they
472 // appear in the member lists.
473 final List<FocusNode> sortedDescendants = <FocusNode>[];
474 void visitGroups(_FocusTraversalGroupInfo info) {
475 for (final FocusNode node in info.members) {
476 if (groups.containsKey(node)) {
477 // This is a policy group focus node. Replace it with the members of
478 // the corresponding policy group.
479 visitGroups(groups[node]!);
480 } else {
481 sortedDescendants.add(node);
482 }
483 }
484 }
485
486 // Visit the children of the scope, if any.
487 if (groups.isNotEmpty && groups.containsKey(scopeGroupNode)) {
488 visitGroups(groups[scopeGroupNode]!);
489 }
490
491 // Remove the FocusTraversalGroup nodes themselves, which aren't focusable.
492 // They were left in above because they were needed to find their members
493 // during sorting.
494 sortedDescendants.removeWhere((FocusNode node) {
495 return node != currentNode && !_canRequestTraversalFocus(node);
496 });
497
498 // Sanity check to make sure that the algorithm above doesn't diverge from
499 // the one in FocusScopeNode.traversalDescendants in terms of which nodes it
500 // finds.
501 assert((){
502 final Set<FocusNode> difference = sortedDescendants.toSet().difference(scope.traversalDescendants.toSet());
503 if (!_canRequestTraversalFocus(currentNode)) {
504 // The scope.traversalDescendants will not contain currentNode if it
505 // skips traversal or not focusable.
506 assert(
507 difference.isEmpty || (difference.length == 1 && difference.contains(currentNode)),
508 'Difference between sorted descendants and FocusScopeNode.traversalDescendants contains '
509 'something other than the current skipped node. This is the difference: $difference',
510 );
511 return true;
512 }
513 assert(
514 difference.isEmpty,
515 'Sorted descendants contains different nodes than FocusScopeNode.traversalDescendants would. '
516 'These are the different nodes: $difference',
517 );
518 return true;
519 }());
520 return sortedDescendants;
521 }
522
523 /// Moves the focus to the next node in the FocusScopeNode nearest to the
524 /// currentNode argument, either in a forward or reverse direction, depending
525 /// on the value of the forward argument.
526 ///
527 /// This function is called by the next and previous members to move to the
528 /// next or previous node, respectively.
529 ///
530 /// Uses [findFirstFocus]/[findLastFocus] to find the first/last node if there is
531 /// no [FocusScopeNode.focusedChild] set. If there is a focused child for the
532 /// scope, then it calls sortDescendants to get a sorted list of descendants,
533 /// and then finds the node after the current first focus of the scope if
534 /// forward is true, and the node before it if forward is false.
535 ///
536 /// Returns true if a node requested focus.
537 @protected
538 bool _moveFocus(FocusNode currentNode, {required bool forward}) {
539 final FocusScopeNode nearestScope = currentNode.nearestScope!;
540 invalidateScopeData(nearestScope);
541 FocusNode? focusedChild = nearestScope.focusedChild;
542 if (focusedChild == null) {
543 final FocusNode? firstFocus = forward ? findFirstFocus(currentNode) : findLastFocus(currentNode);
544 if (firstFocus != null) {
545 return _requestTabTraversalFocus(
546 firstFocus,
547 alignmentPolicy: forward ? ScrollPositionAlignmentPolicy.keepVisibleAtEnd : ScrollPositionAlignmentPolicy.keepVisibleAtStart,
548 forward: forward,
549 );
550 }
551 }
552 focusedChild ??= nearestScope;
553 final List<FocusNode> sortedNodes = _sortAllDescendants(nearestScope, focusedChild);
554 assert(sortedNodes.contains(focusedChild));
555
556 if (forward && focusedChild == sortedNodes.last) {
557 switch (nearestScope.traversalEdgeBehavior) {
558 case TraversalEdgeBehavior.leaveFlutterView:
559 focusedChild.unfocus();
560 return false;
561 case TraversalEdgeBehavior.parentScope:
562 final FocusScopeNode? parentScope = nearestScope.enclosingScope;
563 if (parentScope != null && parentScope != FocusManager.instance.rootScope) {
564 focusedChild.unfocus();
565 parentScope.nextFocus();
566 // Verify the focus really has changed.
567 return focusedChild.enclosingScope?.focusedChild != focusedChild;
568 }
569 // No valid parent scope. Fallback to closed loop behavior.
570 return _requestTabTraversalFocus(
571 sortedNodes.first,
572 alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd,
573 forward: forward,
574 );
575 case TraversalEdgeBehavior.closedLoop:
576 return _requestTabTraversalFocus(
577 sortedNodes.first,
578 alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd,
579 forward: forward,
580 );
581 }
582 }
583 if (!forward && focusedChild == sortedNodes.first) {
584 switch (nearestScope.traversalEdgeBehavior) {
585 case TraversalEdgeBehavior.leaveFlutterView:
586 focusedChild.unfocus();
587 return false;
588 case TraversalEdgeBehavior.parentScope:
589 final FocusScopeNode? parentScope = nearestScope.enclosingScope;
590 if (parentScope != null && parentScope != FocusManager.instance.rootScope) {
591 focusedChild.unfocus();
592 parentScope.previousFocus();
593 // Verify the focus really has changed.
594 return focusedChild.enclosingScope?.focusedChild != focusedChild;
595 }
596 // No valid parent scope. Fallback to closed loop behavior.
597 return _requestTabTraversalFocus(
598 sortedNodes.last,
599 alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart,
600 forward: forward,
601 );
602 case TraversalEdgeBehavior.closedLoop:
603 return _requestTabTraversalFocus(
604 sortedNodes.last,
605 alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart,
606 forward: forward,
607 );
608 }
609 }
610
611 final Iterable<FocusNode> maybeFlipped = forward ? sortedNodes : sortedNodes.reversed;
612 FocusNode? previousNode;
613 for (final FocusNode node in maybeFlipped) {
614 if (previousNode == focusedChild) {
615 return _requestTabTraversalFocus(
616 node,
617 alignmentPolicy: forward ? ScrollPositionAlignmentPolicy.keepVisibleAtEnd : ScrollPositionAlignmentPolicy.keepVisibleAtStart,
618 forward: forward,
619 );
620 }
621 previousNode = node;
622 }
623 return false;
624 }
625}
626
627// A policy data object for use by the DirectionalFocusTraversalPolicyMixin so
628// it can keep track of the traversal history.
629class _DirectionalPolicyDataEntry {
630 const _DirectionalPolicyDataEntry({required this.direction, required this.node});
631
632 final TraversalDirection direction;
633 final FocusNode node;
634}
635
636class _DirectionalPolicyData {
637 const _DirectionalPolicyData({required this.history});
638
639 /// A queue of entries that describe the path taken to the current node.
640 final List<_DirectionalPolicyDataEntry> history;
641}
642
643/// A mixin class that provides an implementation for finding a node in a
644/// particular direction.
645///
646/// This can be mixed in to other [FocusTraversalPolicy] implementations that
647/// only want to implement new next/previous policies.
648///
649/// Since hysteresis in the navigation order is undesirable, this implementation
650/// maintains a stack of previous locations that have been visited on the policy
651/// data for the affected [FocusScopeNode]. If the previous direction was the
652/// opposite of the current direction, then the this policy will request focus
653/// on the previously focused node. Change to another direction other than the
654/// current one or its opposite will clear the stack.
655///
656/// For instance, if the focus moves down, down, down, and then up, up, up, it
657/// will follow the same path through the widgets in both directions. However,
658/// if it moves down, down, down, left, right, and then up, up, up, it may not
659/// follow the same path on the way up as it did on the way down, since changing
660/// the axis of motion resets the history.
661///
662/// This class implements an algorithm that considers an infinite band extending
663/// along the direction of movement, the width or height (depending on
664/// direction) of the currently focused widget, and finds the closest widget in
665/// that band along the direction of movement. If nothing is found in that band,
666/// then it picks the widget with an edge closest to the band in the
667/// perpendicular direction. If two out-of-band widgets are the same distance
668/// from the band, then it picks the one closest along the direction of
669/// movement.
670///
671/// The goal of this algorithm is to pick a widget that (to the user) doesn't
672/// appear to traverse along the wrong axis, as it might if it only sorted
673/// widgets by distance along one axis, but also jumps to the next logical
674/// widget in a direction without skipping over widgets.
675///
676/// See also:
677///
678/// * [FocusNode], for a description of the focus system.
679/// * [FocusTraversalGroup], a widget that groups together and imposes a
680/// traversal policy on the [Focus] nodes below it in the widget hierarchy.
681/// * [WidgetOrderTraversalPolicy], a policy that relies on the widget creation
682/// order to describe the order of traversal.
683/// * [ReadingOrderTraversalPolicy], a policy that describes the order as the
684/// natural "reading order" for the current [Directionality].
685/// * [OrderedTraversalPolicy], a policy that describes the order explicitly
686/// using [FocusTraversalOrder] widgets.
687mixin DirectionalFocusTraversalPolicyMixin on FocusTraversalPolicy {
688 final Map<FocusScopeNode, _DirectionalPolicyData> _policyData = <FocusScopeNode, _DirectionalPolicyData>{};
689
690 @override
691 void invalidateScopeData(FocusScopeNode node) {
692 super.invalidateScopeData(node);
693 _policyData.remove(node);
694 }
695
696 @override
697 void changedScope({FocusNode? node, FocusScopeNode? oldScope}) {
698 super.changedScope(node: node, oldScope: oldScope);
699 if (oldScope != null) {
700 _policyData[oldScope]?.history.removeWhere((_DirectionalPolicyDataEntry entry) {
701 return entry.node == node;
702 });
703 }
704 }
705
706 @override
707 FocusNode? findFirstFocusInDirection(FocusNode currentNode, TraversalDirection direction) {
708 switch (direction) {
709 case TraversalDirection.up:
710 // Find the bottom-most node so we can go up from there.
711 return _sortAndFindInitial(currentNode, vertical: true, first: false);
712 case TraversalDirection.down:
713 // Find the top-most node so we can go down from there.
714 return _sortAndFindInitial(currentNode, vertical: true, first: true);
715 case TraversalDirection.left:
716 // Find the right-most node so we can go left from there.
717 return _sortAndFindInitial(currentNode, vertical: false, first: false);
718 case TraversalDirection.right:
719 // Find the left-most node so we can go right from there.
720 return _sortAndFindInitial(currentNode, vertical: false, first: true);
721 }
722 }
723
724 FocusNode? _sortAndFindInitial(FocusNode currentNode, {required bool vertical, required bool first}) {
725 final Iterable<FocusNode> nodes = currentNode.nearestScope!.traversalDescendants;
726 final List<FocusNode> sorted = nodes.toList();
727 mergeSort<FocusNode>(sorted, compare: (FocusNode a, FocusNode b) {
728 if (vertical) {
729 if (first) {
730 return a.rect.top.compareTo(b.rect.top);
731 } else {
732 return b.rect.bottom.compareTo(a.rect.bottom);
733 }
734 } else {
735 if (first) {
736 return a.rect.left.compareTo(b.rect.left);
737 } else {
738 return b.rect.right.compareTo(a.rect.right);
739 }
740 }
741 });
742
743 if (sorted.isNotEmpty) {
744 return sorted.first;
745 }
746
747 return null;
748 }
749
750 static int _verticalCompare(Offset target, Offset a, Offset b) {
751 return (a.dy - target.dy).abs().compareTo((b.dy - target.dy).abs());
752 }
753
754 static int _horizontalCompare(Offset target, Offset a, Offset b) {
755 return (a.dx - target.dx).abs().compareTo((b.dx - target.dx).abs());
756 }
757
758 // Sort the ones that are closest to target vertically first, and if two are
759 // the same vertical distance, pick the one that is closest horizontally.
760 static Iterable<FocusNode> _sortByDistancePreferVertical(Offset target, Iterable<FocusNode> nodes) {
761 final List<FocusNode> sorted = nodes.toList();
762 mergeSort<FocusNode>(sorted, compare: (FocusNode nodeA, FocusNode nodeB) {
763 final Offset a = nodeA.rect.center;
764 final Offset b = nodeB.rect.center;
765 final int vertical = _verticalCompare(target, a, b);
766 if (vertical == 0) {
767 return _horizontalCompare(target, a, b);
768 }
769 return vertical;
770 });
771 return sorted;
772 }
773
774 // Sort the ones that are closest horizontally first, and if two are the same
775 // horizontal distance, pick the one that is closest vertically.
776 static Iterable<FocusNode> _sortByDistancePreferHorizontal(Offset target, Iterable<FocusNode> nodes) {
777 final List<FocusNode> sorted = nodes.toList();
778 mergeSort<FocusNode>(sorted, compare: (FocusNode nodeA, FocusNode nodeB) {
779 final Offset a = nodeA.rect.center;
780 final Offset b = nodeB.rect.center;
781 final int horizontal = _horizontalCompare(target, a, b);
782 if (horizontal == 0) {
783 return _verticalCompare(target, a, b);
784 }
785 return horizontal;
786 });
787 return sorted;
788 }
789
790 static int _verticalCompareClosestEdge(Offset target, Rect a, Rect b) {
791 // Find which edge is closest to the target for each.
792 final double aCoord = (a.top - target.dy).abs() < (a.bottom - target.dy).abs() ? a.top : a.bottom;
793 final double bCoord = (b.top - target.dy).abs() < (b.bottom - target.dy).abs() ? b.top : b.bottom;
794 return (aCoord - target.dy).abs().compareTo((bCoord - target.dy).abs());
795 }
796
797 static int _horizontalCompareClosestEdge(Offset target, Rect a, Rect b) {
798 // Find which edge is closest to the target for each.
799 final double aCoord = (a.left - target.dx).abs() < (a.right - target.dx).abs() ? a.left : a.right;
800 final double bCoord = (b.left - target.dx).abs() < (b.right - target.dx).abs() ? b.left : b.right;
801 return (aCoord - target.dx).abs().compareTo((bCoord - target.dx).abs());
802 }
803
804 // Sort the ones that have edges that are closest horizontally first, and if
805 // two are the same horizontal distance, pick the one that is closest
806 // vertically.
807 static Iterable<FocusNode> _sortClosestEdgesByDistancePreferHorizontal(Offset target, Iterable<FocusNode> nodes) {
808 final List<FocusNode> sorted = nodes.toList();
809 mergeSort<FocusNode>(sorted, compare: (FocusNode nodeA, FocusNode nodeB) {
810 final int horizontal = _horizontalCompareClosestEdge(target, nodeA.rect, nodeB.rect);
811 if (horizontal == 0) {
812 // If they're the same distance horizontally, pick the closest one
813 // vertically.
814 return _verticalCompare(target, nodeA.rect.center, nodeB.rect.center);
815 }
816 return horizontal;
817 });
818 return sorted;
819 }
820
821 // Sort the ones that have edges that are closest vertically first, and if
822 // two are the same vertical distance, pick the one that is closest
823 // horizontally.
824 static Iterable<FocusNode> _sortClosestEdgesByDistancePreferVertical(Offset target, Iterable<FocusNode> nodes) {
825 final List<FocusNode> sorted = nodes.toList();
826 mergeSort<FocusNode>(sorted, compare: (FocusNode nodeA, FocusNode nodeB) {
827 final int vertical = _verticalCompareClosestEdge(target, nodeA.rect, nodeB.rect);
828 if (vertical == 0) {
829 // If they're the same distance vertically, pick the closest one
830 // horizontally.
831 return _horizontalCompare(target, nodeA.rect.center, nodeB.rect.center);
832 }
833 return vertical;
834 });
835 return sorted;
836 }
837
838 // Sorts nodes from left to right horizontally, and removes nodes that are
839 // either to the right of the left side of the target node if we're going
840 // left, or to the left of the right side of the target node if we're going
841 // right.
842 //
843 // This doesn't need to take into account directionality because it is
844 // typically intending to actually go left or right, not in a reading
845 // direction.
846 Iterable<FocusNode> _sortAndFilterHorizontally(
847 TraversalDirection direction,
848 Rect target,
849 Iterable<FocusNode> nodes,
850 ) {
851 assert(direction == TraversalDirection.left || direction == TraversalDirection.right);
852 final Iterable<FocusNode> filtered;
853 switch (direction) {
854 case TraversalDirection.left:
855 filtered = nodes.where((FocusNode node) => node.rect != target && node.rect.center.dx <= target.left);
856 case TraversalDirection.right:
857 filtered = nodes.where((FocusNode node) => node.rect != target && node.rect.center.dx >= target.right);
858 case TraversalDirection.up:
859 case TraversalDirection.down:
860 throw ArgumentError('Invalid direction $direction');
861 }
862 final List<FocusNode> sorted = filtered.toList();
863 // Sort all nodes from left to right.
864 mergeSort<FocusNode>(sorted, compare: (FocusNode a, FocusNode b) => a.rect.center.dx.compareTo(b.rect.center.dx));
865 return sorted;
866 }
867
868 // Sorts nodes from top to bottom vertically, and removes nodes that are
869 // either below the top of the target node if we're going up, or above the
870 // bottom of the target node if we're going down.
871 Iterable<FocusNode> _sortAndFilterVertically(
872 TraversalDirection direction,
873 Rect target,
874 Iterable<FocusNode> nodes,
875 ) {
876 assert(direction == TraversalDirection.up || direction == TraversalDirection.down);
877 final Iterable<FocusNode> filtered;
878 switch (direction) {
879 case TraversalDirection.up:
880 filtered = nodes.where((FocusNode node) => node.rect != target && node.rect.center.dy <= target.top);
881 case TraversalDirection.down:
882 filtered = nodes.where((FocusNode node) => node.rect != target && node.rect.center.dy >= target.bottom);
883 case TraversalDirection.left:
884 case TraversalDirection.right:
885 throw ArgumentError('Invalid direction $direction');
886 }
887 final List<FocusNode> sorted = filtered.toList();
888 mergeSort<FocusNode>(sorted, compare: (FocusNode a, FocusNode b) => a.rect.center.dy.compareTo(b.rect.center.dy));
889 return sorted;
890 }
891
892 // Updates the policy data to keep the previously visited node so that we can
893 // avoid hysteresis when we change directions in navigation.
894 //
895 // Returns true if focus was requested on a previous node.
896 bool _popPolicyDataIfNeeded(TraversalDirection direction, FocusScopeNode nearestScope, FocusNode focusedChild) {
897 final _DirectionalPolicyData? policyData = _policyData[nearestScope];
898 if (policyData != null && policyData.history.isNotEmpty && policyData.history.first.direction != direction) {
899 if (policyData.history.last.node.parent == null) {
900 // If a node has been removed from the tree, then we should stop
901 // referencing it and reset the scope data so that we don't try and
902 // request focus on it. This can happen in slivers where the rendered
903 // node has been unmounted. This has the side effect that hysteresis
904 // might not be avoided when items that go off screen get unmounted.
905 invalidateScopeData(nearestScope);
906 return false;
907 }
908
909 // Returns true if successfully popped the history.
910 bool popOrInvalidate(TraversalDirection direction) {
911 final FocusNode lastNode = policyData.history.removeLast().node;
912 if (Scrollable.maybeOf(lastNode.context!) != Scrollable.maybeOf(primaryFocus!.context!)) {
913 invalidateScopeData(nearestScope);
914 return false;
915 }
916 final ScrollPositionAlignmentPolicy alignmentPolicy;
917 switch (direction) {
918 case TraversalDirection.up:
919 case TraversalDirection.left:
920 alignmentPolicy = ScrollPositionAlignmentPolicy.keepVisibleAtStart;
921 case TraversalDirection.right:
922 case TraversalDirection.down:
923 alignmentPolicy = ScrollPositionAlignmentPolicy.keepVisibleAtEnd;
924 }
925 requestFocusCallback(
926 lastNode,
927 alignmentPolicy: alignmentPolicy,
928 );
929 return true;
930 }
931
932 switch (direction) {
933 case TraversalDirection.down:
934 case TraversalDirection.up:
935 switch (policyData.history.first.direction) {
936 case TraversalDirection.left:
937 case TraversalDirection.right:
938 // Reset the policy data if we change directions.
939 invalidateScopeData(nearestScope);
940 case TraversalDirection.up:
941 case TraversalDirection.down:
942 if (popOrInvalidate(direction)) {
943 return true;
944 }
945 }
946 case TraversalDirection.left:
947 case TraversalDirection.right:
948 switch (policyData.history.first.direction) {
949 case TraversalDirection.left:
950 case TraversalDirection.right:
951 if (popOrInvalidate(direction)) {
952 return true;
953 }
954 case TraversalDirection.up:
955 case TraversalDirection.down:
956 // Reset the policy data if we change directions.
957 invalidateScopeData(nearestScope);
958 }
959 }
960 }
961 if (policyData != null && policyData.history.isEmpty) {
962 invalidateScopeData(nearestScope);
963 }
964 return false;
965 }
966
967 void _pushPolicyData(TraversalDirection direction, FocusScopeNode nearestScope, FocusNode focusedChild) {
968 final _DirectionalPolicyData? policyData = _policyData[nearestScope];
969 final _DirectionalPolicyDataEntry newEntry = _DirectionalPolicyDataEntry(node: focusedChild, direction: direction);
970 if (policyData != null) {
971 policyData.history.add(newEntry);
972 } else {
973 _policyData[nearestScope] = _DirectionalPolicyData(history: <_DirectionalPolicyDataEntry>[newEntry]);
974 }
975 }
976
977 /// Focuses the next widget in the given [direction] in the [FocusScope] that
978 /// contains the [currentNode].
979 ///
980 /// This determines what the next node to receive focus in the given
981 /// [direction] will be by inspecting the node tree, and then calling
982 /// [FocusNode.requestFocus] on it.
983 ///
984 /// Returns true if it successfully found a node and requested focus.
985 ///
986 /// Maintains a stack of previous locations that have been visited on the
987 /// policy data for the affected [FocusScopeNode]. If the previous direction
988 /// was the opposite of the current direction, then the this policy will
989 /// request focus on the previously focused node. Change to another direction
990 /// other than the current one or its opposite will clear the stack.
991 ///
992 /// If this function returns true when called by a subclass, then the subclass
993 /// should return true and not request focus from any node.
994 @mustCallSuper
995 @override
996 bool inDirection(FocusNode currentNode, TraversalDirection direction) {
997 final FocusScopeNode nearestScope = currentNode.nearestScope!;
998 final FocusNode? focusedChild = nearestScope.focusedChild;
999 if (focusedChild == null) {
1000 final FocusNode firstFocus = findFirstFocusInDirection(currentNode, direction) ?? currentNode;
1001 switch (direction) {
1002 case TraversalDirection.up:
1003 case TraversalDirection.left:
1004 requestFocusCallback(
1005 firstFocus,
1006 alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart,
1007 );
1008 case TraversalDirection.right:
1009 case TraversalDirection.down:
1010 requestFocusCallback(
1011 firstFocus,
1012 alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd,
1013 );
1014 }
1015 return true;
1016 }
1017 if (_popPolicyDataIfNeeded(direction, nearestScope, focusedChild)) {
1018 return true;
1019 }
1020 FocusNode? found;
1021 final ScrollableState? focusedScrollable = Scrollable.maybeOf(focusedChild.context!);
1022 switch (direction) {
1023 case TraversalDirection.down:
1024 case TraversalDirection.up:
1025 Iterable<FocusNode> eligibleNodes = _sortAndFilterVertically(direction, focusedChild.rect, nearestScope.traversalDescendants);
1026 if (eligibleNodes.isEmpty) {
1027 break;
1028 }
1029 if (focusedScrollable != null && !focusedScrollable.position.atEdge) {
1030 final Iterable<FocusNode> filteredEligibleNodes = eligibleNodes.where((FocusNode node) => Scrollable.maybeOf(node.context!) == focusedScrollable);
1031 if (filteredEligibleNodes.isNotEmpty) {
1032 eligibleNodes = filteredEligibleNodes;
1033 }
1034 }
1035 if (direction == TraversalDirection.up) {
1036 eligibleNodes = eligibleNodes.toList().reversed;
1037 }
1038 // Find any nodes that intersect the band of the focused child.
1039 final Rect band = Rect.fromLTRB(focusedChild.rect.left, -double.infinity, focusedChild.rect.right, double.infinity);
1040 final Iterable<FocusNode> inBand = eligibleNodes.where((FocusNode node) => !node.rect.intersect(band).isEmpty);
1041 if (inBand.isNotEmpty) {
1042 found = _sortByDistancePreferVertical(focusedChild.rect.center, inBand).first;
1043 break;
1044 }
1045 // Only out-of-band targets are eligible, so pick the one that is
1046 // closest to the center line horizontally, and if any are the same
1047 // distance horizontally, pick the closest one of those vertically.
1048 found = _sortClosestEdgesByDistancePreferHorizontal(focusedChild.rect.center, eligibleNodes).first;
1049 case TraversalDirection.right:
1050 case TraversalDirection.left:
1051 Iterable<FocusNode> eligibleNodes = _sortAndFilterHorizontally(direction, focusedChild.rect, nearestScope.traversalDescendants);
1052 if (eligibleNodes.isEmpty) {
1053 break;
1054 }
1055 if (focusedScrollable != null && !focusedScrollable.position.atEdge) {
1056 final Iterable<FocusNode> filteredEligibleNodes = eligibleNodes.where((FocusNode node) => Scrollable.maybeOf(node.context!) == focusedScrollable);
1057 if (filteredEligibleNodes.isNotEmpty) {
1058 eligibleNodes = filteredEligibleNodes;
1059 }
1060 }
1061 if (direction == TraversalDirection.left) {
1062 eligibleNodes = eligibleNodes.toList().reversed;
1063 }
1064 // Find any nodes that intersect the band of the focused child.
1065 final Rect band = Rect.fromLTRB(-double.infinity, focusedChild.rect.top, double.infinity, focusedChild.rect.bottom);
1066 final Iterable<FocusNode> inBand = eligibleNodes.where((FocusNode node) => !node.rect.intersect(band).isEmpty);
1067 if (inBand.isNotEmpty) {
1068 found = _sortByDistancePreferHorizontal(focusedChild.rect.center, inBand).first;
1069 break;
1070 }
1071 // Only out-of-band targets are eligible, so pick the one that is
1072 // closest to the center line vertically, and if any are the same
1073 // distance vertically, pick the closest one of those horizontally.
1074 found = _sortClosestEdgesByDistancePreferVertical(focusedChild.rect.center, eligibleNodes).first;
1075 }
1076 if (found != null) {
1077 _pushPolicyData(direction, nearestScope, focusedChild);
1078 switch (direction) {
1079 case TraversalDirection.up:
1080 case TraversalDirection.left:
1081 requestFocusCallback(
1082 found,
1083 alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtStart,
1084 );
1085 case TraversalDirection.down:
1086 case TraversalDirection.right:
1087 requestFocusCallback(
1088 found,
1089 alignmentPolicy: ScrollPositionAlignmentPolicy.keepVisibleAtEnd,
1090 );
1091 }
1092 return true;
1093 }
1094 return false;
1095 }
1096}
1097
1098/// A [FocusTraversalPolicy] that traverses the focus order in widget hierarchy
1099/// order.
1100///
1101/// This policy is used when the order desired is the order in which widgets are
1102/// created in the widget hierarchy.
1103///
1104/// See also:
1105///
1106/// * [FocusNode], for a description of the focus system.
1107/// * [FocusTraversalGroup], a widget that groups together and imposes a
1108/// traversal policy on the [Focus] nodes below it in the widget hierarchy.
1109/// * [ReadingOrderTraversalPolicy], a policy that describes the order as the
1110/// natural "reading order" for the current [Directionality].
1111/// * [DirectionalFocusTraversalPolicyMixin] a mixin class that implements
1112/// focus traversal in a direction.
1113/// * [OrderedTraversalPolicy], a policy that describes the order
1114/// explicitly using [FocusTraversalOrder] widgets.
1115class WidgetOrderTraversalPolicy extends FocusTraversalPolicy with DirectionalFocusTraversalPolicyMixin {
1116 /// Constructs a traversal policy that orders widgets for keyboard traversal
1117 /// based on the widget hierarchy order.
1118 ///
1119 /// {@macro flutter.widgets.FocusTraversalPolicy.requestFocusCallback}
1120 WidgetOrderTraversalPolicy({super.requestFocusCallback});
1121 @override
1122 Iterable<FocusNode> sortDescendants(Iterable<FocusNode> descendants, FocusNode currentNode) => descendants;
1123}
1124
1125// This class exists mainly for efficiency reasons: the rect is copied out of
1126// the node, because it will be accessed many times in the reading order
1127// algorithm, and the FocusNode.rect accessor does coordinate transformation. If
1128// not for this optimization, it could just be removed, and the node used
1129// directly.
1130//
1131// It's also a convenient place to put some utility functions having to do with
1132// the sort data.
1133class _ReadingOrderSortData with Diagnosticable {
1134 _ReadingOrderSortData(this.node)
1135 : rect = node.rect,
1136 directionality = _findDirectionality(node.context!);
1137
1138 final TextDirection? directionality;
1139 final Rect rect;
1140 final FocusNode node;
1141
1142 // Find the directionality in force for a build context without creating a
1143 // dependency.
1144 static TextDirection? _findDirectionality(BuildContext context) {
1145 return context.getInheritedWidgetOfExactType<Directionality>()?.textDirection;
1146 }
1147
1148 /// Finds the common Directional ancestor of an entire list of groups.
1149 static TextDirection? commonDirectionalityOf(List<_ReadingOrderSortData> list) {
1150 final Iterable<Set<Directionality>> allAncestors = list.map<Set<Directionality>>((_ReadingOrderSortData member) => member.directionalAncestors.toSet());
1151 Set<Directionality>? common;
1152 for (final Set<Directionality> ancestorSet in allAncestors) {
1153 common ??= ancestorSet;
1154 common = common.intersection(ancestorSet);
1155 }
1156 if (common!.isEmpty) {
1157 // If there is no common ancestor, then arbitrarily pick the
1158 // directionality of the first group, which is the equivalent of the
1159 // "first strongly typed" item in a bidirectional algorithm.
1160 return list.first.directionality;
1161 }
1162 // Find the closest common ancestor. The memberAncestors list contains the
1163 // ancestors for all members, but the first member's ancestry was
1164 // added in order from nearest to furthest, so we can still use that
1165 // to determine the closest one.
1166 return list.first.directionalAncestors.firstWhere(common.contains).textDirection;
1167 }
1168
1169 static void sortWithDirectionality(List<_ReadingOrderSortData> list, TextDirection directionality) {
1170 mergeSort<_ReadingOrderSortData>(list, compare: (_ReadingOrderSortData a, _ReadingOrderSortData b) {
1171 switch (directionality) {
1172 case TextDirection.ltr:
1173 return a.rect.left.compareTo(b.rect.left);
1174 case TextDirection.rtl:
1175 return b.rect.right.compareTo(a.rect.right);
1176 }
1177 });
1178 }
1179
1180 /// Returns the list of Directionality ancestors, in order from nearest to
1181 /// furthest.
1182 Iterable<Directionality> get directionalAncestors {
1183 List<Directionality> getDirectionalityAncestors(BuildContext context) {
1184 final List<Directionality> result = <Directionality>[];
1185 InheritedElement? directionalityElement = context.getElementForInheritedWidgetOfExactType<Directionality>();
1186 while (directionalityElement != null) {
1187 result.add(directionalityElement.widget as Directionality);
1188 directionalityElement = _getAncestor(directionalityElement)?.getElementForInheritedWidgetOfExactType<Directionality>();
1189 }
1190 return result;
1191 }
1192
1193 _directionalAncestors ??= getDirectionalityAncestors(node.context!);
1194 return _directionalAncestors!;
1195 }
1196
1197 List<Directionality>? _directionalAncestors;
1198
1199 @override
1200 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
1201 super.debugFillProperties(properties);
1202 properties.add(DiagnosticsProperty<TextDirection>('directionality', directionality));
1203 properties.add(StringProperty('name', node.debugLabel, defaultValue: null));
1204 properties.add(DiagnosticsProperty<Rect>('rect', rect));
1205 }
1206}
1207
1208// A class for containing group data while sorting in reading order while taking
1209// into account the ambient directionality.
1210class _ReadingOrderDirectionalGroupData with Diagnosticable {
1211 _ReadingOrderDirectionalGroupData(this.members);
1212
1213 final List<_ReadingOrderSortData> members;
1214
1215 TextDirection? get directionality => members.first.directionality;
1216
1217 Rect? _rect;
1218 Rect get rect {
1219 if (_rect == null) {
1220 for (final Rect rect in members.map<Rect>((_ReadingOrderSortData data) => data.rect)) {
1221 _rect ??= rect;
1222 _rect = _rect!.expandToInclude(rect);
1223 }
1224 }
1225 return _rect!;
1226 }
1227
1228 List<Directionality> get memberAncestors {
1229 if (_memberAncestors == null) {
1230 _memberAncestors = <Directionality>[];
1231 for (final _ReadingOrderSortData member in members) {
1232 _memberAncestors!.addAll(member.directionalAncestors);
1233 }
1234 }
1235 return _memberAncestors!;
1236 }
1237
1238 List<Directionality>? _memberAncestors;
1239
1240 static void sortWithDirectionality(List<_ReadingOrderDirectionalGroupData> list, TextDirection directionality) {
1241 mergeSort<_ReadingOrderDirectionalGroupData>(list, compare: (_ReadingOrderDirectionalGroupData a, _ReadingOrderDirectionalGroupData b) {
1242 switch (directionality) {
1243 case TextDirection.ltr:
1244 return a.rect.left.compareTo(b.rect.left);
1245 case TextDirection.rtl:
1246 return b.rect.right.compareTo(a.rect.right);
1247 }
1248 });
1249 }
1250
1251 @override
1252 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
1253 super.debugFillProperties(properties);
1254 properties.add(DiagnosticsProperty<TextDirection>('directionality', directionality));
1255 properties.add(DiagnosticsProperty<Rect>('rect', rect));
1256 properties.add(IterableProperty<String>('members', members.map<String>((_ReadingOrderSortData member) {
1257 return '"${member.node.debugLabel}"(${member.rect})';
1258 })));
1259 }
1260}
1261
1262/// Traverses the focus order in "reading order".
1263///
1264/// By default, reading order traversal goes in the reading direction, and then
1265/// down, using this algorithm:
1266///
1267/// 1. Find the node rectangle that has the highest `top` on the screen.
1268/// 2. Find any other nodes that intersect the infinite horizontal band defined
1269/// by the highest rectangle's top and bottom edges.
1270/// 3. Pick the closest to the beginning of the reading order from among the
1271/// nodes discovered above.
1272///
1273/// It uses the ambient [Directionality] in the context for the enclosing
1274/// [FocusTraversalGroup] to determine which direction is "reading order".
1275///
1276/// See also:
1277///
1278/// * [FocusNode], for a description of the focus system.
1279/// * [FocusTraversalGroup], a widget that groups together and imposes a
1280/// traversal policy on the [Focus] nodes below it in the widget hierarchy.
1281/// * [WidgetOrderTraversalPolicy], a policy that relies on the widget
1282/// creation order to describe the order of traversal.
1283/// * [DirectionalFocusTraversalPolicyMixin] a mixin class that implements
1284/// focus traversal in a direction.
1285/// * [OrderedTraversalPolicy], a policy that describes the order
1286/// explicitly using [FocusTraversalOrder] widgets.
1287class ReadingOrderTraversalPolicy extends FocusTraversalPolicy with DirectionalFocusTraversalPolicyMixin {
1288 /// Constructs a traversal policy that orders the widgets in "reading order".
1289 ///
1290 /// {@macro flutter.widgets.FocusTraversalPolicy.requestFocusCallback}
1291 ReadingOrderTraversalPolicy({super.requestFocusCallback});
1292
1293 // Collects the given candidates into groups by directionality. The candidates
1294 // have already been sorted as if they all had the directionality of the
1295 // nearest Directionality ancestor.
1296 List<_ReadingOrderDirectionalGroupData> _collectDirectionalityGroups(Iterable<_ReadingOrderSortData> candidates) {
1297 TextDirection? currentDirection = candidates.first.directionality;
1298 List<_ReadingOrderSortData> currentGroup = <_ReadingOrderSortData>[];
1299 final List<_ReadingOrderDirectionalGroupData> result = <_ReadingOrderDirectionalGroupData>[];
1300 // Split candidates into runs of the same directionality.
1301 for (final _ReadingOrderSortData candidate in candidates) {
1302 if (candidate.directionality == currentDirection) {
1303 currentGroup.add(candidate);
1304 continue;
1305 }
1306 currentDirection = candidate.directionality;
1307 result.add(_ReadingOrderDirectionalGroupData(currentGroup));
1308 currentGroup = <_ReadingOrderSortData>[candidate];
1309 }
1310 if (currentGroup.isNotEmpty) {
1311 result.add(_ReadingOrderDirectionalGroupData(currentGroup));
1312 }
1313 // Sort each group separately. Each group has the same directionality.
1314 for (final _ReadingOrderDirectionalGroupData bandGroup in result) {
1315 if (bandGroup.members.length == 1) {
1316 continue; // No need to sort one node.
1317 }
1318 _ReadingOrderSortData.sortWithDirectionality(bandGroup.members, bandGroup.directionality!);
1319 }
1320 return result;
1321 }
1322
1323 _ReadingOrderSortData _pickNext(List<_ReadingOrderSortData> candidates) {
1324 // Find the topmost node by sorting on the top of the rectangles.
1325 mergeSort<_ReadingOrderSortData>(candidates, compare: (_ReadingOrderSortData a, _ReadingOrderSortData b) => a.rect.top.compareTo(b.rect.top));
1326 final _ReadingOrderSortData topmost = candidates.first;
1327
1328 // Find the candidates that are in the same horizontal band as the current one.
1329 List<_ReadingOrderSortData> inBand(_ReadingOrderSortData current, Iterable<_ReadingOrderSortData> candidates) {
1330 final Rect band = Rect.fromLTRB(double.negativeInfinity, current.rect.top, double.infinity, current.rect.bottom);
1331 return candidates.where((_ReadingOrderSortData item) {
1332 return !item.rect.intersect(band).isEmpty;
1333 }).toList();
1334 }
1335
1336 final List<_ReadingOrderSortData> inBandOfTop = inBand(topmost, candidates);
1337 // It has to have at least topmost in it if the topmost is not degenerate.
1338 assert(topmost.rect.isEmpty || inBandOfTop.isNotEmpty);
1339
1340 // The topmost rect is in a band by itself, so just return that one.
1341 if (inBandOfTop.length <= 1) {
1342 return topmost;
1343 }
1344
1345 // Now that we know there are others in the same band as the topmost, then pick
1346 // the one at the beginning, depending on the text direction in force.
1347
1348 // Find out the directionality of the nearest common Directionality
1349 // ancestor for all nodes. This provides a base directionality to use for
1350 // the ordering of the groups.
1351 final TextDirection? nearestCommonDirectionality = _ReadingOrderSortData.commonDirectionalityOf(inBandOfTop);
1352
1353 // Do an initial common-directionality-based sort to get consistent geometric
1354 // ordering for grouping into directionality groups. It has to use the
1355 // common directionality to be able to group into sane groups for the
1356 // given directionality, since rectangles can overlap and give different
1357 // results for different directionalities.
1358 _ReadingOrderSortData.sortWithDirectionality(inBandOfTop, nearestCommonDirectionality!);
1359
1360 // Collect the top band into internally sorted groups with shared directionality.
1361 final List<_ReadingOrderDirectionalGroupData> bandGroups = _collectDirectionalityGroups(inBandOfTop);
1362 if (bandGroups.length == 1) {
1363 // There's only one directionality group, so just send back the first
1364 // one in that group, since it's already sorted.
1365 return bandGroups.first.members.first;
1366 }
1367
1368 // Sort the groups based on the common directionality and bounding boxes.
1369 _ReadingOrderDirectionalGroupData.sortWithDirectionality(bandGroups, nearestCommonDirectionality);
1370 return bandGroups.first.members.first;
1371 }
1372
1373 // Sorts the list of nodes based on their geometry into the desired reading
1374 // order based on the directionality of the context for each node.
1375 @override
1376 Iterable<FocusNode> sortDescendants(Iterable<FocusNode> descendants, FocusNode currentNode) {
1377 if (descendants.length <= 1) {
1378 return descendants;
1379 }
1380
1381 final List<_ReadingOrderSortData> data = <_ReadingOrderSortData>[
1382 for (final FocusNode node in descendants) _ReadingOrderSortData(node),
1383 ];
1384
1385 final List<FocusNode> sortedList = <FocusNode>[];
1386 final List<_ReadingOrderSortData> unplaced = data;
1387
1388 // Pick the initial widget as the one that is at the beginning of the band
1389 // of the topmost, or the topmost, if there are no others in its band.
1390 _ReadingOrderSortData current = _pickNext(unplaced);
1391 sortedList.add(current.node);
1392 unplaced.remove(current);
1393
1394 // Go through each node, picking the next one after eliminating the previous
1395 // one, since removing the previously picked node will expose a new band in
1396 // which to choose candidates.
1397 while (unplaced.isNotEmpty) {
1398 final _ReadingOrderSortData next = _pickNext(unplaced);
1399 current = next;
1400 sortedList.add(current.node);
1401 unplaced.remove(current);
1402 }
1403 return sortedList;
1404 }
1405}
1406
1407/// Base class for all sort orders for [OrderedTraversalPolicy] traversal.
1408///
1409/// {@template flutter.widgets.FocusOrder.comparable}
1410/// Only orders of the same type are comparable. If a set of widgets in the same
1411/// [FocusTraversalGroup] contains orders that are not comparable with each
1412/// other, it will assert, since the ordering between such keys is undefined. To
1413/// avoid collisions, use a [FocusTraversalGroup] to group similarly ordered
1414/// widgets together.
1415///
1416/// When overriding, [FocusOrder.doCompare] must be overridden instead of
1417/// [FocusOrder.compareTo], which calls [FocusOrder.doCompare] to do the actual
1418/// comparison.
1419/// {@endtemplate}
1420///
1421/// See also:
1422///
1423/// * [FocusTraversalGroup], a widget that groups together and imposes a
1424/// traversal policy on the [Focus] nodes below it in the widget hierarchy.
1425/// * [FocusTraversalOrder], a widget that assigns an order to a widget subtree
1426/// for the [OrderedTraversalPolicy] to use.
1427/// * [NumericFocusOrder], for a focus order that describes its order with a
1428/// `double`.
1429/// * [LexicalFocusOrder], a focus order that assigns a string-based lexical
1430/// traversal order to a [FocusTraversalOrder] widget.
1431@immutable
1432abstract class FocusOrder with Diagnosticable implements Comparable<FocusOrder> {
1433 /// Abstract const constructor. This constructor enables subclasses to provide
1434 /// const constructors so that they can be used in const expressions.
1435 const FocusOrder();
1436
1437 /// Compares this object to another [Comparable].
1438 ///
1439 /// When overriding [FocusOrder], implement [doCompare] instead of this
1440 /// function to do the actual comparison.
1441 ///
1442 /// Returns a value like a [Comparator] when comparing `this` to [other].
1443 /// That is, it returns a negative integer if `this` is ordered before [other],
1444 /// a positive integer if `this` is ordered after [other],
1445 /// and zero if `this` and [other] are ordered together.
1446 ///
1447 /// The [other] argument must be a value that is comparable to this object.
1448 @override
1449 @nonVirtual
1450 int compareTo(FocusOrder other) {
1451 assert(
1452 runtimeType == other.runtimeType,
1453 "The sorting algorithm must not compare incomparable keys, since they don't "
1454 'know how to order themselves relative to each other. Comparing $this with $other',
1455 );
1456 return doCompare(other);
1457 }
1458
1459 /// The subclass implementation called by [compareTo] to compare orders.
1460 ///
1461 /// The argument is guaranteed to be of the same [runtimeType] as this object.
1462 ///
1463 /// The method should return a negative number if this object comes earlier in
1464 /// the sort order than the `other` argument; and a positive number if it
1465 /// comes later in the sort order than `other`. Returning zero causes the
1466 /// system to fall back to the secondary sort order defined by
1467 /// [OrderedTraversalPolicy.secondary]
1468 @protected
1469 int doCompare(covariant FocusOrder other);
1470}
1471
1472/// Can be given to a [FocusTraversalOrder] widget to assign a numerical order
1473/// to a widget subtree that is using a [OrderedTraversalPolicy] to define the
1474/// order in which widgets should be traversed with the keyboard.
1475///
1476/// {@macro flutter.widgets.FocusOrder.comparable}
1477///
1478/// See also:
1479///
1480/// * [FocusTraversalOrder], a widget that assigns an order to a widget subtree
1481/// for the [OrderedTraversalPolicy] to use.
1482class NumericFocusOrder extends FocusOrder {
1483 /// Creates an object that describes a focus traversal order numerically.
1484 const NumericFocusOrder(this.order);
1485
1486 /// The numerical order to assign to the widget subtree using
1487 /// [FocusTraversalOrder].
1488 ///
1489 /// Determines the placement of this widget in a sequence of widgets that defines
1490 /// the order in which this node is traversed by the focus policy.
1491 ///
1492 /// Lower values will be traversed first.
1493 final double order;
1494
1495 @override
1496 int doCompare(NumericFocusOrder other) => order.compareTo(other.order);
1497
1498 @override
1499 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
1500 super.debugFillProperties(properties);
1501 properties.add(DoubleProperty('order', order));
1502 }
1503}
1504
1505/// Can be given to a [FocusTraversalOrder] widget to use a String to assign a
1506/// lexical order to a widget subtree that is using a
1507/// [OrderedTraversalPolicy] to define the order in which widgets should be
1508/// traversed with the keyboard.
1509///
1510/// This sorts strings using Dart's default string comparison, which is not
1511/// locale-specific.
1512///
1513/// {@macro flutter.widgets.FocusOrder.comparable}
1514///
1515/// See also:
1516///
1517/// * [FocusTraversalOrder], a widget that assigns an order to a widget subtree
1518/// for the [OrderedTraversalPolicy] to use.
1519class LexicalFocusOrder extends FocusOrder {
1520 /// Creates an object that describes a focus traversal order lexically.
1521 const LexicalFocusOrder(this.order);
1522
1523 /// The String that defines the lexical order to assign to the widget subtree
1524 /// using [FocusTraversalOrder].
1525 ///
1526 /// Determines the placement of this widget in a sequence of widgets that defines
1527 /// the order in which this node is traversed by the focus policy.
1528 ///
1529 /// Lower lexical values will be traversed first (e.g. 'a' comes before 'z').
1530 final String order;
1531
1532 @override
1533 int doCompare(LexicalFocusOrder other) => order.compareTo(other.order);
1534
1535 @override
1536 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
1537 super.debugFillProperties(properties);
1538 properties.add(StringProperty('order', order));
1539 }
1540}
1541
1542// Used to help sort the focus nodes in an OrderedFocusTraversalPolicy.
1543class _OrderedFocusInfo {
1544 const _OrderedFocusInfo({required this.node, required this.order});
1545
1546 final FocusNode node;
1547 final FocusOrder order;
1548}
1549
1550/// A [FocusTraversalPolicy] that orders nodes by an explicit order that resides
1551/// in the nearest [FocusTraversalOrder] widget ancestor.
1552///
1553/// {@macro flutter.widgets.FocusOrder.comparable}
1554///
1555/// {@tool dartpad}
1556/// This sample shows how to assign a traversal order to a widget. In the
1557/// example, the focus order goes from bottom right (the "One" button) to top
1558/// left (the "Six" button).
1559///
1560/// ** See code in examples/api/lib/widgets/focus_traversal/ordered_traversal_policy.0.dart **
1561/// {@end-tool}
1562///
1563/// See also:
1564///
1565/// * [FocusTraversalGroup], a widget that groups together and imposes a
1566/// traversal policy on the [Focus] nodes below it in the widget hierarchy.
1567/// * [WidgetOrderTraversalPolicy], a policy that relies on the widget
1568/// creation order to describe the order of traversal.
1569/// * [ReadingOrderTraversalPolicy], a policy that describes the order as the
1570/// natural "reading order" for the current [Directionality].
1571/// * [NumericFocusOrder], a focus order that assigns a numeric traversal order
1572/// to a [FocusTraversalOrder] widget.
1573/// * [LexicalFocusOrder], a focus order that assigns a string-based lexical
1574/// traversal order to a [FocusTraversalOrder] widget.
1575/// * [FocusOrder], an abstract base class for all types of focus traversal
1576/// orderings.
1577class OrderedTraversalPolicy extends FocusTraversalPolicy with DirectionalFocusTraversalPolicyMixin {
1578 /// Constructs a traversal policy that orders widgets for keyboard traversal
1579 /// based on an explicit order.
1580 ///
1581 /// If [secondary] is null, it will default to [ReadingOrderTraversalPolicy].
1582 OrderedTraversalPolicy({this.secondary, super.requestFocusCallback});
1583
1584 /// This is the policy that is used when a node doesn't have an order
1585 /// assigned, or when multiple nodes have orders which are identical.
1586 ///
1587 /// If not set, this defaults to [ReadingOrderTraversalPolicy].
1588 ///
1589 /// This policy determines the secondary sorting order of nodes which evaluate
1590 /// as having an identical order (including those with no order specified).
1591 ///
1592 /// Nodes with no order specified will be sorted after nodes with an explicit
1593 /// order.
1594 final FocusTraversalPolicy? secondary;
1595
1596 @override
1597 Iterable<FocusNode> sortDescendants(Iterable<FocusNode> descendants, FocusNode currentNode) {
1598 final FocusTraversalPolicy secondaryPolicy = secondary ?? ReadingOrderTraversalPolicy();
1599 final Iterable<FocusNode> sortedDescendants = secondaryPolicy.sortDescendants(descendants, currentNode);
1600 final List<FocusNode> unordered = <FocusNode>[];
1601 final List<_OrderedFocusInfo> ordered = <_OrderedFocusInfo>[];
1602 for (final FocusNode node in sortedDescendants) {
1603 final FocusOrder? order = FocusTraversalOrder.maybeOf(node.context!);
1604 if (order != null) {
1605 ordered.add(_OrderedFocusInfo(node: node, order: order));
1606 } else {
1607 unordered.add(node);
1608 }
1609 }
1610 mergeSort<_OrderedFocusInfo>(ordered, compare: (_OrderedFocusInfo a, _OrderedFocusInfo b) {
1611 assert(
1612 a.order.runtimeType == b.order.runtimeType,
1613 'When sorting nodes for determining focus order, the order (${a.order}) of '
1614 "node ${a.node}, isn't the same type as the order (${b.order}) of ${b.node}. "
1615 "Incompatible order types can't be compared. Use a FocusTraversalGroup to group "
1616 'similar orders together.',
1617 );
1618 return a.order.compareTo(b.order);
1619 });
1620 return ordered.map<FocusNode>((_OrderedFocusInfo info) => info.node).followedBy(unordered);
1621 }
1622}
1623
1624/// An inherited widget that describes the order in which its child subtree
1625/// should be traversed.
1626///
1627/// {@macro flutter.widgets.FocusOrder.comparable}
1628///
1629/// The order for a widget is determined by the [FocusOrder] returned by
1630/// [FocusTraversalOrder.of] for a particular context.
1631class FocusTraversalOrder extends InheritedWidget {
1632 /// Creates an inherited widget used to describe the focus order of
1633 /// the [child] subtree.
1634 const FocusTraversalOrder({super.key, required this.order, required super.child});
1635
1636 /// The order for the widget descendants of this [FocusTraversalOrder].
1637 final FocusOrder order;
1638
1639 /// Finds the [FocusOrder] in the nearest ancestor [FocusTraversalOrder] widget.
1640 ///
1641 /// It does not create a rebuild dependency because changing the traversal
1642 /// order doesn't change the widget tree, so nothing needs to be rebuilt as a
1643 /// result of an order change.
1644 ///
1645 /// If no [FocusTraversalOrder] ancestor exists, or the order is null, this
1646 /// will assert in debug mode, and throw an exception in release mode.
1647 static FocusOrder of(BuildContext context) {
1648 final FocusTraversalOrder? marker = context.getInheritedWidgetOfExactType<FocusTraversalOrder>();
1649 assert(() {
1650 if (marker == null) {
1651 throw FlutterError(
1652 'FocusTraversalOrder.of() was called with a context that '
1653 'does not contain a FocusTraversalOrder widget. No TraversalOrder widget '
1654 'ancestor could be found starting from the context that was passed to '
1655 'FocusTraversalOrder.of().\n'
1656 'The context used was:\n'
1657 ' $context',
1658 );
1659 }
1660 return true;
1661 }());
1662 return marker!.order;
1663 }
1664
1665 /// Finds the [FocusOrder] in the nearest ancestor [FocusTraversalOrder] widget.
1666 ///
1667 /// It does not create a rebuild dependency because changing the traversal
1668 /// order doesn't change the widget tree, so nothing needs to be rebuilt as a
1669 /// result of an order change.
1670 ///
1671 /// If no [FocusTraversalOrder] ancestor exists, or the order is null, returns null.
1672 static FocusOrder? maybeOf(BuildContext context) {
1673 final FocusTraversalOrder? marker = context.getInheritedWidgetOfExactType<FocusTraversalOrder>();
1674 return marker?.order;
1675 }
1676
1677 // Since the order of traversal doesn't affect display of anything, we don't
1678 // need to force a rebuild of anything that depends upon it.
1679 @override
1680 bool updateShouldNotify(InheritedWidget oldWidget) => false;
1681
1682 @override
1683 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
1684 super.debugFillProperties(properties);
1685 properties.add(DiagnosticsProperty<FocusOrder>('order', order));
1686 }
1687}
1688
1689/// A widget that describes the inherited focus policy for focus traversal for
1690/// its descendants, grouping them into a separate traversal group.
1691///
1692/// A traversal group is treated as one entity when sorted by the traversal
1693/// algorithm, so it can be used to segregate different parts of the widget tree
1694/// that need to be sorted using different algorithms and/or sort orders when
1695/// using an [OrderedTraversalPolicy].
1696///
1697/// Within the group, it will use the given [policy] to order the elements. The
1698/// group itself will be ordered using the parent group's policy.
1699///
1700/// By default, traverses in reading order using [ReadingOrderTraversalPolicy].
1701///
1702/// To prevent the members of the group from being focused, set the
1703/// [descendantsAreFocusable] attribute to false.
1704///
1705/// {@tool dartpad}
1706/// This sample shows three rows of buttons, each grouped by a
1707/// [FocusTraversalGroup], each with different traversal order policies. Use tab
1708/// traversal to see the order they are traversed in. The first row follows a
1709/// numerical order, the second follows a lexical order (ordered to traverse
1710/// right to left), and the third ignores the numerical order assigned to it and
1711/// traverses in widget order.
1712///
1713/// ** See code in examples/api/lib/widgets/focus_traversal/focus_traversal_group.0.dart **
1714/// {@end-tool}
1715///
1716/// See also:
1717///
1718/// * [FocusNode], for a description of the focus system.
1719/// * [WidgetOrderTraversalPolicy], a policy that relies on the widget
1720/// creation order to describe the order of traversal.
1721/// * [ReadingOrderTraversalPolicy], a policy that describes the order as the
1722/// natural "reading order" for the current [Directionality].
1723/// * [DirectionalFocusTraversalPolicyMixin] a mixin class that implements
1724/// focus traversal in a direction.
1725class FocusTraversalGroup extends StatefulWidget {
1726 /// Creates a [FocusTraversalGroup] object.
1727 FocusTraversalGroup({
1728 super.key,
1729 FocusTraversalPolicy? policy,
1730 this.descendantsAreFocusable = true,
1731 this.descendantsAreTraversable = true,
1732 required this.child,
1733 }) : policy = policy ?? ReadingOrderTraversalPolicy();
1734
1735 /// The policy used to move the focus from one focus node to another when
1736 /// traversing them using a keyboard.
1737 ///
1738 /// If not specified, traverses in reading order using
1739 /// [ReadingOrderTraversalPolicy].
1740 ///
1741 /// See also:
1742 ///
1743 /// * [FocusTraversalPolicy] for the API used to impose traversal order
1744 /// policy.
1745 /// * [WidgetOrderTraversalPolicy] for a traversal policy that traverses
1746 /// nodes in the order they are added to the widget tree.
1747 /// * [ReadingOrderTraversalPolicy] for a traversal policy that traverses
1748 /// nodes in the reading order defined in the widget tree, and then top to
1749 /// bottom.
1750 final FocusTraversalPolicy policy;
1751
1752 /// {@macro flutter.widgets.Focus.descendantsAreFocusable}
1753 final bool descendantsAreFocusable;
1754
1755 /// {@macro flutter.widgets.Focus.descendantsAreTraversable}
1756 final bool descendantsAreTraversable;
1757
1758 /// The child widget of this [FocusTraversalGroup].
1759 ///
1760 /// {@macro flutter.widgets.ProxyWidget.child}
1761 final Widget child;
1762
1763 /// Returns the [FocusTraversalPolicy] that applies to the nearest ancestor of
1764 /// the given [FocusNode].
1765 ///
1766 /// Will return null if no [FocusTraversalPolicy] ancestor applies to the
1767 /// given [FocusNode].
1768 ///
1769 /// The [FocusTraversalPolicy] is set by introducing a [FocusTraversalGroup]
1770 /// into the widget tree, which will associate a policy with the focus tree
1771 /// under the nearest ancestor [Focus] widget.
1772 ///
1773 /// This function differs from [maybeOf] in that it takes a [FocusNode] and
1774 /// only traverses the focus tree to determine the policy in effect. Unlike
1775 /// this function, the [maybeOf] function takes a [BuildContext] and first
1776 /// walks up the widget tree to find the nearest ancestor [Focus] or
1777 /// [FocusScope] widget, and then calls this function with the focus node
1778 /// associated with that widget to determine the policy in effect.
1779 static FocusTraversalPolicy? maybeOfNode(FocusNode node) {
1780 return _getGroupNode(node)?.policy;
1781 }
1782
1783 static _FocusTraversalGroupNode? _getGroupNode(FocusNode node) {
1784 while (node.parent != null) {
1785 if (node.context == null) {
1786 return null;
1787 }
1788 if (node is _FocusTraversalGroupNode) {
1789 return node;
1790 }
1791 node = node.parent!;
1792 }
1793 return null;
1794 }
1795
1796 /// Returns the [FocusTraversalPolicy] that applies to the [FocusNode] of the
1797 /// nearest ancestor [Focus] widget, given a [BuildContext].
1798 ///
1799 /// Will throw a [FlutterError] in debug mode, and throw a null check
1800 /// exception in release mode, if no [Focus] ancestor is found, or if no
1801 /// [FocusTraversalPolicy] applies to the associated [FocusNode].
1802 ///
1803 /// {@template flutter.widgets.focus_traversal.FocusTraversalGroup.of}
1804 /// This function looks up the nearest ancestor [Focus] (or [FocusScope])
1805 /// widget, and uses its [FocusNode] (or [FocusScopeNode]) to walk up the
1806 /// focus tree to find the applicable [FocusTraversalPolicy] for that node.
1807 ///
1808 /// Calling this function does not create a rebuild dependency because
1809 /// changing the traversal order doesn't change the widget tree, so nothing
1810 /// needs to be rebuilt as a result of an order change.
1811 ///
1812 /// The [FocusTraversalPolicy] is set by introducing a [FocusTraversalGroup]
1813 /// into the widget tree, which will associate a policy with the focus tree
1814 /// under the nearest ancestor [Focus] widget.
1815 /// {@endtemplate}
1816 ///
1817 /// See also:
1818 ///
1819 /// * [maybeOf] for a similar function that will return null if no
1820 /// [FocusTraversalGroup] ancestor is found.
1821 /// * [maybeOfNode] for a function that will look for a policy using a given
1822 /// [FocusNode], and return null if no policy applies.
1823 static FocusTraversalPolicy of(BuildContext context) {
1824 final FocusTraversalPolicy? policy = maybeOf(context);
1825 assert(() {
1826 if (policy == null) {
1827 throw FlutterError(
1828 'Unable to find a Focus or FocusScope widget in the given context, or the FocusNode '
1829 'from with the widget that was found is not associated with a FocusTraversalPolicy.\n'
1830 'FocusTraversalGroup.of() was called with a context that does not contain a '
1831 'Focus or FocusScope widget, or there was no FocusTraversalPolicy in effect.\n'
1832 'This can happen if there is not a FocusTraversalGroup that defines the policy, '
1833 'or if the context comes from a widget that is above the WidgetsApp, MaterialApp, '
1834 'or CupertinoApp widget (those widgets introduce an implicit default policy) \n'
1835 'The context used was:\n'
1836 ' $context',
1837 );
1838 }
1839 return true;
1840 }());
1841 return policy!;
1842 }
1843
1844 /// Returns the [FocusTraversalPolicy] that applies to the [FocusNode] of the
1845 /// nearest ancestor [Focus] widget, or null, given a [BuildContext].
1846 ///
1847 /// Will return null if it doesn't find an ancestor [Focus] or [FocusScope]
1848 /// widget, or doesn't find a [FocusTraversalPolicy] that applies to the node.
1849 ///
1850 /// {@macro flutter.widgets.focus_traversal.FocusTraversalGroup.of}
1851 ///
1852 /// See also:
1853 ///
1854 /// * [maybeOfNode] for a similar function that will look for a policy using a
1855 /// given [FocusNode].
1856 /// * [of] for a similar function that will throw if no [FocusTraversalPolicy]
1857 /// applies.
1858 static FocusTraversalPolicy? maybeOf(BuildContext context) {
1859 final FocusNode? node = Focus.maybeOf(context, scopeOk: true, createDependency: false);
1860 if (node == null) {
1861 return null;
1862 }
1863 return FocusTraversalGroup.maybeOfNode(node);
1864 }
1865
1866 @override
1867 State<FocusTraversalGroup> createState() => _FocusTraversalGroupState();
1868
1869 @override
1870 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
1871 super.debugFillProperties(properties);
1872 properties.add(DiagnosticsProperty<FocusTraversalPolicy>('policy', policy));
1873 }
1874}
1875
1876// A special focus node subclass that only FocusTraversalGroup uses so that it
1877// can be used to cache the policy in the focus tree, and so that the traversal
1878// code can find groups in the focus tree.
1879class _FocusTraversalGroupNode extends FocusNode {
1880 _FocusTraversalGroupNode({
1881 super.debugLabel,
1882 required this.policy,
1883 }) {
1884 if (kFlutterMemoryAllocationsEnabled) {
1885 ChangeNotifier.maybeDispatchObjectCreation(this);
1886 }
1887 }
1888
1889 FocusTraversalPolicy policy;
1890}
1891
1892class _FocusTraversalGroupState extends State<FocusTraversalGroup> {
1893 // The internal focus node used to collect the children of this node into a
1894 // group, and to provide a context for the traversal algorithm to sort the
1895 // group with. It's a special subclass of FocusNode just so that it can be
1896 // identified when walking the focus tree during traversal, and hold the
1897 // current policy.
1898 late final _FocusTraversalGroupNode focusNode = _FocusTraversalGroupNode(
1899 debugLabel: 'FocusTraversalGroup',
1900 policy: widget.policy,
1901 );
1902
1903 @override
1904 void dispose() {
1905 focusNode.dispose();
1906 super.dispose();
1907 }
1908
1909 @override
1910 void didUpdateWidget (FocusTraversalGroup oldWidget) {
1911 super.didUpdateWidget(oldWidget);
1912 if (oldWidget.policy != widget.policy) {
1913 focusNode.policy = widget.policy;
1914 }
1915 }
1916
1917 @override
1918 Widget build(BuildContext context) {
1919 return Focus(
1920 focusNode: focusNode,
1921 canRequestFocus: false,
1922 skipTraversal: true,
1923 includeSemantics: false,
1924 descendantsAreFocusable: widget.descendantsAreFocusable,
1925 descendantsAreTraversable: widget.descendantsAreTraversable,
1926 child: widget.child,
1927 );
1928 }
1929}
1930
1931/// An intent for use with the [RequestFocusAction], which supplies the
1932/// [FocusNode] that should be focused.
1933class RequestFocusIntent extends Intent {
1934 /// Creates an intent used with [RequestFocusAction].
1935 ///
1936 /// {@macro flutter.widgets.FocusTraversalPolicy.requestFocusCallback}
1937 const RequestFocusIntent(this.focusNode, {
1938 TraversalRequestFocusCallback? requestFocusCallback
1939 }) : requestFocusCallback = requestFocusCallback ?? FocusTraversalPolicy.defaultTraversalRequestFocusCallback;
1940
1941 /// The callback used to move the focus to the node [focusNode].
1942 /// By default it requests focus on the node and ensures the node is visible
1943 /// if it's in a scrollable.
1944 final TraversalRequestFocusCallback requestFocusCallback;
1945
1946 /// The [FocusNode] that is to be focused.
1947 final FocusNode focusNode;
1948}
1949
1950/// An [Action] that requests the focus on the node it is given in its
1951/// [RequestFocusIntent].
1952///
1953/// This action can be used to request focus for a particular node, by calling
1954/// [Action.invoke] like so:
1955///
1956/// ```dart
1957/// Actions.invoke(context, RequestFocusIntent(focusNode));
1958/// ```
1959///
1960/// Where the `focusNode` is the node for which the focus will be requested.
1961///
1962/// The difference between requesting focus in this way versus calling
1963/// [FocusNode.requestFocus] directly is that it will use the [Action]
1964/// registered in the nearest [Actions] widget associated with
1965/// [RequestFocusIntent] to make the request, rather than just requesting focus
1966/// directly. This allows the action to have additional side effects, like
1967/// logging, or undo and redo functionality.
1968///
1969/// This [RequestFocusAction] class is the default action associated with the
1970/// [RequestFocusIntent] in the [WidgetsApp]. It requests focus. You
1971/// can redefine the associated action with your own [Actions] widget.
1972///
1973/// See [FocusTraversalPolicy] for more information about focus traversal.
1974class RequestFocusAction extends Action<RequestFocusIntent> {
1975
1976 @override
1977 void invoke(RequestFocusIntent intent) {
1978 intent.requestFocusCallback(intent.focusNode);
1979 }
1980}
1981
1982/// An [Intent] bound to [NextFocusAction], which moves the focus to the next
1983/// focusable node in the focus traversal order.
1984///
1985/// See [FocusTraversalPolicy] for more information about focus traversal.
1986class NextFocusIntent extends Intent {
1987 /// Creates an intent that is used with [NextFocusAction].
1988 const NextFocusIntent();
1989}
1990
1991/// An [Action] that moves the focus to the next focusable node in the focus
1992/// order.
1993///
1994/// This action is the default action registered for the [NextFocusIntent], and
1995/// by default is bound to the [LogicalKeyboardKey.tab] key in the [WidgetsApp].
1996///
1997/// See [FocusTraversalPolicy] for more information about focus traversal.
1998class NextFocusAction extends Action<NextFocusIntent> {
1999 /// Attempts to pass the focus to the next widget.
2000 ///
2001 /// Returns true if a widget was focused as a result of invoking this action.
2002 ///
2003 /// Returns false when the traversal reached the end and the engine must pass
2004 /// focus to platform UI.
2005 @override
2006 bool invoke(NextFocusIntent intent) {
2007 return primaryFocus!.nextFocus();
2008 }
2009
2010 @override
2011 KeyEventResult toKeyEventResult(NextFocusIntent intent, bool invokeResult) {
2012 return invokeResult ? KeyEventResult.handled : KeyEventResult.skipRemainingHandlers;
2013 }
2014}
2015
2016/// An [Intent] bound to [PreviousFocusAction], which moves the focus to the
2017/// previous focusable node in the focus traversal order.
2018///
2019/// See [FocusTraversalPolicy] for more information about focus traversal.
2020class PreviousFocusIntent extends Intent {
2021 /// Creates an intent that is used with [PreviousFocusAction].
2022 const PreviousFocusIntent();
2023}
2024
2025/// An [Action] that moves the focus to the previous focusable node in the focus
2026/// order.
2027///
2028/// This action is the default action registered for the [PreviousFocusIntent],
2029/// and by default is bound to a combination of the [LogicalKeyboardKey.tab] key
2030/// and the [LogicalKeyboardKey.shift] key in the [WidgetsApp].
2031///
2032/// See [FocusTraversalPolicy] for more information about focus traversal.
2033class PreviousFocusAction extends Action<PreviousFocusIntent> {
2034 /// Attempts to pass the focus to the previous widget.
2035 ///
2036 /// Returns true if a widget was focused as a result of invoking this action.
2037 ///
2038 /// Returns false when the traversal reached the beginning and the engine must
2039 /// pass focus to platform UI.
2040 @override
2041 bool invoke(PreviousFocusIntent intent) {
2042 return primaryFocus!.previousFocus();
2043 }
2044
2045 @override
2046 KeyEventResult toKeyEventResult(PreviousFocusIntent intent, bool invokeResult) {
2047 return invokeResult ? KeyEventResult.handled : KeyEventResult.skipRemainingHandlers;
2048 }
2049}
2050
2051/// An [Intent] that represents moving to the next focusable node in the given
2052/// [direction].
2053///
2054/// This is the [Intent] bound by default to the [LogicalKeyboardKey.arrowUp],
2055/// [LogicalKeyboardKey.arrowDown], [LogicalKeyboardKey.arrowLeft], and
2056/// [LogicalKeyboardKey.arrowRight] keys in the [WidgetsApp], with the
2057/// appropriate associated directions.
2058///
2059/// See [FocusTraversalPolicy] for more information about focus traversal.
2060class DirectionalFocusIntent extends Intent {
2061 /// Creates an intent used to move the focus in the given [direction].
2062 const DirectionalFocusIntent(this.direction, {this.ignoreTextFields = true});
2063
2064 /// The direction in which to look for the next focusable node when the
2065 /// associated [DirectionalFocusAction] is invoked.
2066 final TraversalDirection direction;
2067
2068 /// If true, then directional focus actions that occur within a text field
2069 /// will not happen when the focus node which received the key is a text
2070 /// field.
2071 ///
2072 /// Defaults to true.
2073 final bool ignoreTextFields;
2074
2075 @override
2076 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
2077 super.debugFillProperties(properties);
2078 properties.add(EnumProperty<TraversalDirection>('direction', direction));
2079 }
2080}
2081
2082/// An [Action] that moves the focus to the focusable node in the direction
2083/// configured by the associated [DirectionalFocusIntent.direction].
2084///
2085/// This is the [Action] associated with [DirectionalFocusIntent] and bound by
2086/// default to the [LogicalKeyboardKey.arrowUp], [LogicalKeyboardKey.arrowDown],
2087/// [LogicalKeyboardKey.arrowLeft], and [LogicalKeyboardKey.arrowRight] keys in
2088/// the [WidgetsApp], with the appropriate associated directions.
2089class DirectionalFocusAction extends Action<DirectionalFocusIntent> {
2090 /// Creates a [DirectionalFocusAction].
2091 DirectionalFocusAction() : _isForTextField = false;
2092
2093 /// Creates a [DirectionalFocusAction] that ignores [DirectionalFocusIntent]s
2094 /// whose `ignoreTextFields` field is true.
2095 DirectionalFocusAction.forTextField() : _isForTextField = true;
2096
2097 // Whether this action is defined in a text field.
2098 final bool _isForTextField;
2099 @override
2100 void invoke(DirectionalFocusIntent intent) {
2101 if (!intent.ignoreTextFields || !_isForTextField) {
2102 primaryFocus!.focusInDirection(intent.direction);
2103 }
2104 }
2105}
2106
2107/// A widget that controls whether or not the descendants of this widget are
2108/// traversable.
2109///
2110/// Does not affect the value of [FocusNode.skipTraversal] of the descendants.
2111///
2112/// See also:
2113///
2114/// * [Focus], a widget for adding and managing a [FocusNode] in the widget tree.
2115/// * [ExcludeFocus], a widget that excludes its descendants from focusability.
2116/// * [FocusTraversalGroup], a widget that groups widgets for focus traversal,
2117/// and can also be used in the same way as this widget by setting its
2118/// `descendantsAreFocusable` attribute.
2119class ExcludeFocusTraversal extends StatelessWidget {
2120 /// Const constructor for [ExcludeFocusTraversal] widget.
2121 const ExcludeFocusTraversal({
2122 super.key,
2123 this.excluding = true,
2124 required this.child,
2125 });
2126
2127 /// If true, will make this widget's descendants untraversable.
2128 ///
2129 /// Defaults to true.
2130 ///
2131 /// Does not affect the value of [FocusNode.skipTraversal] on the descendants.
2132 ///
2133 /// See also:
2134 ///
2135 /// * [Focus.descendantsAreTraversable], the attribute of a [Focus] widget that
2136 /// controls this same property for focus widgets.
2137 /// * [FocusTraversalGroup], a widget used to group together and configure the
2138 /// focus traversal policy for a widget subtree that has a
2139 /// `descendantsAreFocusable` parameter to conditionally block focus for a
2140 /// subtree.
2141 final bool excluding;
2142
2143 /// The child widget of this [ExcludeFocusTraversal].
2144 ///
2145 /// {@macro flutter.widgets.ProxyWidget.child}
2146 final Widget child;
2147
2148 @override
2149 Widget build(BuildContext context) {
2150 return Focus(
2151 canRequestFocus: false,
2152 skipTraversal: true,
2153 includeSemantics: false,
2154 descendantsAreTraversable: !excluding,
2155 child: child,
2156 );
2157 }
2158}
2159