Files
FlClash/lib/ui/pop_scope.dart
T
chen08209 ba15c313b4 feat(ui): add Liquid Glass and slide-to-pick popup menus
Changelog: Optional two-tier Liquid Glass and slide-to-pick menus
2026-10-17 09:54:29 +08:00

145 lines
3.4 KiB
Dart

import 'dart:async';
import 'package:flutter/widgets.dart';
import 'inherited.dart';
class CommonPopScope extends StatelessWidget {
final Widget child;
final FutureOr<bool> Function(BuildContext context)? onPop;
final FutureOr<void> Function()? onPopSuccess;
const CommonPopScope({
super.key,
required this.child,
this.onPop,
this.onPopSuccess,
});
@override
Widget build(BuildContext context) {
final route = ModalRoute.of(context);
final hasBackLayer = route?.willHandlePopInternally == true;
return PopScope(
canPop: onPop == null || hasBackLayer,
onPopInvokedWithResult: onPop == null
? null
: (didPop, _) async {
if (didPop) {
return;
}
// BackLayerScope adds its entry a frame after this builds, which
// rebuilds nothing here on a route with another below it.
if (route?.willHandlePopInternally == true) {
Navigator.of(context).pop();
return;
}
final res = await onPop!(context);
if (!context.mounted) {
return;
}
if (!res) {
return;
}
Navigator.of(context).pop();
if (onPopSuccess != null) {
await onPopSuccess!();
}
},
child: child,
);
}
}
class BackLayerScope extends StatefulWidget {
final Widget child;
final VoidCallback onBack;
@visibleForTesting
final void Function(void Function(Duration) callback)?
schedulePostFrameCallback;
const BackLayerScope({
super.key,
required this.onBack,
required this.child,
@visibleForTesting this.schedulePostFrameCallback,
});
@override
State<BackLayerScope> createState() => _BackLayerScopeState();
}
class _BackLayerScopeState extends State<BackLayerScope> {
ModalRoute<dynamic>? _route;
LocalHistoryEntry? _entry;
bool _isDetaching = false;
bool _isPageActive = true;
int _syncRevision = 0;
@override
void didChangeDependencies() {
super.didChangeDependencies();
final route = ModalRoute.of(context);
final isPageActive = PageActivityScope.isActiveOf(context);
if (identical(_route, route) && _isPageActive == isPageActive) {
return;
}
_detach();
_route = route;
_isPageActive = isPageActive;
final revision = ++_syncRevision;
final schedulePostFrameCallback =
widget.schedulePostFrameCallback ??
WidgetsBinding.instance.addPostFrameCallback;
schedulePostFrameCallback((_) {
if (!mounted || revision != _syncRevision) {
return;
}
if (!_isPageActive) {
widget.onBack();
return;
}
if (route == null) {
return;
}
final entry = LocalHistoryEntry(
impliesAppBarDismissal: false,
onRemove: _handleRemove,
);
_entry = entry;
route.addLocalHistoryEntry(entry);
});
}
void _handleRemove() {
_entry = null;
if (!_isDetaching && mounted) {
widget.onBack();
}
}
void _detach() {
final entry = _entry;
if (entry == null) {
return;
}
_entry = null;
_isDetaching = true;
entry.remove();
_isDetaching = false;
}
@override
void dispose() {
_syncRevision++;
_detach();
_route = null;
super.dispose();
}
@override
Widget build(BuildContext context) {
return widget.child;
}
}