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