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 'package:flutter/material.dart';
6library;
7
8import 'framework.dart';
9
10// Examples can assume:
11// class MyWidget extends StatelessWidget { const MyWidget({super.key, required this.child}); final Widget child; @override Widget build(BuildContext context) => child; }
12
13/// A lookup boundary controls what entities are visible to descendants of the
14/// boundary via the static lookup methods provided by the boundary.
15///
16/// The static lookup methods of the boundary mirror the lookup methods by the
17/// same name exposed on [BuildContext] and they can be used as direct
18/// replacements. Unlike the methods on [BuildContext], these methods do not
19/// find any ancestor entities of the closest [LookupBoundary] surrounding the
20/// provided [BuildContext]. The root of the tree is an implicit lookup boundary.
21///
22/// {@tool snippet}
23/// In the example below, the [LookupBoundary.findAncestorWidgetOfExactType]
24/// call returns null because the [LookupBoundary] "hides" `MyWidget` from the
25/// [BuildContext] that was queried.
26///
27/// ```dart
28/// MyWidget(
29/// child: LookupBoundary(
30/// child: Builder(
31/// builder: (BuildContext context) {
32/// MyWidget? widget = LookupBoundary.findAncestorWidgetOfExactType<MyWidget>(context);
33/// return Text('$widget'); // "null"
34/// },
35/// ),
36/// ),
37/// )
38/// ```
39/// {@end-tool}
40///
41/// A [LookupBoundary] only affects the behavior of the static lookup methods
42/// defined on the boundary. It does not affect the behavior of the lookup
43/// methods defined on [BuildContext].
44///
45/// A [LookupBoundary] is rarely instantiated directly. They are inserted at
46/// locations of the widget tree where the render tree diverges from the element
47/// tree, which is rather uncommon. Such anomalies are created by
48/// [RenderObjectElement]s that don't attach their [RenderObject] to the closest
49/// ancestor [RenderObjectElement], e.g. because they bootstrap a separate
50/// stand-alone render tree. This behavior breaks the assumption some widgets
51/// have about the structure of the render tree: These widgets may try to reach
52/// out to an ancestor widget, assuming that their associated [RenderObject]s
53/// are also ancestors, which due to the anomaly may not be the case. At the
54/// point where the divergence in the two trees is introduced, a
55/// [LookupBoundary] can be used to hide that ancestor from the querying widget.
56/// The [ViewAnchor], for example, wraps its [ViewAnchor.view] child in a
57/// [LookupBoundary] because the [RenderObject] produced by that widget subtree
58/// is not attached to the render tree that the [ViewAnchor] itself belongs to.
59///
60/// As an example, [Material.of] relies on lookup boundaries to hide the
61/// [Material] widget from certain descendant button widget. Buttons reach out
62/// to their [Material] ancestor to draw ink splashes on its associated render
63/// object. This only produces the desired effect if the button render object
64/// is a descendant of the [Material] render object. If the element tree and
65/// the render tree are not in sync due to anomalies described above, this may
66/// not be the case. To avoid incorrect visuals, the [Material] relies on
67/// lookup boundaries to hide itself from descendants in subtrees with such
68/// anomalies. Those subtrees are expected to introduce their own [Material]
69/// widget that buttons there can utilize without crossing a lookup boundary.
70class LookupBoundary extends InheritedWidget {
71 /// Creates a [LookupBoundary].
72 ///
73 /// A none-null [child] widget must be provided.
74 const LookupBoundary({super.key, required super.child});
75
76 /// Obtains the nearest widget of the given type `T` within the current
77 /// [LookupBoundary] of `context`, which must be the type of a concrete
78 /// [InheritedWidget] subclass, and registers the provided build `context`
79 /// with that widget such that when that widget changes (or a new widget of
80 /// that type is introduced, or the widget goes away), the build context is
81 /// rebuilt so that it can obtain new values from that widget.
82 ///
83 /// This method behaves exactly like
84 /// [BuildContext.dependOnInheritedWidgetOfExactType], except it only
85 /// considers [InheritedWidget]s of the specified type `T` between the
86 /// provided [BuildContext] and its closest [LookupBoundary] ancestor.
87 /// [InheritedWidget]s past that [LookupBoundary] are invisible to this
88 /// method. The root of the tree is treated as an implicit lookup boundary.
89 ///
90 /// {@macro flutter.widgets.BuildContext.dependOnInheritedWidgetOfExactType}
91 static T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>(
92 BuildContext context, {
93 Object? aspect,
94 }) {
95 // The following call makes sure that context depends on something so
96 // Element.didChangeDependencies is called when context moves in the tree
97 // even when requested dependency remains unfulfilled (i.e. null is
98 // returned).
99 context.dependOnInheritedWidgetOfExactType<LookupBoundary>();
100 final InheritedElement? candidate = getElementForInheritedWidgetOfExactType<T>(context);
101 if (candidate == null) {
102 return null;
103 }
104 context.dependOnInheritedElement(candidate, aspect: aspect);
105 return candidate.widget as T;
106 }
107
108 /// Obtains the element corresponding to the nearest widget of the given type
109 /// `T` within the current [LookupBoundary] of `context`.
110 ///
111 /// `T` must be the type of a concrete [InheritedWidget] subclass. Returns
112 /// null if no such element is found.
113 ///
114 /// This method behaves exactly like
115 /// [BuildContext.getElementForInheritedWidgetOfExactType], except it only
116 /// considers [InheritedWidget]s of the specified type `T` between the
117 /// provided [BuildContext] and its closest [LookupBoundary] ancestor.
118 /// [InheritedWidget]s past that [LookupBoundary] are invisible to this
119 /// method. The root of the tree is treated as an implicit lookup boundary.
120 ///
121 /// {@macro flutter.widgets.BuildContext.getElementForInheritedWidgetOfExactType}
122 static InheritedElement? getElementForInheritedWidgetOfExactType<T extends InheritedWidget>(
123 BuildContext context,
124 ) {
125 final InheritedElement? candidate = context.getElementForInheritedWidgetOfExactType<T>();
126 if (candidate == null) {
127 return null;
128 }
129 final Element? boundary = context.getElementForInheritedWidgetOfExactType<LookupBoundary>();
130 if (boundary != null && boundary.depth > candidate.depth) {
131 return null;
132 }
133 return candidate;
134 }
135
136 /// Returns the nearest ancestor widget of the given type `T` within the
137 /// current [LookupBoundary] of `context`.
138 ///
139 /// `T` must be the type of a concrete [Widget] subclass.
140 ///
141 /// This method behaves exactly like
142 /// [BuildContext.findAncestorWidgetOfExactType], except it only considers
143 /// [Widget]s of the specified type `T` between the provided [BuildContext]
144 /// and its closest [LookupBoundary] ancestor. [Widget]s past that
145 /// [LookupBoundary] are invisible to this method. The root of the tree is
146 /// treated as an implicit lookup boundary.
147 ///
148 /// {@macro flutter.widgets.BuildContext.findAncestorWidgetOfExactType}
149 static T? findAncestorWidgetOfExactType<T extends Widget>(BuildContext context) {
150 Element? target;
151 context.visitAncestorElements((Element ancestor) {
152 if (ancestor.widget.runtimeType == T) {
153 target = ancestor;
154 return false;
155 }
156 return ancestor.widget.runtimeType != LookupBoundary;
157 });
158 return target?.widget as T?;
159 }
160
161 /// Returns the [State] object of the nearest ancestor [StatefulWidget] widget
162 /// within the current [LookupBoundary] of `context` that is an instance of
163 /// the given type `T`.
164 ///
165 /// This method behaves exactly like
166 /// [BuildContext.findAncestorWidgetOfExactType], except it only considers
167 /// [State] objects of the specified type `T` between the provided
168 /// [BuildContext] and its closest [LookupBoundary] ancestor. [State] objects
169 /// past that [LookupBoundary] are invisible to this method. The root of the
170 /// tree is treated as an implicit lookup boundary.
171 ///
172 /// {@macro flutter.widgets.BuildContext.findAncestorStateOfType}
173 static T? findAncestorStateOfType<T extends State>(BuildContext context) {
174 StatefulElement? target;
175 context.visitAncestorElements((Element ancestor) {
176 if (ancestor is StatefulElement && ancestor.state is T) {
177 target = ancestor;
178 return false;
179 }
180 return ancestor.widget.runtimeType != LookupBoundary;
181 });
182 return target?.state as T?;
183 }
184
185 /// Returns the [State] object of the furthest ancestor [StatefulWidget]
186 /// widget within the current [LookupBoundary] of `context` that is an
187 /// instance of the given type `T`.
188 ///
189 /// This method behaves exactly like
190 /// [BuildContext.findRootAncestorStateOfType], except it considers the
191 /// closest [LookupBoundary] ancestor of `context` to be the root. [State]
192 /// objects past that [LookupBoundary] are invisible to this method. The root
193 /// of the tree is treated as an implicit lookup boundary.
194 ///
195 /// {@macro flutter.widgets.BuildContext.findRootAncestorStateOfType}
196 static T? findRootAncestorStateOfType<T extends State>(BuildContext context) {
197 StatefulElement? target;
198 context.visitAncestorElements((Element ancestor) {
199 if (ancestor is StatefulElement && ancestor.state is T) {
200 target = ancestor;
201 }
202 return ancestor.widget.runtimeType != LookupBoundary;
203 });
204 return target?.state as T?;
205 }
206
207 /// Returns the [RenderObject] object of the nearest ancestor
208 /// [RenderObjectWidget] widget within the current [LookupBoundary] of
209 /// `context` that is an instance of the given type `T`.
210 ///
211 /// This method behaves exactly like
212 /// [BuildContext.findAncestorRenderObjectOfType], except it only considers
213 /// [RenderObject]s of the specified type `T` between the provided
214 /// [BuildContext] and its closest [LookupBoundary] ancestor. [RenderObject]s
215 /// past that [LookupBoundary] are invisible to this method. The root of the
216 /// tree is treated as an implicit lookup boundary.
217 ///
218 /// {@macro flutter.widgets.BuildContext.findAncestorRenderObjectOfType}
219 static T? findAncestorRenderObjectOfType<T extends RenderObject>(BuildContext context) {
220 Element? target;
221 context.visitAncestorElements((Element ancestor) {
222 if (ancestor is RenderObjectElement && ancestor.renderObject is T) {
223 target = ancestor;
224 return false;
225 }
226 return ancestor.widget.runtimeType != LookupBoundary;
227 });
228 return target?.renderObject as T?;
229 }
230
231 /// Walks the ancestor chain, starting with the parent of the build context's
232 /// widget, invoking the argument for each ancestor until a [LookupBoundary]
233 /// or the root is reached.
234 ///
235 /// This method behaves exactly like [BuildContext.visitAncestorElements],
236 /// except it only walks the tree up to the closest [LookupBoundary] ancestor
237 /// of the provided context. The root of the tree is treated as an implicit
238 /// lookup boundary.
239 ///
240 /// {@macro flutter.widgets.BuildContext.visitAncestorElements}
241 static void visitAncestorElements(BuildContext context, ConditionalElementVisitor visitor) {
242 context.visitAncestorElements((Element ancestor) {
243 return visitor(ancestor) && ancestor.widget.runtimeType != LookupBoundary;
244 });
245 }
246
247 /// Walks the non-[LookupBoundary] child [Element]s of the provided
248 /// `context`.
249 ///
250 /// This method behaves exactly like [BuildContext.visitChildElements],
251 /// except it only visits children that are not a [LookupBoundary].
252 ///
253 /// {@macro flutter.widgets.BuildContext.visitChildElements}
254 static void visitChildElements(BuildContext context, ElementVisitor visitor) {
255 context.visitChildElements((Element child) {
256 if (child.widget.runtimeType != LookupBoundary) {
257 visitor(child);
258 }
259 });
260 }
261
262 /// Returns true if a [LookupBoundary] is hiding the nearest
263 /// [Widget] of the specified type `T` from the provided [BuildContext].
264 ///
265 /// This method throws when asserts are disabled.
266 static bool debugIsHidingAncestorWidgetOfExactType<T extends Widget>(BuildContext context) {
267 bool? result;
268 assert(() {
269 bool hiddenByBoundary = false;
270 bool ancestorFound = false;
271 context.visitAncestorElements((Element ancestor) {
272 if (ancestor.widget.runtimeType == T) {
273 ancestorFound = true;
274 return false;
275 }
276 hiddenByBoundary = hiddenByBoundary || ancestor.widget.runtimeType == LookupBoundary;
277 return true;
278 });
279 result = ancestorFound & hiddenByBoundary;
280 return true;
281 }());
282 return result!;
283 }
284
285 /// Returns true if a [LookupBoundary] is hiding the nearest [StatefulWidget]
286 /// with a [State] of the specified type `T` from the provided [BuildContext].
287 ///
288 /// This method throws when asserts are disabled.
289 static bool debugIsHidingAncestorStateOfType<T extends State>(BuildContext context) {
290 bool? result;
291 assert(() {
292 bool hiddenByBoundary = false;
293 bool ancestorFound = false;
294 context.visitAncestorElements((Element ancestor) {
295 if (ancestor is StatefulElement && ancestor.state is T) {
296 ancestorFound = true;
297 return false;
298 }
299 hiddenByBoundary = hiddenByBoundary || ancestor.widget.runtimeType == LookupBoundary;
300 return true;
301 });
302 result = ancestorFound & hiddenByBoundary;
303 return true;
304 }());
305 return result!;
306 }
307
308 /// Returns true if a [LookupBoundary] is hiding the nearest
309 /// [RenderObjectWidget] with a [RenderObject] of the specified type `T`
310 /// from the provided [BuildContext].
311 ///
312 /// This method throws when asserts are disabled.
313 static bool debugIsHidingAncestorRenderObjectOfType<T extends RenderObject>(
314 BuildContext context,
315 ) {
316 bool? result;
317 assert(() {
318 bool hiddenByBoundary = false;
319 bool ancestorFound = false;
320 context.visitAncestorElements((Element ancestor) {
321 if (ancestor is RenderObjectElement && ancestor.renderObject is T) {
322 ancestorFound = true;
323 return false;
324 }
325 hiddenByBoundary = hiddenByBoundary || ancestor.widget.runtimeType == LookupBoundary;
326 return true;
327 });
328 result = ancestorFound & hiddenByBoundary;
329 return true;
330 }());
331 return result!;
332 }
333
334 @override
335 bool updateShouldNotify(covariant InheritedWidget oldWidget) => false;
336}
337