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 | import 'framework.dart'; |
6 | |
7 | // Examples can assume: |
8 | // TooltipThemeData data = const TooltipThemeData(); |
9 | |
10 | /// An [InheritedWidget] that defines visual properties like colors |
11 | /// and text styles, which the [child]'s subtree depends on. |
12 | /// |
13 | /// The [wrap] method is used by [captureAll] and [CapturedThemes.wrap] to |
14 | /// construct a widget that will wrap a child in all of the inherited themes |
15 | /// which are present in a specified part of the widget tree. |
16 | /// |
17 | /// A widget that's shown in a different context from the one it's built in, |
18 | /// like the contents of a new route or an overlay, will be able to see the |
19 | /// ancestor inherited themes of the context it was built in. |
20 | /// |
21 | /// {@tool dartpad} |
22 | /// This example demonstrates how `InheritedTheme.capture()` can be used |
23 | /// to wrap the contents of a new route with the inherited themes that |
24 | /// are present when the route was built - but are not present when route |
25 | /// is actually shown. |
26 | /// |
27 | /// If the same code is run without `InheritedTheme.capture(), the |
28 | /// new route's Text widget will inherit the "something must be wrong" |
29 | /// fallback text style, rather than the default text style defined in MyApp. |
30 | /// |
31 | /// ** See code in examples/api/lib/widgets/inherited_theme/inherited_theme.0.dart ** |
32 | /// {@end-tool} |
33 | abstract class InheritedTheme extends InheritedWidget { |
34 | /// Abstract const constructor. This constructor enables subclasses to provide |
35 | /// const constructors so that they can be used in const expressions. |
36 | |
37 | const InheritedTheme({ |
38 | super.key, |
39 | required super.child, |
40 | }); |
41 | |
42 | /// Return a copy of this inherited theme with the specified [child]. |
43 | /// |
44 | /// This implementation for [TooltipTheme] is typical: |
45 | /// |
46 | /// ```dart |
47 | /// Widget wrap(BuildContext context, Widget child) { |
48 | /// return TooltipTheme(data: data, child: child); |
49 | /// } |
50 | /// ``` |
51 | Widget wrap(BuildContext context, Widget child); |
52 | |
53 | /// Returns a widget that will [wrap] `child` in all of the inherited themes |
54 | /// which are present between `context` and the specified `to` |
55 | /// [BuildContext]. |
56 | /// |
57 | /// The `to` context must be an ancestor of `context`. If `to` is not |
58 | /// specified, all inherited themes up to the root of the widget tree are |
59 | /// captured. |
60 | /// |
61 | /// After calling this method, the themes present between `context` and `to` |
62 | /// are frozen for the provided `child`. If the themes (or their theme data) |
63 | /// change in the original subtree, those changes will not be visible to |
64 | /// the wrapped `child` - unless this method is called again to re-wrap the |
65 | /// child. |
66 | static Widget captureAll(BuildContext context, Widget child, {BuildContext? to}) { |
67 | |
68 | return capture(from: context, to: to).wrap(child); |
69 | } |
70 | |
71 | /// Returns a [CapturedThemes] object that includes all the [InheritedTheme]s |
72 | /// between the given `from` and `to` [BuildContext]s. |
73 | /// |
74 | /// The `to` context must be an ancestor of the `from` context. If `to` is |
75 | /// null, all ancestor inherited themes of `from` up to the root of the |
76 | /// widget tree are captured. |
77 | /// |
78 | /// After calling this method, the themes present between `from` and `to` are |
79 | /// frozen in the returned [CapturedThemes] object. If the themes (or their |
80 | /// theme data) change in the original subtree, those changes will not be |
81 | /// applied to the themes captured in the [CapturedThemes] object - unless |
82 | /// this method is called again to re-capture the updated themes. |
83 | /// |
84 | /// To wrap a [Widget] in the captured themes, call [CapturedThemes.wrap]. |
85 | /// |
86 | /// This method can be expensive if there are many widgets between `from` and |
87 | /// `to` (it walks the element tree between those nodes). |
88 | static CapturedThemes capture({ required BuildContext from, required BuildContext? to }) { |
89 | |
90 | if (from == to) { |
91 | // Nothing to capture. |
92 | return CapturedThemes._(const <InheritedTheme>[]); |
93 | } |
94 | |
95 | final List<InheritedTheme> themes = <InheritedTheme>[]; |
96 | final Set<Type> themeTypes = <Type>{}; |
97 | late bool debugDidFindAncestor; |
98 | assert(() { |
99 | debugDidFindAncestor = to == null; |
100 | return true; |
101 | }()); |
102 | from.visitAncestorElements((Element ancestor) { |
103 | if (ancestor == to) { |
104 | assert(() { |
105 | debugDidFindAncestor = true; |
106 | return true; |
107 | }()); |
108 | return false; |
109 | } |
110 | if (ancestor is InheritedElement && ancestor.widget is InheritedTheme) { |
111 | final InheritedTheme theme = ancestor.widget as InheritedTheme; |
112 | final Type themeType = theme.runtimeType; |
113 | // Only remember the first theme of any type. This assumes |
114 | // that inherited themes completely shadow ancestors of the |
115 | // same type. |
116 | if (!themeTypes.contains(themeType)) { |
117 | themeTypes.add(themeType); |
118 | themes.add(theme); |
119 | } |
120 | } |
121 | return true; |
122 | }); |
123 | |
124 | assert(debugDidFindAncestor, 'The provided `to` context must be an ancestor of the `from` context.' ); |
125 | return CapturedThemes._(themes); |
126 | } |
127 | } |
128 | |
129 | /// Stores a list of captured [InheritedTheme]s that can be wrapped around a |
130 | /// child [Widget]. |
131 | /// |
132 | /// Used as return type by [InheritedTheme.capture]. |
133 | class CapturedThemes { |
134 | CapturedThemes._(this._themes); |
135 | |
136 | final List<InheritedTheme> _themes; |
137 | |
138 | /// Wraps a `child` [Widget] in the [InheritedTheme]s captured in this object. |
139 | Widget wrap(Widget child) { |
140 | return _CaptureAll(themes: _themes, child: child); |
141 | } |
142 | } |
143 | |
144 | class _CaptureAll extends StatelessWidget { |
145 | const _CaptureAll({ |
146 | required this.themes, |
147 | required this.child, |
148 | }); |
149 | |
150 | final List<InheritedTheme> themes; |
151 | final Widget child; |
152 | |
153 | @override |
154 | Widget build(BuildContext context) { |
155 | Widget wrappedChild = child; |
156 | for (final InheritedTheme theme in themes) { |
157 | wrappedChild = theme.wrap(context, wrappedChild); |
158 | } |
159 | return wrappedChild; |
160 | } |
161 | } |
162 | |