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