// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'framework.dart'; // Examples can assume: // class MyWidget extends StatelessWidget { const MyWidget({super.key, required this.child}); final Widget child; @override Widget build(BuildContext context) => child; } /// A lookup boundary controls what entities are visible to descendants of the /// boundary via the static lookup methods provided by the boundary. /// /// The static lookup methods of the boundary mirror the lookup methods by the /// same name exposed on [BuildContext] and they can be used as direct /// replacements. Unlike the methods on [BuildContext], these methods do not /// find any ancestor entities of the closest [LookupBoundary] surrounding the /// provided [BuildContext]. The root of the tree is an implicit lookup boundary. /// /// {@tool snippet} /// In the example below, the [LookupBoundary.findAncestorWidgetOfExactType] /// call returns null because the [LookupBoundary] "hides" `MyWidget` from the /// [BuildContext] that was queried. /// /// ```dart /// MyWidget( /// child: LookupBoundary( /// child: Builder( /// builder: (BuildContext context) { /// MyWidget? widget = LookupBoundary.findAncestorWidgetOfExactType<MyWidget>(context); /// return Text('$widget'); // "null" /// }, /// ), /// ), /// ) /// ``` /// {@end-tool} /// /// A [LookupBoundary] only affects the behavior of the static lookup methods /// defined on the boundary. It does not affect the behavior of the lookup /// methods defined on [BuildContext]. /// /// A [LookupBoundary] is rarely instantiated directly. They are inserted at /// locations of the widget tree where the render tree diverges from the element /// tree, which is rather uncommon. Such anomalies are created by /// [RenderObjectElement]s that don't attach their [RenderObject] to the closest /// ancestor [RenderObjectElement], e.g. because they bootstrap a separate /// stand-alone render tree. // TODO(goderbauer): Reference the View widget here once available. /// This behavior breaks the assumption some widgets have about the structure of /// the render tree: These widgets may try to reach out to an ancestor widget, /// assuming that their associated [RenderObject]s are also ancestors, which due /// to the anomaly may not be the case. At the point where the divergence in the /// two trees is introduced, a [LookupBoundary] can be used to hide that ancestor /// from the querying widget. /// /// As an example, [Material.of] relies on lookup boundaries to hide the /// [Material] widget from certain descendant button widget. Buttons reach out /// to their [Material] ancestor to draw ink splashes on its associated render /// object. This only produces the desired effect if the button render object /// is a descendant of the [Material] render object. If the element tree and /// the render tree are not in sync due to anomalies described above, this may /// not be the case. To avoid incorrect visuals, the [Material] relies on /// lookup boundaries to hide itself from descendants in subtrees with such /// anomalies. Those subtrees are expected to introduce their own [Material] /// widget that buttons there can utilize without crossing a lookup boundary. class LookupBoundary extends InheritedWidget { /// Creates a [LookupBoundary]. /// /// A none-null [child] widget must be provided. const LookupBoundary({super.key, required super.child}); /// Obtains the nearest widget of the given type `T` within the current /// [LookupBoundary] of `context`, which must be the type of a concrete /// [InheritedWidget] subclass, and registers the provided build `context` /// with that widget such that when that widget changes (or a new widget of /// that type is introduced, or the widget goes away), the build context is /// rebuilt so that it can obtain new values from that widget. /// /// This method behaves exactly like /// [BuildContext.dependOnInheritedWidgetOfExactType], except it only /// considers [InheritedWidget]s of the specified type `T` between the /// provided [BuildContext] and its closest [LookupBoundary] ancestor. /// [InheritedWidget]s past that [LookupBoundary] are invisible to this /// method. The root of the tree is treated as an implicit lookup boundary. /// /// {@macro flutter.widgets.BuildContext.dependOnInheritedWidgetOfExactType} static T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>(BuildContext context, { Object? aspect }) { // The following call makes sure that context depends on something so // Element.didChangeDependencies is called when context moves in the tree // even when requested dependency remains unfulfilled (i.e. null is // returned). context.dependOnInheritedWidgetOfExactType<LookupBoundary>(); final InheritedElement? candidate = getElementForInheritedWidgetOfExactType<T>(context); if (candidate == null) { return null; } context.dependOnInheritedElement(candidate, aspect: aspect); return candidate.widget as T; } /// Obtains the element corresponding to the nearest widget of the given type /// `T` within the current [LookupBoundary] of `context`. /// /// `T` must be the type of a concrete [InheritedWidget] subclass. Returns /// null if no such element is found. /// /// This method behaves exactly like /// [BuildContext.getElementForInheritedWidgetOfExactType], except it only /// considers [InheritedWidget]s of the specified type `T` between the /// provided [BuildContext] and its closest [LookupBoundary] ancestor. /// [InheritedWidget]s past that [LookupBoundary] are invisible to this /// method. The root of the tree is treated as an implicit lookup boundary. /// /// {@macro flutter.widgets.BuildContext.getElementForInheritedWidgetOfExactType} static InheritedElement? getElementForInheritedWidgetOfExactType<T extends InheritedWidget>(BuildContext context) { final InheritedElement? candidate = context.getElementForInheritedWidgetOfExactType<T>(); if (candidate == null) { return null; } final Element? boundary = context.getElementForInheritedWidgetOfExactType<LookupBoundary>(); if (boundary != null && boundary.depth > candidate.depth) { return null; } return candidate; } /// Returns the nearest ancestor widget of the given type `T` within the /// current [LookupBoundary] of `context`. /// /// `T` must be the type of a concrete [Widget] subclass. /// /// This method behaves exactly like /// [BuildContext.findAncestorWidgetOfExactType], except it only considers /// [Widget]s of the specified type `T` between the provided [BuildContext] /// and its closest [LookupBoundary] ancestor. [Widget]s past that /// [LookupBoundary] are invisible to this method. The root of the tree is /// treated as an implicit lookup boundary. /// /// {@macro flutter.widgets.BuildContext.findAncestorWidgetOfExactType} static T? findAncestorWidgetOfExactType<T extends Widget>(BuildContext context) { Element? target; context.visitAncestorElements((Element ancestor) { if (ancestor.widget.runtimeType == T) { target = ancestor; return false; } return ancestor.widget.runtimeType != LookupBoundary; }); return target?.widget as T?; } /// Returns the [State] object of the nearest ancestor [StatefulWidget] widget /// within the current [LookupBoundary] of `context` that is an instance of /// the given type `T`. /// /// This method behaves exactly like /// [BuildContext.findAncestorWidgetOfExactType], except it only considers /// [State] objects of the specified type `T` between the provided /// [BuildContext] and its closest [LookupBoundary] ancestor. [State] objects /// past that [LookupBoundary] are invisible to this method. The root of the /// tree is treated as an implicit lookup boundary. /// /// {@macro flutter.widgets.BuildContext.findAncestorStateOfType} static T? findAncestorStateOfType<T extends State>(BuildContext context) { StatefulElement? target; context.visitAncestorElements((Element ancestor) { if (ancestor is StatefulElement && ancestor.state is T) { target = ancestor; return false; } return ancestor.widget.runtimeType != LookupBoundary; }); return target?.state as T?; } /// Returns the [State] object of the furthest ancestor [StatefulWidget] /// widget within the current [LookupBoundary] of `context` that is an /// instance of the given type `T`. /// /// This method behaves exactly like /// [BuildContext.findRootAncestorStateOfType], except it considers the /// closest [LookupBoundary] ancestor of `context` to be the root. [State] /// objects past that [LookupBoundary] are invisible to this method. The root /// of the tree is treated as an implicit lookup boundary. /// /// {@macro flutter.widgets.BuildContext.findRootAncestorStateOfType} static T? findRootAncestorStateOfType<T extends State>(BuildContext context) { StatefulElement? target; context.visitAncestorElements((Element ancestor) { if (ancestor is StatefulElement && ancestor.state is T) { target = ancestor; } return ancestor.widget.runtimeType != LookupBoundary; }); return target?.state as T?; } /// Returns the [RenderObject] object of the nearest ancestor /// [RenderObjectWidget] widget within the current [LookupBoundary] of /// `context` that is an instance of the given type `T`. /// /// This method behaves exactly like /// [BuildContext.findAncestorRenderObjectOfType], except it only considers /// [RenderObject]s of the specified type `T` between the provided /// [BuildContext] and its closest [LookupBoundary] ancestor. [RenderObject]s /// past that [LookupBoundary] are invisible to this method. The root of the /// tree is treated as an implicit lookup boundary. /// /// {@macro flutter.widgets.BuildContext.findAncestorRenderObjectOfType} static T? findAncestorRenderObjectOfType<T extends RenderObject>(BuildContext context) { Element? target; context.visitAncestorElements((Element ancestor) { if (ancestor is RenderObjectElement && ancestor.renderObject is T) { target = ancestor; return false; } return ancestor.widget.runtimeType != LookupBoundary; }); return target?.renderObject as T?; } /// Walks the ancestor chain, starting with the parent of the build context's /// widget, invoking the argument for each ancestor until a [LookupBoundary] /// or the root is reached. /// /// This method behaves exactly like [BuildContext.visitAncestorElements], /// except it only walks the tree up to the closest [LookupBoundary] ancestor /// of the provided context. The root of the tree is treated as an implicit /// lookup boundary. /// /// {@macro flutter.widgets.BuildContext.visitAncestorElements} static void visitAncestorElements(BuildContext context, ConditionalElementVisitor visitor) { context.visitAncestorElements((Element ancestor) { return visitor(ancestor) && ancestor.widget.runtimeType != LookupBoundary; }); } /// Walks the non-[LookupBoundary] child [Element]s of the provided /// `context`. /// /// This method behaves exactly like [BuildContext.visitChildElements], /// except it only visits children that are not a [LookupBoundary]. /// /// {@macro flutter.widgets.BuildContext.visitChildElements} static void visitChildElements(BuildContext context, ElementVisitor visitor) { context.visitChildElements((Element child) { if (child.widget.runtimeType != LookupBoundary) { visitor(child); } }); } /// Returns true if a [LookupBoundary] is hiding the nearest /// [Widget] of the specified type `T` from the provided [BuildContext]. /// /// This method throws when asserts are disabled. static bool debugIsHidingAncestorWidgetOfExactType<T extends Widget>(BuildContext context) { bool? result; assert(() { bool hiddenByBoundary = false; bool ancestorFound = false; context.visitAncestorElements((Element ancestor) { if (ancestor.widget.runtimeType == T) { ancestorFound = true; return false; } hiddenByBoundary = hiddenByBoundary || ancestor.widget.runtimeType == LookupBoundary; return true; }); result = ancestorFound & hiddenByBoundary; return true; } ()); return result!; } /// Returns true if a [LookupBoundary] is hiding the nearest [StatefulWidget] /// with a [State] of the specified type `T` from the provided [BuildContext]. /// /// This method throws when asserts are disabled. static bool debugIsHidingAncestorStateOfType<T extends State>(BuildContext context) { bool? result; assert(() { bool hiddenByBoundary = false; bool ancestorFound = false; context.visitAncestorElements((Element ancestor) { if (ancestor is StatefulElement && ancestor.state is T) { ancestorFound = true; return false; } hiddenByBoundary = hiddenByBoundary || ancestor.widget.runtimeType == LookupBoundary; return true; }); result = ancestorFound & hiddenByBoundary; return true; } ()); return result!; } /// Returns true if a [LookupBoundary] is hiding the nearest /// [RenderObjectWidget] with a [RenderObject] of the specified type `T` /// from the provided [BuildContext]. /// /// This method throws when asserts are disabled. static bool debugIsHidingAncestorRenderObjectOfType<T extends RenderObject>(BuildContext context) { bool? result; assert(() { bool hiddenByBoundary = false; bool ancestorFound = false; context.visitAncestorElements((Element ancestor) { if (ancestor is RenderObjectElement && ancestor.renderObject is T) { ancestorFound = true; return false; } hiddenByBoundary = hiddenByBoundary || ancestor.widget.runtimeType == LookupBoundary; return true; }); result = ancestorFound & hiddenByBoundary; return true; } ()); return result!; } @override bool updateShouldNotify(covariant InheritedWidget oldWidget) => false; }