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
5import '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}
33abstract 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].
133class 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
144class _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