Skip to content

Commit

Permalink
feat(overlays): Added the 'priority' parameter for overlays (#3349)
Browse files Browse the repository at this point in the history
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).
  • Loading branch information
raspberry-jenshen authored Oct 21, 2024
1 parent b233cdb commit e591ebf
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 25 deletions.
11 changes: 9 additions & 2 deletions doc/flame/overlays.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
```

Expand All @@ -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');
},
},
);
}
Expand Down
56 changes: 47 additions & 9 deletions examples/lib/stories/system/overlays_example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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),
],
),
),
),
);
Expand All @@ -61,8 +94,13 @@ Widget _pauseMenuBuilder(BuildContext buildContext, OverlaysExample game) {
Widget overlayBuilder(DashbookContext ctx) {
return GameWidget<OverlaysExample>(
game: OverlaysExample()..paused = true,
overlayBuilderMap: const {
'PauseMenu': _pauseMenuBuilder,
overlayBuilderMap: {
'PauseMenu': (context, game) => _pauseMenuBuilder(
context,
game,
() => game.onTap(),
),
'SecondaryMenu': _secondaryMenuBuilder,
},
initialActiveOverlays: const ['PauseMenu'],
);
Expand Down
62 changes: 48 additions & 14 deletions packages/flame/lib/src/game/overlay_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,17 @@ class OverlayManager {
OverlayManager(this._game);

final Game _game;
final List<String> _activeOverlays = [];
final List<_OverlayData> _activeOverlays = [];
final Map<String, OverlayBuilderFunction> _builders = {};

/// The names of all currently active overlays.
UnmodifiableListView<String> 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() {
Expand All @@ -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);
}
Expand All @@ -40,32 +43,38 @@ class OverlayManager {
/// Marks [overlayNames] to be rendered.
void addAll(Iterable<String> 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;
}

/// 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);
}
Expand All @@ -75,7 +84,8 @@ class OverlayManager {
/// Hides multiple overlays specified in [overlayNames].
void removeAll(Iterable<String> overlayNames) {
final initialCount = _activeOverlays.length;
overlayNames.forEach(_activeOverlays.remove);
_activeOverlays
.removeWhere((overlay) => overlayNames.contains(overlay.name));
if (_activeOverlays.length != initialCount) {
_game.refreshWidget(isInternalRefresh: false);
}
Expand All @@ -84,20 +94,44 @@ class OverlayManager {
@internal
List<Widget> buildCurrentOverlayWidgets(BuildContext context) {
final widgets = <Widget>[];
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;
}
12 changes: 12 additions & 0 deletions packages/flame/test/game/overlays_manager_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down

1 comment on commit e591ebf

@gnarhard
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👏🏻

Please sign in to comment.