Line data Source code
1 : import 'package:flutter/rendering.dart';
2 : import 'package:flutter/widgets.dart';
3 :
4 : import '../../../extensions.dart';
5 : import '../../extensions/size.dart';
6 : import '../game.dart';
7 : import '../game_render_box.dart';
8 : import 'gestures.dart';
9 :
10 : typedef GameLoadingWidgetBuilder = Widget Function(
11 : BuildContext,
12 : );
13 :
14 : typedef GameErrorWidgetBuilder = Widget Function(
15 : BuildContext,
16 : Object error,
17 : );
18 :
19 : typedef OverlayWidgetBuilder<T extends Game> = Widget Function(
20 : BuildContext context,
21 : T game,
22 : );
23 :
24 : /// A [StatefulWidget] that is in charge of attaching a [Game] instance into the flutter tree
25 : ///
26 : class GameWidget<T extends Game> extends StatefulWidget {
27 : /// The game instance in which this widget will render
28 : final T game;
29 :
30 : /// The text direction to be used in text elements in a game.
31 : final TextDirection? textDirection;
32 :
33 : /// Builder to provide a widget tree to be built whilst the [Future] provided
34 : /// via [Game.onLoad] is not resolved. By default this is an empty Container().
35 : final GameLoadingWidgetBuilder? loadingBuilder;
36 :
37 : /// If set, errors during the onLoad method will not be thrown
38 : /// but instead this widget will be shown. If not provided, errors are
39 : /// propagated up.
40 : final GameErrorWidgetBuilder? errorBuilder;
41 :
42 : /// Builder to provide a widget tree to be built between the game elements and
43 : /// the background color provided via [Game.backgroundColor]
44 : final WidgetBuilder? backgroundBuilder;
45 :
46 : /// A map to show widgets overlay.
47 : ///
48 : /// See also:
49 : /// - [new GameWidget]
50 : /// - [Game.overlays]
51 : final Map<String, OverlayWidgetBuilder<T>>? overlayBuilderMap;
52 :
53 : /// A List of the initially active overlays, this is used only on the first build of the widget.
54 : /// To control the overlays that are active use [Game.overlays]
55 : ///
56 : /// See also:
57 : /// - [new GameWidget]
58 : /// - [Game.overlays]
59 : final List<String>? initialActiveOverlays;
60 :
61 : /// Renders a [game] in a flutter widget tree.
62 : ///
63 : /// Ex:
64 : /// ```
65 : /// ...
66 : /// Widget build(BuildContext context) {
67 : /// return GameWidget(
68 : /// game: MyGameClass(),
69 : /// )
70 : /// }
71 : /// ...
72 : /// ```
73 : ///
74 : /// It is also possible to render layers of widgets over the game surface with widget subtrees.
75 : ///
76 : /// To do that a [overlayBuilderMap] should be provided. The visibility of
77 : /// these overlays are controlled by [Game.overlays] property
78 : ///
79 : /// Ex:
80 : /// ```
81 : /// ...
82 : ///
83 : /// final game = MyGame();
84 : ///
85 : /// Widget build(BuildContext context) {
86 : /// return GameWidget(
87 : /// game: game,
88 : /// overlayBuilderMap: {
89 : /// 'PauseMenu': (ctx, game) {
90 : /// return Text('A pause menu');
91 : /// },
92 : /// },
93 : /// )
94 : /// }
95 : /// ...
96 : /// game.overlays.add('PauseMenu');
97 : /// ```
98 8 : const GameWidget({
99 : Key? key,
100 : required this.game,
101 : this.textDirection,
102 : this.loadingBuilder,
103 : this.errorBuilder,
104 : this.backgroundBuilder,
105 : this.overlayBuilderMap,
106 : this.initialActiveOverlays,
107 8 : }) : super(key: key);
108 :
109 : /// Renders a [game] in a flutter widget tree alongside widgets overlays.
110 : ///
111 : /// To use overlays, the game subclass has to be mixed with HasWidgetsOverlay.
112 8 : @override
113 8 : _GameWidgetState<T> createState() => _GameWidgetState<T>();
114 : }
115 :
116 : class _GameWidgetState<T extends Game> extends State<GameWidget<T>> {
117 : Set<String> initialActiveOverlays = {};
118 :
119 : Future<void>? _gameLoaderFuture;
120 8 : Future<void> get _gameLoaderFutureCache =>
121 40 : _gameLoaderFuture ?? (_gameLoaderFuture = widget.game.onLoad());
122 :
123 8 : @override
124 : void initState() {
125 8 : super.initState();
126 :
127 : // Add the initial overlays
128 8 : _initActiveOverlays();
129 :
130 24 : addOverlaysListener(widget.game);
131 : }
132 :
133 8 : void _initActiveOverlays() {
134 16 : if (widget.initialActiveOverlays == null) {
135 : return;
136 : }
137 0 : _checkOverlays(widget.initialActiveOverlays!.toSet());
138 0 : widget.initialActiveOverlays!.forEach((key) {
139 0 : widget.game.overlays.add(key);
140 : });
141 : }
142 :
143 0 : @override
144 : void didUpdateWidget(GameWidget<T> oldWidget) {
145 0 : super.didUpdateWidget(oldWidget);
146 0 : if (oldWidget.game != widget.game) {
147 0 : removeOverlaysListener(oldWidget.game);
148 :
149 : // Reset the overlays
150 0 : _initActiveOverlays();
151 0 : addOverlaysListener(widget.game);
152 :
153 : // Reset the loader future
154 0 : _gameLoaderFuture = null;
155 : }
156 : }
157 :
158 8 : @override
159 : void dispose() {
160 8 : super.dispose();
161 24 : removeOverlaysListener(widget.game);
162 : }
163 :
164 : // widget overlay stuff
165 8 : void addOverlaysListener(T game) {
166 40 : widget.game.overlays.addListener(onChangeActiveOverlays);
167 40 : initialActiveOverlays = widget.game.overlays.value;
168 : }
169 :
170 8 : void removeOverlaysListener(T game) {
171 24 : game.overlays.removeListener(onChangeActiveOverlays);
172 : }
173 :
174 0 : void _checkOverlays(Set<String> overlays) {
175 0 : overlays.forEach((overlayKey) {
176 : assert(
177 0 : widget.overlayBuilderMap?.containsKey(overlayKey) ?? false,
178 0 : 'A non mapped overlay has been added: $overlayKey',
179 : );
180 : });
181 : }
182 :
183 0 : void onChangeActiveOverlays() {
184 0 : _checkOverlays(widget.game.overlays.value);
185 0 : setState(() {
186 0 : initialActiveOverlays = widget.game.overlays.value;
187 : });
188 : }
189 :
190 8 : @override
191 : Widget build(BuildContext context) {
192 24 : Widget internalGameWidget = _GameRenderObjectWidget(widget.game);
193 :
194 24 : final hasBasicDetectors = hasBasicGestureDetectors(widget.game);
195 24 : final hasAdvancedDetectors = hasAdvancedGesturesDetectors(widget.game);
196 :
197 : assert(
198 0 : !(hasBasicDetectors && hasAdvancedDetectors),
199 : '''
200 : WARNING: Both Advanced and Basic detectors detected.
201 : Advanced detectors will override basic detectors and the later will not receive events
202 : ''',
203 : );
204 :
205 : if (hasBasicDetectors) {
206 0 : internalGameWidget = applyBasicGesturesDetectors(
207 0 : widget.game,
208 : internalGameWidget,
209 : );
210 : } else if (hasAdvancedDetectors) {
211 1 : internalGameWidget = applyAdvancedGesturesDetectors(
212 2 : widget.game,
213 : internalGameWidget,
214 : );
215 : }
216 :
217 24 : if (hasMouseDetectors(widget.game)) {
218 0 : internalGameWidget = applyMouseDetectors(
219 0 : widget.game,
220 : internalGameWidget,
221 : );
222 : }
223 :
224 8 : final stackedWidgets = [internalGameWidget];
225 8 : _addBackground(context, stackedWidgets);
226 8 : _addOverlays(context, stackedWidgets);
227 :
228 : // We can use Directionality.maybeOf when that method lands on stable
229 16 : final textDir = widget.textDirection ?? TextDirection.ltr;
230 :
231 8 : return Directionality(
232 : textDirection: textDir,
233 8 : child: Container(
234 24 : color: widget.game.backgroundColor(),
235 8 : child: LayoutBuilder(
236 8 : builder: (_, BoxConstraints constraints) {
237 40 : widget.game.onResize(constraints.biggest.toVector2());
238 8 : return FutureBuilder(
239 8 : future: _gameLoaderFutureCache,
240 8 : builder: (_, snapshot) {
241 8 : if (snapshot.hasError) {
242 0 : if (widget.errorBuilder == null) {
243 0 : throw snapshot.error!;
244 : } else {
245 0 : return widget.errorBuilder!(context, snapshot.error!);
246 : }
247 : }
248 16 : if (snapshot.connectionState == ConnectionState.done) {
249 1 : return Stack(children: stackedWidgets);
250 : }
251 24 : return widget.loadingBuilder?.call(context) ?? Container();
252 : },
253 : );
254 : },
255 : ),
256 : ),
257 : );
258 : }
259 :
260 8 : List<Widget> _addBackground(BuildContext context, List<Widget> stackWidgets) {
261 16 : if (widget.backgroundBuilder == null) {
262 : return stackWidgets;
263 : }
264 0 : final backgroundContent = KeyedSubtree(
265 0 : key: ValueKey(widget.game),
266 0 : child: widget.backgroundBuilder!(context),
267 : );
268 0 : stackWidgets.insert(0, backgroundContent);
269 : return stackWidgets;
270 : }
271 :
272 8 : List<Widget> _addOverlays(BuildContext context, List<Widget> stackWidgets) {
273 16 : if (widget.overlayBuilderMap == null) {
274 : return stackWidgets;
275 : }
276 0 : final widgets = initialActiveOverlays.map((String overlayKey) {
277 0 : final builder = widget.overlayBuilderMap![overlayKey]!;
278 0 : return KeyedSubtree(
279 0 : key: ValueKey(overlayKey),
280 0 : child: builder(context, widget.game),
281 : );
282 : });
283 0 : stackWidgets.addAll(widgets);
284 : return stackWidgets;
285 : }
286 : }
287 :
288 : class _GameRenderObjectWidget extends LeafRenderObjectWidget {
289 : final Game game;
290 :
291 8 : const _GameRenderObjectWidget(this.game);
292 :
293 1 : @override
294 : RenderBox createRenderObject(BuildContext context) {
295 2 : return GameRenderBox(context, game);
296 : }
297 :
298 0 : @override
299 : void updateRenderObject(BuildContext context, GameRenderBox renderObject) {
300 0 : renderObject.game = game;
301 : }
302 : }
|