Files
ImBenjiandClaude Opus 5.5 b269201919 The Garage SDKs, in the open
garage_auth, garage_entitlements, garage_iap and garage_ui, moved out of
Garage-Services and Metro-Map-Maker into one public repo. MIT, one readme,
docs under docs/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013F4NWNvYcdeSgqbWMT1VQ7
2026-09-23 18:49:21 +01:00

236 lines
6.6 KiB
Dart

import "package:flutter/widgets.dart";
sealed class AppMenuItem {
const AppMenuItem();
}
class AppMenuGroup extends AppMenuItem {
const AppMenuGroup({
required this.label,
required this.children,
this.icon,
this.visible = true,
});
final String label;
final IconData? icon;
final List<AppMenuItem> children;
final bool visible;
List<AppMenuItem> get visibleChildren => children.where((item) {
return switch (item) {
AppMenuGroup group => group.visible,
AppMenuAction action => action.visible,
_ => true,
};
}).toList();
}
class AppMenuAction extends AppMenuItem {
const AppMenuAction({
required this.label,
this.icon,
this.trailingIcon,
this.onTap,
this.shortcut,
this.visible = true,
});
final String label;
final IconData? icon;
final IconData? trailingIcon;
final VoidCallback? onTap;
final SingleActivator? shortcut;
final bool visible;
bool get enabled => onTap != null;
}
class AppMenuCheck extends AppMenuItem {
const AppMenuCheck({
required this.label,
required this.checked,
this.checkedLabel,
this.onToggle,
this.shortcut,
});
final String label;
final String? checkedLabel;
final bool checked;
final VoidCallback? onToggle;
final SingleActivator? shortcut;
}
class AppMenuPlatformProvided extends AppMenuItem {
const AppMenuPlatformProvided(this.type);
final PlatformProvidedMenuItemType type;
}
class AppMenuSeparator extends AppMenuItem {
const AppMenuSeparator({this.visible = true});
final bool visible;
}
/// Converts the shared menu model to Flutter's native platform menu model.
class AppMenuNativeRenderer {
static List<PlatformMenuItem> build(
List<AppMenuGroup> menus, {
String appName = "Garage App",
}) {
final appItems = <PlatformMenuItem>[
for (final type in [
PlatformProvidedMenuItemType.about,
PlatformProvidedMenuItemType.servicesSubmenu,
PlatformProvidedMenuItemType.hide,
PlatformProvidedMenuItemType.hideOtherApplications,
PlatformProvidedMenuItemType.showAllApplications,
PlatformProvidedMenuItemType.quit,
])
if (PlatformProvidedMenuItem.hasMenu(type))
PlatformProvidedMenuItem(type: type),
];
return [
if (appItems.isNotEmpty) PlatformMenu(label: appName, menus: appItems),
for (final group in menus)
if (group.visible)
PlatformMenu(
label: group.label,
menus: _convertItems(group.visibleChildren),
),
];
}
static String signature(List<AppMenuGroup> menus) {
final buffer = StringBuffer();
for (final group in menus) {
if (!group.visible) continue;
_writeSignature(buffer, group);
buffer.write(";");
}
return buffer.toString();
}
static List<PlatformMenuItem> _convertItems(List<AppMenuItem> items) {
final groups = <List<AppMenuItem>>[<AppMenuItem>[]];
for (final item in items) {
if (item case AppMenuSeparator(visible: true)) {
groups.add(<AppMenuItem>[]);
} else if (item is! AppMenuSeparator) {
groups.last.add(item);
}
}
final result = <PlatformMenuItem>[];
for (var index = 0; index < groups.length; index++) {
final native = groups[index]
.map(_toNativeItem)
.whereType<PlatformMenuItem>();
final items = native.toList();
if (items.isEmpty) continue;
if (index == 0) {
result.addAll(items);
} else {
result.add(PlatformMenuItemGroup(members: items));
}
}
return result;
}
static PlatformMenuItem? _toNativeItem(AppMenuItem item) {
return switch (item) {
AppMenuGroup group when group.visible => PlatformMenu(
label: group.label,
menus: _convertItems(group.visibleChildren),
),
AppMenuAction action when action.visible => PlatformMenuItem(
label: action.label,
shortcut: action.shortcut,
onSelected: action.onTap,
),
AppMenuCheck check => PlatformMenuItem(
label: check.checked && check.checkedLabel != null
? check.checkedLabel!
: check.label,
shortcut: check.shortcut,
onSelected: check.onToggle,
),
AppMenuPlatformProvided provided
when PlatformProvidedMenuItem.hasMenu(provided.type) =>
PlatformProvidedMenuItem(type: provided.type),
_ => null,
};
}
static void _writeSignature(StringBuffer buffer, AppMenuItem item) {
switch (item) {
case AppMenuGroup group:
if (!group.visible) return;
buffer
..write("G(")
..write(group.label)
..write("|")
..write(group.icon?.codePoint ?? -1)
..write(")[");
for (final child in group.visibleChildren) {
_writeSignature(buffer, child);
buffer.write(",");
}
buffer.write("]");
case AppMenuAction action:
if (!action.visible) return;
buffer
..write("A(")
..write(action.label)
..write("|")
..write(action.icon?.codePoint ?? -1)
..write("|")
..write(action.enabled ? 1 : 0)
..write("|")
..write(_shortcutSignature(action.shortcut))
..write(")");
case AppMenuCheck check:
buffer
..write("C(")
..write(check.label)
..write("|")
..write(check.checkedLabel ?? "-")
..write("|")
..write(check.checked ? 1 : 0)
..write("|")
..write(check.onToggle != null ? 1 : 0)
..write("|")
..write(_shortcutSignature(check.shortcut))
..write(")");
case AppMenuPlatformProvided provided:
buffer
..write("P(")
..write(provided.type.name)
..write(")");
case AppMenuSeparator separator:
if (separator.visible) buffer.write("S");
}
}
// SingleActivator has no value-based toString (it's Diagnosticable, so the
// default one embeds the object's identity hash) - and menu_def.dart builds
// a fresh activator on every rebuild, so hashing the instance itself would
// make the signature change every frame and defeat the whole point of this
// gate. build it off the actual key/modifier fields instead, those are what
// we care about being stable.
static String _shortcutSignature(SingleActivator? shortcut) {
if (shortcut == null) return "-";
return [
shortcut.trigger.keyLabel,
shortcut.trigger.keyId.toRadixString(16),
shortcut.alt ? "a" : "",
shortcut.control ? "c" : "",
shortcut.meta ? "m" : "",
shortcut.shift ? "s" : "",
].join(":");
}
}