9dfa06ffee
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
Signed-off-by: cls_宁波本机 <908705107@qq.com>
273 lines
8.5 KiB
Dart
273 lines
8.5 KiB
Dart
import 'dart:math' as math;
|
|
import 'dart:ui' show SemanticsRole;
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import '../theme/theme.dart';
|
|
|
|
const _popoverEnterDuration = Duration(milliseconds: 150);
|
|
const _popoverExitDuration = Duration(milliseconds: 110);
|
|
const _popoverStartScale = 0.96;
|
|
|
|
/// Elevation shared by anchored menus and composer popover surfaces.
|
|
const appPopoverElevation = 8.0;
|
|
|
|
/// Returns the translucent surface color shared by app popovers.
|
|
Color appPopoverColor(BuildContext context) =>
|
|
context.colors.surface.withValues(alpha: 0.98);
|
|
|
|
/// Returns the shadow color shared by app popovers.
|
|
Color appPopoverShadowColor(BuildContext context) =>
|
|
context.colors.shadow.withValues(alpha: 0.18);
|
|
|
|
/// Returns the 20px shape and composer-matching hairline shared by popovers.
|
|
RoundedRectangleBorder appPopoverShape(BuildContext context) =>
|
|
RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(Radii.popover),
|
|
side: BorderSide(color: Colors.black.withValues(alpha: 0.04), width: 1),
|
|
);
|
|
|
|
/// The horizontal edge a popover aligns to on its triggering control.
|
|
enum AnchoredPopoverAlignment {
|
|
/// Aligns the popover's leading edge with the trigger's leading edge.
|
|
start,
|
|
|
|
/// Centers the popover horizontally on the trigger.
|
|
center,
|
|
|
|
/// Aligns the popover's trailing edge with the trigger's trailing edge.
|
|
end,
|
|
}
|
|
|
|
/// Shows an anchored, cross-platform popup menu with the Activity controls'
|
|
/// sizing, motion, and safe-area placement.
|
|
Future<T?> showAnchoredPopover<T>({
|
|
required BuildContext context,
|
|
required List<PopupMenuEntry<T>> items,
|
|
required double width,
|
|
required AnchoredPopoverAlignment alignment,
|
|
Color? color,
|
|
ShapeBorder? shape,
|
|
double elevation = appPopoverElevation,
|
|
Color? shadowColor,
|
|
Offset offset = Offset.zero,
|
|
EdgeInsetsGeometry menuPadding = EdgeInsets.zero,
|
|
Clip clipBehavior = Clip.antiAlias,
|
|
Key? surfaceKey,
|
|
}) {
|
|
final navigator = Navigator.of(context);
|
|
final overlay = navigator.overlay;
|
|
final triggerRenderObject = context.findRenderObject();
|
|
final overlayRenderObject = overlay?.context.findRenderObject();
|
|
if (triggerRenderObject is! RenderBox || overlayRenderObject is! RenderBox) {
|
|
return Future<T?>.value();
|
|
}
|
|
|
|
final triggerRect = MatrixUtils.transformRect(
|
|
triggerRenderObject.getTransformTo(overlayRenderObject),
|
|
Offset.zero & triggerRenderObject.size,
|
|
);
|
|
final overlayRect = Offset.zero & overlayRenderObject.size;
|
|
final mediaQuery = MediaQuery.of(context);
|
|
|
|
return navigator.push<T>(
|
|
_AnchoredPopoverRoute<T>(
|
|
position: RelativeRect.fromRect(triggerRect, overlayRect),
|
|
items: items,
|
|
width: width,
|
|
alignment: alignment,
|
|
offset: offset,
|
|
color: color ?? appPopoverColor(context),
|
|
shape: shape ?? appPopoverShape(context),
|
|
elevation: elevation,
|
|
shadowColor: shadowColor ?? appPopoverShadowColor(context),
|
|
menuPadding: menuPadding,
|
|
clipBehavior: clipBehavior,
|
|
surfaceKey: surfaceKey,
|
|
screenPadding: EdgeInsets.fromLTRB(
|
|
math.max(Grid.xxs, mediaQuery.padding.left),
|
|
math.max(Grid.xxs, mediaQuery.padding.top),
|
|
math.max(Grid.xxs, mediaQuery.padding.right),
|
|
math.max(Grid.xxs, mediaQuery.padding.bottom),
|
|
),
|
|
reducedMotion: mediaQuery.disableAnimations,
|
|
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
|
|
),
|
|
);
|
|
}
|
|
|
|
class _AnchoredPopoverRoute<T> extends PopupRoute<T> {
|
|
final RelativeRect position;
|
|
final List<PopupMenuEntry<T>> items;
|
|
final double width;
|
|
final AnchoredPopoverAlignment alignment;
|
|
final Offset offset;
|
|
final Color color;
|
|
final ShapeBorder shape;
|
|
final double elevation;
|
|
final Color shadowColor;
|
|
final EdgeInsetsGeometry menuPadding;
|
|
final Clip clipBehavior;
|
|
final Key? surfaceKey;
|
|
final EdgeInsets screenPadding;
|
|
final bool reducedMotion;
|
|
final String _barrierLabel;
|
|
|
|
_AnchoredPopoverRoute({
|
|
required this.position,
|
|
required this.items,
|
|
required this.width,
|
|
required this.alignment,
|
|
required this.offset,
|
|
required this.color,
|
|
required this.shape,
|
|
required this.elevation,
|
|
required this.shadowColor,
|
|
required this.menuPadding,
|
|
required this.clipBehavior,
|
|
required this.surfaceKey,
|
|
required this.screenPadding,
|
|
required this.reducedMotion,
|
|
required String barrierLabel,
|
|
}) : _barrierLabel = barrierLabel;
|
|
|
|
@override
|
|
Color? get barrierColor => null;
|
|
|
|
@override
|
|
bool get barrierDismissible => true;
|
|
|
|
@override
|
|
String? get barrierLabel => _barrierLabel;
|
|
|
|
@override
|
|
Duration get transitionDuration =>
|
|
reducedMotion ? Duration.zero : _popoverEnterDuration;
|
|
|
|
@override
|
|
Duration get reverseTransitionDuration =>
|
|
reducedMotion ? Duration.zero : _popoverExitDuration;
|
|
|
|
@override
|
|
Widget buildPage(
|
|
BuildContext context,
|
|
Animation<double> animation,
|
|
Animation<double> secondaryAnimation,
|
|
) {
|
|
final curvedAnimation = animation.drive(
|
|
CurveTween(curve: Curves.easeOutCubic),
|
|
);
|
|
final scaleAnimation = Tween<double>(
|
|
begin: _popoverStartScale,
|
|
end: 1,
|
|
).animate(curvedAnimation);
|
|
final transformOrigin = switch (alignment) {
|
|
AnchoredPopoverAlignment.start => Alignment.topLeft,
|
|
AnchoredPopoverAlignment.center => Alignment.topCenter,
|
|
AnchoredPopoverAlignment.end => Alignment.topRight,
|
|
};
|
|
|
|
return CustomSingleChildLayout(
|
|
delegate: _AnchoredPopoverLayoutDelegate(
|
|
position: position,
|
|
alignment: alignment,
|
|
offset: offset,
|
|
screenPadding: screenPadding,
|
|
),
|
|
child: FadeTransition(
|
|
key: const ValueKey('activity-popover-fade'),
|
|
opacity: curvedAnimation,
|
|
child: ScaleTransition(
|
|
key: const ValueKey('activity-popover-scale'),
|
|
scale: scaleAnimation,
|
|
alignment: transformOrigin,
|
|
child: Material(
|
|
key: surfaceKey,
|
|
type: MaterialType.card,
|
|
color: color,
|
|
surfaceTintColor: Colors.transparent,
|
|
elevation: elevation,
|
|
shadowColor: shadowColor,
|
|
shape: shape,
|
|
clipBehavior: clipBehavior,
|
|
child: SizedBox(
|
|
width: width,
|
|
child: Semantics(
|
|
role: SemanticsRole.menu,
|
|
scopesRoute: true,
|
|
namesRoute: true,
|
|
explicitChildNodes: true,
|
|
child: SingleChildScrollView(
|
|
padding: menuPadding,
|
|
child: ListBody(children: items),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _AnchoredPopoverLayoutDelegate extends SingleChildLayoutDelegate {
|
|
final RelativeRect position;
|
|
final AnchoredPopoverAlignment alignment;
|
|
final Offset offset;
|
|
final EdgeInsets screenPadding;
|
|
|
|
const _AnchoredPopoverLayoutDelegate({
|
|
required this.position,
|
|
required this.alignment,
|
|
required this.offset,
|
|
required this.screenPadding,
|
|
});
|
|
|
|
@override
|
|
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
|
|
return BoxConstraints.loose(
|
|
Size(
|
|
constraints.maxWidth - screenPadding.horizontal,
|
|
constraints.maxHeight - screenPadding.vertical,
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Offset getPositionForChild(Size size, Size childSize) {
|
|
final anchorBottom = size.height - position.bottom;
|
|
final desiredX = switch (alignment) {
|
|
AnchoredPopoverAlignment.start => position.left + offset.dx,
|
|
AnchoredPopoverAlignment.center =>
|
|
position.left +
|
|
(size.width - position.left - position.right - childSize.width) /
|
|
2 +
|
|
offset.dx,
|
|
AnchoredPopoverAlignment.end =>
|
|
size.width - position.right - childSize.width + offset.dx,
|
|
};
|
|
final minX = screenPadding.left;
|
|
final maxX = size.width - screenPadding.right - childSize.width;
|
|
final x = desiredX.clamp(minX, maxX).toDouble();
|
|
|
|
final belowY = anchorBottom + offset.dy;
|
|
final aboveY = position.top - childSize.height - offset.dy;
|
|
final maxY = size.height - screenPadding.bottom - childSize.height;
|
|
final desiredY =
|
|
belowY + childSize.height <= size.height - screenPadding.bottom
|
|
? belowY
|
|
: aboveY;
|
|
final y = desiredY.clamp(screenPadding.top, maxY).toDouble();
|
|
|
|
return Offset(x, y);
|
|
}
|
|
|
|
@override
|
|
bool shouldRelayout(_AnchoredPopoverLayoutDelegate oldDelegate) {
|
|
return position != oldDelegate.position ||
|
|
alignment != oldDelegate.alignment ||
|
|
offset != oldDelegate.offset ||
|
|
screenPadding != oldDelegate.screenPadding;
|
|
}
|
|
}
|