make user sheets popovers on desktop

This commit is contained in:
Henry Hiles 2026-10-15 10:25:20 -04:00
commit ce5c7b972b
Signed by: Henry-Hiles
SSH key fingerprint: SHA256:VKQUdS31Q90KvX7EkKMHMBpUspcmItAh86a+v7PGiIs
6 changed files with 122 additions and 44 deletions

View file

@ -0,0 +1,15 @@
import "package:material_ui/material_ui.dart";
extension GlobalRect on BuildContext {
/// Where the widget this context belongs to is on screen, e.g. to open a
/// menu or popover next to it.
Rect get globalRect {
final overlay = Overlay.of(this).context.findRenderObject() as RenderBox;
return switch (findRenderObject()) {
final RenderBox box =>
box.localToGlobal(.zero, ancestor: overlay) & box.size,
_ => Rect.zero,
};
}
}

View file

@ -1,4 +1,5 @@
import "package:material_ui/material_ui.dart"; import "package:material_ui/material_ui.dart";
import "package:nexus/helpers/extensions/global_rect.dart";
import "package:nexus/helpers/extensions/is_desktop.dart"; import "package:nexus/helpers/extensions/is_desktop.dart";
extension ShowAdaptiveMenu on BuildContext { extension ShowAdaptiveMenu on BuildContext {
@ -38,11 +39,7 @@ extension ShowAdaptiveMenu on BuildContext {
final overlay = Overlay.of(this).context.findRenderObject() as RenderBox; final overlay = Overlay.of(this).context.findRenderObject() as RenderBox;
final anchor = switch (globalPosition) { final anchor = switch (globalPosition) {
final position? => position & Size.zero, final position? => position & Size.zero,
null => switch (findRenderObject()) { null => globalRect,
final RenderBox box =>
box.localToGlobal(.zero, ancestor: overlay) & box.size,
_ => Rect.zero,
},
}; };
await showMenu( await showMenu(

View file

@ -0,0 +1,35 @@
import "package:material_ui/material_ui.dart";
import "package:nexus/helpers/extensions/global_rect.dart";
import "package:nexus/helpers/extensions/is_desktop.dart";
import "package:nexus/helpers/extensions/show_adaptive_sheet.dart";
import "package:nexus/widgets/popover_layout.dart";
extension ShowAdaptivePopover on BuildContext {
/// Shows the widget from [builder] in a popover next to the widget this
/// context belongs to on desktop, or a bottom sheet on mobile. Use the
/// context passed to [builder] to close it.
Future<void> showAdaptivePopover(WidgetBuilder builder) {
if (!isDesktop) return showAdaptiveSheet(builder);
final anchor = globalRect;
return showDialog(
context: this,
barrierColor: Colors.transparent,
useSafeArea: false,
builder: (context) => CustomSingleChildLayout(
delegate: PopoverLayout(anchor),
child: ConstrainedBox(
constraints: const .new(maxWidth: 420, maxHeight: 600),
child: Material(
elevation: 8,
clipBehavior: .antiAlias,
color: Theme.of(context).colorScheme.surfaceContainer,
borderRadius: .circular(16),
child: Builder(builder: builder),
),
),
),
);
}
}

View file

@ -1,5 +1,5 @@
import "package:material_ui/material_ui.dart"; import "package:material_ui/material_ui.dart";
import "package:nexus/helpers/extensions/show_adaptive_sheet.dart"; import "package:nexus/helpers/extensions/show_adaptive_popover.dart";
import "package:nexus/models/content/membership.dart"; import "package:nexus/models/content/membership.dart";
import "package:nexus/widgets/user_bottom_sheet.dart"; import "package:nexus/widgets/user_bottom_sheet.dart";
@ -8,7 +8,7 @@ extension ShowUserPopover on BuildContext {
MembershipContent member, MembershipContent member,
String userId, { String userId, {
String? roomId, String? roomId,
}) => showAdaptiveSheet( }) => showAdaptivePopover(
(context) => UserBottomSheet(member, userId, roomId: roomId), (context) => UserBottomSheet(member, userId, roomId: roomId),
); );
} }

View file

@ -106,8 +106,16 @@ class const MemberList(final String roomId, {super.key})
MembershipContent( MembershipContent(
:final avatarUrl, :final avatarUrl,
:final displayName, :final displayName,
) => ) &&
ListTile( final content =>
Builder(
builder: (context) => ListTile(
onTap: () =>
context.showUserPopover(
content,
members[index].stateKey!,
roomId: roomId,
),
title: Text( title: Text(
displayName ?? displayName ??
members[index] members[index]
@ -137,21 +145,11 @@ class const MemberList(final String roomId, {super.key})
), ),
), ),
), ),
),
_ => throw Exception( _ => throw Exception(
"Member content was not MembershipContent", "Member content was not MembershipContent",
), ),
}, },
onTap: (index) {
final member = members[index];
if (member.content
case MembershipContent content) {
context.showUserPopover(
content,
member.stateKey!,
roomId: roomId,
);
}
},
), ),
]; ];
}).toList(), }).toList(),

View file

@ -0,0 +1,33 @@
import "dart:math";
import "package:material_ui/material_ui.dart";
/// Positions a popover below [anchor], or above it if there isn't room below,
/// keeping it fully on screen.
class const PopoverLayout(final Rect anchor) extends SingleChildLayoutDelegate {
static const margin = 8.0;
@override
BoxConstraints getConstraintsForChild(BoxConstraints constraints) =>
constraints.loosen().deflate(const .all(margin));
@override
Offset getPositionForChild(Size size, Size childSize) {
final fitsBelow = anchor.bottom + childSize.height + margin <= size.height;
return .new(
anchor.left.clamp(
margin,
max(margin, size.width - childSize.width - margin),
),
(fitsBelow ? anchor.bottom : anchor.top - childSize.height).clamp(
margin,
max(margin, size.height - childSize.height - margin),
),
);
}
@override
bool shouldRelayout(PopoverLayout oldDelegate) =>
oldDelegate.anchor != anchor;
}