desktopCall function
- CallController c,
- BuildContext context
Returns a desktop design of a CallView.
Implementation
Widget desktopCall(CallController c, BuildContext context) {
final style = Theme.of(context).style;
return LayoutBuilder(
builder: (context, constraints) {
// Call stackable content.
List<Widget> content = [
const SvgImage.asset(
'assets/images/background_dark.svg',
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,
),
];
// Secondary view possible alignment.
Widget possibleContainer() {
return Obx(() {
Alignment? alignment = c.possibleSecondaryAlignment.value;
if (alignment == null) {
return Container();
}
double width = 10;
double height = 10;
if (alignment == Alignment.topCenter ||
alignment == Alignment.bottomCenter) {
width = double.infinity;
} else {
height = double.infinity;
}
return Align(
alignment: alignment,
child: ConditionalBackdropFilter(
child: Container(
height: height,
width: width,
color:
ConditionalBackdropFilter.enabled
? style.colors.primaryAuxiliaryOpacity25
: style.colors.primaryAuxiliaryOpacity55,
),
),
);
});
}
content.addAll([
// Call's primary view.
Column(
children: [
Obx(
() => SizedBox(
width: double.infinity,
height:
c.secondary.isNotEmpty &&
c.secondaryAlignment.value == Alignment.topCenter
? c.secondaryHeight.value
: 0,
),
),
Expanded(
child: Row(
children: [
Obx(
() => SizedBox(
height: double.infinity,
width:
c.secondary.isNotEmpty &&
c.secondaryAlignment.value ==
Alignment.centerLeft
? c.secondaryWidth.value
: 0,
),
),
Expanded(
child: Stack(
children: [
Obx(() {
final bool isOutgoing =
(c.outgoing ||
c.state.value == OngoingCallState.local) &&
!c.started;
final bool isIncoming =
c.state.value != OngoingCallState.active &&
c.state.value != OngoingCallState.joining &&
!isOutgoing;
final Widget child;
if (!isIncoming) {
child = _primaryView(c);
} else {
if (c.isDialog) {
final RxUser? user =
c.chat.value?.members.values
.firstWhereOrNull(
(e) => e.user.id != c.me.id.userId,
)
?.user;
child = CallCoverWidget(
c.chat.value?.callCover,
user: user,
);
} else {
if (c.chat.value?.avatar.value != null) {
final Avatar avatar =
c.chat.value!.avatar.value!;
child = CallCoverWidget(
UserCallCover(
full: avatar.full,
original: avatar.original,
square: avatar.full,
vertical: avatar.full,
),
);
} else {
child = CallCoverWidget(
null,
chat: c.chat.value,
);
}
}
}
return SafeAnimatedSwitcher(
duration: 400.milliseconds,
child: child,
);
}),
],
),
),
Obx(
() => SizedBox(
height: double.infinity,
width:
c.secondary.isNotEmpty &&
c.secondaryAlignment.value ==
Alignment.centerRight
? c.secondaryWidth.value
: 0,
),
),
],
),
),
Obx(
() => SizedBox(
width: double.infinity,
height:
c.secondary.isNotEmpty &&
c.secondaryAlignment.value == Alignment.bottomCenter
? c.secondaryHeight.value
: 0,
),
),
],
),
// Reconnection indicator.
Obx(() {
final Widget child = IgnorePointer(
child: Container(
width: double.infinity,
height: double.infinity,
color: style.colors.onBackgroundOpacity70,
padding: const EdgeInsets.all(21.0),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (!Config.disableInfiniteAnimations)
const DoubleBounceLoadingIndicator(),
const SizedBox(height: 16),
Text(
'label_reconnecting_ellipsis'.l10n,
style: style.fonts.normal.regular.onPrimary,
),
],
),
),
),
);
return AnimatedOpacity(
opacity: c.connectionLost.isTrue ? 1 : 0,
duration: 200.milliseconds,
child: child,
);
}),
possibleContainer(),
// Makes UI appear on click.
Listener(
behavior: HitTestBehavior.translucent,
onPointerDown: (d) {
c.downPosition = d.localPosition;
c.downButtons = d.buttons;
},
onPointerUp: (d) {
if (c.downButtons & kPrimaryButton != 0 &&
(d.localPosition.distanceSquared -
c.downPosition.distanceSquared)
.abs() <=
1500) {
if (c.primaryDrags.value == 0 && c.secondaryDrags.value == 0) {
if (c.state.value == OngoingCallState.active) {
if (!c.showUi.value) {
c.keepUi();
} else {
c.keepUi(false);
}
}
}
}
},
),
// Empty drop zone if [secondary] is empty.
Obx(() {
final Axis secondaryAxis =
c.size.width >= c.size.height ? Axis.horizontal : Axis.vertical;
/// Pre-calculate the [ReorderableFit]'s size.
final double panelSize = max(
ReorderableFit.calculateSize(
maxSize: c.size.shortestSide / 4,
constraints: Size(c.size.width, c.size.height - 45),
axis:
c.size.width >= c.size.height
? Axis.horizontal
: Axis.vertical,
length: c.secondary.length,
),
130,
);
return SafeAnimatedSwitcher(
key: const Key('SecondaryTargetAnimatedSwitcher'),
duration: 200.milliseconds,
child:
c.secondary.isEmpty && c.doughDraggedRenderer.value != null
? DropBoxArea<_DragData>(
size: panelSize,
axis: secondaryAxis,
visible: c.primaryDrags.value >= 1,
onWillAccept: (d) => d?.chatId == c.chatId.value,
onAccept: (_DragData d) {
if (secondaryAxis == Axis.horizontal) {
c.secondaryAlignment.value = Alignment.centerRight;
} else {
c.secondaryAlignment.value = Alignment.topCenter;
}
c.unfocus(d.participant);
},
)
: const SizedBox(),
);
}),
]);
// Builds the [Dock] containing the [CallController.buttons].
Widget dock() {
return Obx(() {
final bool isOutgoing =
(c.outgoing || c.state.value == OngoingCallState.local) &&
!c.started;
final bool showBottomUi =
(c.showUi.isTrue ||
c.draggedButton.value != null ||
c.state.value != OngoingCallState.active ||
(c.state.value == OngoingCallState.active &&
c.locals.isEmpty &&
c.remotes.isEmpty &&
c.focused.isEmpty &&
c.paneled.isEmpty));
final bool answer =
c.state.value != OngoingCallState.joining &&
c.state.value != OngoingCallState.active &&
!isOutgoing;
final Widget child;
if (answer) {
child = Row(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(width: 11),
AcceptAudioButton(
c,
highlight: !c.withVideo,
).build(hinted: false),
const SizedBox(width: 24),
AcceptVideoButton(
c,
highlight: c.withVideo,
).build(hinted: false),
const SizedBox(width: 24),
DeclineButton(c).build(hinted: false),
const SizedBox(width: 11),
],
);
} else {
child = Dock<CallButton>(
items: c.buttons,
itemWidth: CallController.buttonSize,
itemBuilder:
(e) => e.build(hinted: c.draggedButton.value == null),
onReorder: (buttons) {
c.buttons.value = buttons;
c.relocateSecondary();
},
onDragStarted: (b) {
c.showDragAndDropButtonsHint = false;
c.draggedButton.value = b;
},
onDragEnded: (_) => c.draggedButton.value = null,
onLeave: (_) => c.displayMore.value = true,
onWillAccept: (d) => d?.c == c,
);
}
return DockDecorator(
show: showBottomUi,
dockKey: c.dockKey,
onAnimation:
() => Future.delayed(Duration.zero, c.relocateSecondary),
onEnter: (d) => c.keepUi(true),
onHover: (d) => c.keepUi(true),
onExit:
c.showUi.value && !c.displayMore.value
? (d) => c.keepUi(false)
: (d) => c.keepUi(),
child: child,
);
});
}
// Builds the [Launchpad] panel containing the [CallController.panel].
Widget launchpad() {
return Obx(() {
bool enabled =
c.displayMore.isTrue &&
c.primaryDrags.value == 0 &&
c.secondaryDrags.value == 0;
return Flexible(
child: AnimatedOpacity(
duration: const Duration(milliseconds: 150),
opacity: c.displayMore.value ? 1 : 0,
child: IgnorePointer(
ignoring: !c.displayMore.value,
child: Launchpad(
onEnter: enabled ? (d) => c.keepUi(true) : null,
onHover: enabled ? (d) => c.keepUi(true) : null,
onExit: enabled ? (d) => c.keepUi() : null,
onAccept: (CallButton data) {
c.buttons.remove(data);
c.draggedButton.value = null;
},
onWillAccept:
(CallButton? a) => a?.c == c && a?.isRemovable == true,
children:
c.panel.map((e) {
return DelayedDraggable(
feedback: Transform.translate(
offset: const Offset(
CallController.buttonSize / 2 * -1,
CallController.buttonSize / 2 * -1,
),
child: e.build(),
),
data: e,
onDragStarted: () {
c.showDragAndDropButtonsHint = false;
c.draggedButton.value = e;
},
onDragCompleted: () => c.draggedButton.value = null,
onDragEnd: (_) => c.draggedButton.value = null,
onDraggableCanceled:
(_, __) => c.draggedButton.value = null,
maxSimultaneousDrags: e.isRemovable ? null : 0,
dragAnchorStrategy: pointerDragAnchorStrategy,
child: e.build(
hinted: false,
big: true,
expanded: true,
),
);
}).toList(),
),
),
),
);
});
}
// Footer part of the call with buttons.
List<Widget> footer = [
// Animated bottom buttons.
Align(
alignment: Alignment.bottomCenter,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Flexible(
child: Column(
mainAxisSize: MainAxisSize.min,
verticalDirection: VerticalDirection.up,
children: [dock(), launchpad()],
),
),
],
),
),
// Display the more hint, if not dismissed.
Obx(() {
return SafeAnimatedSwitcher(
duration: 150.milliseconds,
child:
c.showDragAndDropButtonsHint && c.displayMore.value
? Align(
alignment: Alignment.bottomCenter,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedDelayedSwitcher(
delay: const Duration(milliseconds: 500),
duration: const Duration(milliseconds: 200),
child: Align(
alignment: Alignment.topCenter,
child: Container(
width: 290,
padding: EdgeInsets.only(
top:
10 +
(WebUtils.isPopup
? 0
: CallController.titleHeight),
),
child: HintWidget(
text: 'label_hint_drag_n_drop_buttons'.l10n,
onTap:
() =>
c.showDragAndDropButtonsHint = false,
),
),
),
),
const Flexible(child: SizedBox(height: 420)),
],
),
)
: Container(),
);
}),
];
List<Widget> ui = [
Obx(() {
bool preferTitle = c.state.value != OngoingCallState.active;
return GestureDetector(
behavior: HitTestBehavior.translucent,
onDoubleTap: c.toggleFullscreen,
onPanUpdate:
preferTitle
? (d) {
c.left.value = c.left.value + d.delta.dx;
c.top.value = c.top.value + d.delta.dy;
c.applyConstraints(context);
}
: null,
);
}),
// Sliding from the top title bar.
Obx(() {
final bool isOutgoing =
(c.outgoing || c.state.value == OngoingCallState.local) &&
!c.started;
final bool preferTitle =
c.state.value != OngoingCallState.active && !isOutgoing;
return SafeAnimatedSwitcher(
key: const Key('AnimatedSwitcherCallTitle'),
duration: const Duration(milliseconds: 200),
child:
preferTitle
? Align(
key: const Key('CallTitlePadding'),
alignment: Alignment.topCenter,
child: Padding(
padding: EdgeInsets.only(
left: 10,
right: 10,
top: c.size.height * 0.05,
),
child: callTitle(c),
),
)
: Container(key: UniqueKey()),
);
}),
// Bottom [MouseRegion] that toggles UI on hover.
Obx(() {
final bool enabled =
c.primaryDrags.value == 0 && c.secondaryDrags.value == 0;
return Align(
alignment: Alignment.bottomCenter,
child: SizedBox(
height: 100,
width: double.infinity,
child: MouseRegion(
opaque: false,
onEnter: enabled ? (d) => c.keepUi(true) : null,
onHover: enabled ? (d) => c.keepUi(true) : null,
onExit:
c.showUi.value && enabled
? (d) {
if (c.displayMore.isTrue) {
c.keepUi();
} else {
c.keepUi(false);
}
}
: null,
),
),
);
}),
// Secondary panel itself.
Obx(() {
final bool isIncoming =
c.state.value != OngoingCallState.active &&
c.state.value != OngoingCallState.joining &&
!(c.outgoing || c.state.value == OngoingCallState.local);
if (isIncoming) {
return const SizedBox();
}
return LayoutBuilder(
builder: (_, constraints) {
// Scale the secondary panel after this frame is displayed, as
// otherwise it invokes re-drawing twice in a frame, resulting in an
// error.
WidgetsBinding.instance.addPostFrameCallback((_) {
c.scaleSecondary(constraints);
WidgetsBinding.instance.addPostFrameCallback(
(_) => c.relocateSecondary(),
);
});
return _secondaryView(c, context);
},
);
}),
// [MouseRegion] changing the cursor.
Obx(() {
return MouseRegion(
opaque: false,
cursor:
c.draggedRenderer.value != null ||
c.doughDraggedRenderer.value != null
? CustomMouseCursors.grabbing
: c.isCursorHidden.value
? SystemMouseCursors.none
: c.hoveredRenderer.value != null
? CustomMouseCursors.grab
: c.hoveredParticipant.value != null
? SystemMouseCursors.basic
: MouseCursor.defer,
);
}),
// Top [MouseRegion] that toggles info header on hover.
Align(
alignment: Alignment.topCenter,
child: SizedBox(
height: 100,
width: double.infinity,
child: MouseRegion(
opaque: false,
onEnter: (_) {
c.showHeader.value = true;
c.isCursorHidden.value = false;
},
onHover: (_) {
c.showHeader.value = true;
c.isCursorHidden.value = false;
},
onExit: (_) {
c.showHeader.value = false;
},
),
),
),
// Show a hint if any renderer is draggable.
Obx(() {
final bool hideSecondary = c.size.width < 500 && c.size.height < 500;
final bool mayDragVideo =
!hideSecondary &&
(c.focused.length > 1 ||
(c.focused.isEmpty &&
c.primary.length + c.secondary.length > 1));
return SafeAnimatedSwitcher(
duration: 150.milliseconds,
child:
c.showDragAndDropVideosHint && mayDragVideo
? Padding(
padding: EdgeInsets.only(
top:
c.secondary.isNotEmpty &&
c.secondaryAlignment.value ==
Alignment.topCenter
? 10 + c.secondaryHeight.value
: 10,
right:
c.secondary.isNotEmpty &&
c.secondaryAlignment.value ==
Alignment.centerRight
? 10 + c.secondaryWidth.value
: 10,
),
child: Align(
alignment: Alignment.topRight,
child: SizedBox(
width: 320,
child: HintWidget(
text: 'label_hint_drag_n_drop_video'.l10n,
onTap: () => c.showDragAndDropVideosHint = false,
),
),
),
)
: const SizedBox(),
);
}),
// Sliding from the top info header.
if (WebUtils.isPopup)
Obx(() {
return Align(
alignment: Alignment.topCenter,
child: AnimatedSlider(
duration: 400.milliseconds,
translate: false,
beginOffset: const Offset(0, -1),
endOffset: const Offset(0, 0),
isOpen:
c.state.value == OngoingCallState.active &&
c.showHeader.value,
child: MouseRegion(
onEnter: (_) {
c.showHeader.value = true;
c.headerHovered = true;
},
onHover: (_) {
c.showHeader.value = true;
c.headerHovered = true;
},
onExit: (_) {
c.showHeader.value = false;
c.headerHovered = false;
},
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(11),
boxShadow: [
CustomBoxShadow(
color: style.colors.onBackgroundOpacity20,
blurRadius: 8,
blurStyle: BlurStyle.outer,
),
],
),
margin: const EdgeInsets.fromLTRB(10, 5, 10, 2),
child: ConditionalBackdropFilter(
borderRadius: BorderRadius.circular(11),
filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15),
child: Container(
decoration: BoxDecoration(
color:
ConditionalBackdropFilter.enabled
? style.colors.primaryAuxiliaryOpacity25
: style.colors.primaryAuxiliaryOpacity55,
borderRadius: BorderRadius.circular(11),
),
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 10,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (c.fullscreen.value) ...[
Text(
'label_call_title'.l10nfmt(c.titleArguments),
style: style.fonts.small.regular.onPrimary,
overflow: TextOverflow.ellipsis,
),
Container(
margin: const EdgeInsets.fromLTRB(12, 0, 12, 0),
color: style.colors.onPrimary,
width: 1,
height: 12,
),
],
AnimatedButton(
enabled: c.draggedRenderer.value == null,
onPressed: c.layoutAsPrimary,
child: const SvgIcon(SvgIcons.callGallery),
),
const SizedBox(width: 16),
AnimatedButton(
enabled: c.draggedRenderer.value == null,
onPressed:
() => c.layoutAsSecondary(floating: true),
child: const SvgIcon(SvgIcons.callFloating),
),
const SizedBox(width: 16),
AnimatedButton(
enabled: c.draggedRenderer.value == null,
onPressed:
() => c.layoutAsSecondary(floating: false),
child: const SvgIcon(SvgIcons.callSide),
),
const SizedBox(width: 16),
AnimatedButton(
enabled: c.draggedRenderer.value == null,
onPressed: c.toggleFullscreen,
child: SvgIcon(
c.fullscreen.value
? SvgIcons.fullscreenExitSmall
: SvgIcons.fullscreenEnterSmall,
),
),
if (c.fullscreen.value) const SizedBox(width: 4),
],
),
),
),
),
),
),
);
}),
// If there's any notifications to show, display them.
Align(
alignment: Alignment.topCenter,
child: Padding(
padding: const EdgeInsets.only(top: 8),
child: Obx(() {
if (c.notifications.isEmpty) {
return const SizedBox();
}
return Column(
mainAxisSize: MainAxisSize.min,
children:
c.notifications.reversed.take(3).map((e) {
return CallNotificationWidget(
e,
onClose: () => c.notifications.remove(e),
);
}).toList(),
);
}),
),
),
Obx(() {
if (c.minimized.value && !c.fullscreen.value) {
return Container();
}
return Stack(children: footer);
}),
];
// Combines all the stackable content into [Scaffold].
Widget scaffold = Scaffold(
backgroundColor: style.colors.onBackground,
body: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (!WebUtils.isPopup)
GestureDetector(
behavior: HitTestBehavior.translucent,
onPanUpdate: (d) {
c.left.value = c.left.value + d.delta.dx;
c.top.value = c.top.value + d.delta.dy;
c.applyConstraints(context);
},
child: Container(
decoration: BoxDecoration(
color: style.colors.transparent,
borderRadius: BorderRadius.circular(30),
boxShadow: [
CustomBoxShadow(
color: style.colors.onBackgroundOpacity20,
blurRadius: 8,
blurStyle: BlurStyle.outer,
),
],
),
child: Obx(() {
return TitleBar(
title: 'label_call_title'.l10nfmt(c.titleArguments),
chat: c.chat.value,
fullscreen: c.fullscreen.value,
height: CallController.titleHeight,
toggleFullscreen:
c.draggedRenderer.value == null
? c.toggleFullscreen
: null,
onPrimary:
c.draggedRenderer.value == null
? c.layoutAsPrimary
: null,
onFloating:
c.draggedRenderer.value == null
? () => c.layoutAsSecondary(floating: true)
: null,
onSecondary:
c.draggedRenderer.value == null
? () => c.layoutAsSecondary(floating: false)
: null,
);
}),
),
),
Expanded(child: Stack(children: [...content, ...ui])),
],
),
);
if (c.minimized.value && !c.fullscreen.value) {
// Applies constraints on every rebuild.
// This includes the screen size changes.
c.applyConstraints(context);
if (c.hidden.value) {
return const SizedBox();
}
// Returns a [Scaler] scaling the minimized view.
Widget scaler({
Key? key,
MouseCursor cursor = MouseCursor.defer,
required Function(double, double) onDrag,
double? width,
double? height,
}) {
return Obx(() {
return MouseRegion(
cursor:
c.draggedRenderer.value != null ? MouseCursor.defer : cursor,
child: Scaler(
key: key,
onDragUpdate: onDrag,
onDragEnd: (_) => c.updateSecondaryAttach(),
width: width ?? Scaler.size,
height: height ?? Scaler.size,
),
);
});
}
// Returns a stack of draggable [Scaler]s on each of the sides:
//
// +-------+
// | |
// | |
// | |
// +-------+
//
// 1) + is a cornered scale point;
// 2) | is a horizontal scale point;
// 3) - is a vertical scale point.
return Stack(
children: [
// Top middle.
Obx(() {
return Positioned(
top: c.top.value - Scaler.size / 2,
left: c.left.value + Scaler.size / 2,
child: scaler(
cursor: SystemMouseCursors.resizeUpDown,
width: c.width.value - Scaler.size,
onDrag:
(dx, dy) => c.resize(context, y: ScaleModeY.top, dy: dy),
),
);
}),
// Center left.
Obx(() {
return Positioned(
top: c.top.value + Scaler.size / 2,
left: c.left.value - Scaler.size / 2,
child: scaler(
cursor: SystemMouseCursors.resizeLeftRight,
height: c.height.value - Scaler.size,
onDrag:
(dx, dy) => c.resize(context, x: ScaleModeX.left, dx: dx),
),
);
}),
// Center right.
Obx(() {
return Positioned(
top: c.top.value + Scaler.size / 2,
left: c.left.value + c.width.value - Scaler.size / 2,
child: scaler(
cursor: SystemMouseCursors.resizeLeftRight,
height: c.height.value - Scaler.size,
onDrag:
(dx, dy) =>
c.resize(context, x: ScaleModeX.right, dx: -dx),
),
);
}),
// Bottom center.
Obx(() {
return Positioned(
top: c.top.value + c.height.value - Scaler.size / 2,
left: c.left.value + Scaler.size / 2,
child: scaler(
cursor: SystemMouseCursors.resizeUpDown,
width: c.width.value - Scaler.size,
onDrag:
(dx, dy) =>
c.resize(context, y: ScaleModeY.bottom, dy: -dy),
),
);
}),
// Top left.
Obx(() {
return Positioned(
top: c.top.value - Scaler.size / 2,
left: c.left.value - Scaler.size / 2,
child: scaler(
cursor: CustomMouseCursors.resizeUpLeftDownRight,
width: Scaler.size * 2,
height: Scaler.size * 2,
onDrag:
(dx, dy) => c.resize(
context,
y: ScaleModeY.top,
x: ScaleModeX.left,
dx: dx,
dy: dy,
),
),
);
}),
// Top right.
Obx(() {
return Positioned(
top: c.top.value - Scaler.size / 2,
left: c.left.value + c.width.value - 3 * Scaler.size / 2,
child: scaler(
cursor: CustomMouseCursors.resizeUpRightDownLeft,
width: Scaler.size * 2,
height: Scaler.size * 2,
onDrag:
(dx, dy) => c.resize(
context,
y: ScaleModeY.top,
x: ScaleModeX.right,
dx: -dx,
dy: dy,
),
),
);
}),
// Bottom left.
Obx(() {
return Positioned(
top: c.top.value + c.height.value - 3 * Scaler.size / 2,
left: c.left.value - Scaler.size / 2,
child: scaler(
cursor: CustomMouseCursors.resizeUpRightDownLeft,
width: Scaler.size * 2,
height: Scaler.size * 2,
onDrag:
(dx, dy) => c.resize(
context,
y: ScaleModeY.bottom,
x: ScaleModeX.left,
dx: dx,
dy: -dy,
),
),
);
}),
// Bottom right.
Obx(() {
return Positioned(
top: c.top.value + c.height.value - 3 * Scaler.size / 2,
left: c.left.value + c.width.value - 3 * Scaler.size / 2,
child: scaler(
cursor: CustomMouseCursors.resizeUpLeftDownRight,
width: Scaler.size * 2,
height: Scaler.size * 2,
onDrag:
(dx, dy) => c.resize(
context,
y: ScaleModeY.bottom,
x: ScaleModeX.right,
dx: -dx,
dy: -dy,
),
),
);
}),
Obx(() {
return Positioned(
left: c.left.value,
top: c.top.value,
width: c.width.value,
height: c.height.value,
child: Material(
type: MaterialType.card,
borderRadius: BorderRadius.circular(10),
elevation: 10,
child: Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(10),
child: scaffold,
),
ClipRect(child: Stack(children: footer)),
],
),
),
);
}),
],
);
}
// If the call popup is not [minimized], then return the [scaffold].
return scaffold;
},
);
}