From e591ebf8a320ff3d55b9ae9e50390bf2ab5a8919 Mon Sep 17 00:00:00 2001 From: Raspberry Jenshen Date: Mon, 21 Oct 2024 16:54:28 +0100 Subject: [PATCH] feat(overlays): Added the 'priority' parameter for overlays (#3349) To sort overlays we can introduce a priority. Let's say my app contains several menus that can be displayed simultaneously, but I want my main menu displayed at first in the stack (at the bottom). --- doc/flame/overlays.md | 11 +++- .../lib/stories/system/overlays_example.dart | 56 ++++++++++++++--- .../flame/lib/src/game/overlay_manager.dart | 62 ++++++++++++++----- .../test/game/overlays_manager_test.dart | 12 ++++ 4 files changed, 116 insertions(+), 25 deletions(-) diff --git a/doc/flame/overlays.md b/doc/flame/overlays.md index 7a96d4f29a6..ad6a01925cd 100644 --- a/doc/flame/overlays.md +++ b/doc/flame/overlays.md @@ -16,10 +16,14 @@ by providing an `overlayBuilderMap`. ```dart // Inside your game: final pauseOverlayIdentifier = 'PauseMenu'; + final secondaryOverlayIdentifier = 'SecondaryMenu'; - // Marks 'PauseMenu' to be rendered. + // Marks 'SecondaryMenu' to be rendered. + overlays.add(secondaryOverlayIdentifier, priority: 1); + // Marks 'PauseMenu' to be rendered. Priority = 0 by default + // which means the 'PauseMenu' will be displayed under the 'SecondaryMenu' overlays.add(pauseOverlayIdentifier); - // Marks 'PauseMenu' to not be rendered. + // Marks 'PauseMenu' to not be rendered. overlays.remove(pauseOverlayIdentifier); ``` @@ -34,6 +38,9 @@ Widget build(BuildContext context) { 'PauseMenu': (BuildContext context, MyGame game) { return Text('A pause menu'); }, + 'SecondaryMenu': (BuildContext context, MyGame game) { + return Text('A secondary menu'); + }, }, ); } diff --git a/examples/lib/stories/system/overlays_example.dart b/examples/lib/stories/system/overlays_example.dart index 5c620e13092..ab4b1453d52 100644 --- a/examples/lib/stories/system/overlays_example.dart +++ b/examples/lib/stories/system/overlays_example.dart @@ -31,6 +31,9 @@ class OverlaysExample extends FlameGame with TapDetector { ..anchor = Anchor.center ..size = Vector2.all(100), ); + + // 'SecondaryMenu' will be displayed above 'PauseMenu' + overlays.add('SecondaryMenu', priority: 1); } @override @@ -45,14 +48,44 @@ class OverlaysExample extends FlameGame with TapDetector { } } -Widget _pauseMenuBuilder(BuildContext buildContext, OverlaysExample game) { +Widget _pauseMenuBuilder( + BuildContext buildContext, + OverlaysExample game, + GestureTapCallback? onTap, +) { return Center( - child: Container( - width: 100, - height: 100, - color: Colors.orange, - child: const Center( - child: Text('Paused'), + child: GestureDetector( + onTap: onTap, + child: Container( + width: 100, + height: 100, + color: Colors.orange, + child: const Center( + child: Text('Paused'), + ), + ), + ), + ); +} + +Widget _secondaryMenuBuilder(BuildContext buildContext, OverlaysExample game) { + return Align( + alignment: Alignment.bottomRight, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Container( + width: 100, + height: 50, + alignment: Alignment.center, + color: Colors.red, + child: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.music_off_rounded), + Icon(Icons.info), + Icon(Icons.star), + ], + ), ), ), ); @@ -61,8 +94,13 @@ Widget _pauseMenuBuilder(BuildContext buildContext, OverlaysExample game) { Widget overlayBuilder(DashbookContext ctx) { return GameWidget( game: OverlaysExample()..paused = true, - overlayBuilderMap: const { - 'PauseMenu': _pauseMenuBuilder, + overlayBuilderMap: { + 'PauseMenu': (context, game) => _pauseMenuBuilder( + context, + game, + () => game.onTap(), + ), + 'SecondaryMenu': _secondaryMenuBuilder, }, initialActiveOverlays: const ['PauseMenu'], ); diff --git a/packages/flame/lib/src/game/overlay_manager.dart b/packages/flame/lib/src/game/overlay_manager.dart index e7f723a17b4..66f27dd2f9d 100644 --- a/packages/flame/lib/src/game/overlay_manager.dart +++ b/packages/flame/lib/src/game/overlay_manager.dart @@ -11,16 +11,17 @@ class OverlayManager { OverlayManager(this._game); final Game _game; - final List _activeOverlays = []; + final List<_OverlayData> _activeOverlays = []; final Map _builders = {}; /// The names of all currently active overlays. UnmodifiableListView get activeOverlays { - return UnmodifiableListView(_activeOverlays); + return UnmodifiableListView(_activeOverlays.map((overlay) => overlay.name)); } /// Returns if the given [overlayName] is active - bool isActive(String overlayName) => _activeOverlays.contains(overlayName); + bool isActive(String overlayName) => + _activeOverlays.any((overlay) => overlay.name == overlayName); /// Clears all active overlays. void clear() { @@ -29,8 +30,10 @@ class OverlayManager { } /// Marks the [overlayName] to be rendered. - bool add(String overlayName) { - final setChanged = _addImpl(overlayName); + /// [priority] is used to sort widgets for [buildCurrentOverlayWidgets] + /// The smaller the priority, the sooner your component will be build. + bool add(String overlayName, {int priority = 0}) { + final setChanged = _addImpl(priority: priority, name: overlayName); if (setChanged) { _game.refreshWidget(isInternalRefresh: false); } @@ -40,24 +43,29 @@ class OverlayManager { /// Marks [overlayNames] to be rendered. void addAll(Iterable overlayNames) { final initialCount = _activeOverlays.length; - overlayNames.forEach(_addImpl); + overlayNames.forEach((overlayName) => _addImpl(name: overlayName)); if (initialCount != _activeOverlays.length) { _game.refreshWidget(isInternalRefresh: false); } } - bool _addImpl(String name) { + bool _addImpl({required String name, int priority = 0}) { assert( _builders.containsKey(name), 'Trying to add an unknown overlay "$name"', ); - if (_activeOverlays.contains(name)) { + if (isActive(name)) { return false; } - _activeOverlays.add(name); + _activeOverlays.add(_OverlayData(priority: priority, name: name)); + _activeOverlays.sort(_compare); return true; } + _OverlayData? _getOverlay(String name) { + return _activeOverlays.where((overlay) => overlay.name == name).firstOrNull; + } + /// Adds a named overlay builder void addEntry(String name, OverlayBuilderFunction builder) { _builders[name] = builder; @@ -65,7 +73,8 @@ class OverlayManager { /// Hides the [overlayName]. bool remove(String overlayName) { - final hasRemoved = _activeOverlays.remove(overlayName); + final overlay = _getOverlay(overlayName); + final hasRemoved = _activeOverlays.remove(overlay); if (hasRemoved) { _game.refreshWidget(isInternalRefresh: false); } @@ -75,7 +84,8 @@ class OverlayManager { /// Hides multiple overlays specified in [overlayNames]. void removeAll(Iterable overlayNames) { final initialCount = _activeOverlays.length; - overlayNames.forEach(_activeOverlays.remove); + _activeOverlays + .removeWhere((overlay) => overlayNames.contains(overlay.name)); if (_activeOverlays.length != initialCount) { _game.refreshWidget(isInternalRefresh: false); } @@ -84,20 +94,44 @@ class OverlayManager { @internal List buildCurrentOverlayWidgets(BuildContext context) { final widgets = []; - for (final overlayName in _activeOverlays) { - final builder = _builders[overlayName]!; + for (final overlay in _activeOverlays) { + final builder = _builders[overlay.name]!; widgets.add( KeyedSubtree( - key: ValueKey(overlayName), + key: ValueKey(overlay), child: builder(context, _game), ), ); } return widgets; } + + /// Comparator function used to sort overlays. + int _compare(_OverlayData a, _OverlayData b) { + return a.priority - b.priority; + } } typedef OverlayBuilderFunction = Widget Function( BuildContext context, Game game, ); + +@immutable +class _OverlayData { + final int priority; + final String name; + + const _OverlayData({required this.priority, required this.name}); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is _OverlayData && + runtimeType == other.runtimeType && + priority == other.priority && + name == other.name; + + @override + int get hashCode => priority.hashCode ^ name.hashCode; +} diff --git a/packages/flame/test/game/overlays_manager_test.dart b/packages/flame/test/game/overlays_manager_test.dart index b2ec4f2215c..425b3f4edd4 100644 --- a/packages/flame/test/game/overlays_manager_test.dart +++ b/packages/flame/test/game/overlays_manager_test.dart @@ -109,6 +109,18 @@ void main() { expect(overlays.activeOverlays.length, 2); }); + test('can add multiple overlays with priorities', () { + final overlays = FlameGame().overlays + ..addEntry('test1', (ctx, game) => Container()) + ..addEntry('test2', (ctx, game) => Container()); + overlays.add('test1', priority: 1); + overlays.add('test2'); + expect(overlays.activeOverlays, ['test2', 'test1']); + expect(overlays.isActive('test1'), true); + expect(overlays.isActive('test2'), true); + expect(overlays.activeOverlays.length, 2); + }); + test('cannot add an unknown overlay', () { final overlays = FlameGame().overlays; expect(