import 'package:flutter/material.dart'; /// Responsive & hinge-aware layout utilities for iPhone Duo (folded, unfolded, and Split View postures). class DuoResponsive { DuoResponsive._(); /// Viewport width threshold (in logical points) where the layout transitions /// from folded / narrow Split View to the unfolded inner display posture. static const double unfoldedBreakpoint = 600.0; /// Reserved center fold clearance (~40pt) for unfolded dual-pane postures /// since MediaQuery.displayFeatures does not natively report the iOS fold yet. static const double foldGap = 40.0; /// Maximum width for dialogs and modal bottom sheets so they do not stretch /// awkwardly across the 40pt center fold when unfolded. static const double maxModalWidth = 420.0; /// Standard capped width for navigation drawers on wide/unfolded screens. static const double maxDrawerWidth = 320.0; /// Width of the adaptive side navigation rail when unfolded. static const double sideRailWidth = 88.0; /// Returns true when the current window width is in the unfolded / expanded posture. static bool isUnfolded(BuildContext context) { return MediaQuery.sizeOf(context).width >= unfoldedBreakpoint; } /// Returns true when the given constraint or viewport width is in the unfolded posture. static bool isWideWidth(double width) { return width >= unfoldedBreakpoint; } /// Calculates the width of a single pane when split across the center fold gap. static double singlePaneWidth(double availableWidth, {double horizontalPadding = 0.0}) { double usable = availableWidth - (horizontalPadding * 2); if (!isWideWidth(availableWidth)) { return usable; } return ((usable - foldGap) / 2).clamp(0.0, double.infinity); } /// Returns adaptive dialog insetPadding so dialogs stay within a single pane /// or centered with a comfortable width instead of stretching across the hinge. static EdgeInsets dialogInsetPadding(BuildContext context, {double defaultHorizontal = 21.0, double defaultVertical = 24.0}) { Size size = MediaQuery.sizeOf(context); if (size.width >= unfoldedBreakpoint) { double horizontal = ((size.width - maxModalWidth) / 2).clamp(defaultHorizontal, size.width * 0.35); return EdgeInsets.symmetric(horizontal: horizontal, vertical: defaultVertical); } return EdgeInsets.symmetric(horizontal: defaultHorizontal, vertical: defaultVertical); } /// Returns BoxConstraints for modal bottom sheets so they never stretch across the fold. static BoxConstraints bottomSheetConstraints(BuildContext context, {double? maxHeight}) { Size size = MediaQuery.sizeOf(context); bool unfolded = size.width >= unfoldedBreakpoint; return BoxConstraints( maxWidth: unfolded ? maxModalWidth : size.width, maxHeight: maxHeight ?? double.infinity, ); } } /// A two-pane row that automatically inserts [DuoResponsive.foldGap] (~40pt) /// between [startPane] and [endPane] when in the unfolded posture, or [compactSpacing] /// when in the folded / compact posture. class DuoSplitRow extends StatelessWidget { final Widget startPane; final Widget endPane; final double compactSpacing; final CrossAxisAlignment crossAxisAlignment; const DuoSplitRow({ Key? key, required this.startPane, required this.endPane, this.compactSpacing = 9.0, this.crossAxisAlignment = CrossAxisAlignment.start, }) : super(key: key); @override Widget build(BuildContext context) { return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { bool isWide = DuoResponsive.isWideWidth(constraints.maxWidth) || DuoResponsive.isUnfolded(context); double gap = isWide ? DuoResponsive.foldGap : compactSpacing; return Row( crossAxisAlignment: crossAxisAlignment, children: [ Expanded(child: startPane), SizedBox(width: gap), Expanded(child: endPane), ], ); }, ); } } /// Constrains wide content (such as forms, cards, or lists) on unfolded screens /// while letting it fill the width naturally when folded. class DuoContentConstraint extends StatelessWidget { final Widget child; final double maxWidth; final AlignmentGeometry alignment; const DuoContentConstraint({ Key? key, required this.child, this.maxWidth = 720.0, this.alignment = Alignment.topCenter, }) : super(key: key); @override Widget build(BuildContext context) { return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { if (constraints.maxWidth <= maxWidth) { return child; } return Align( alignment: alignment, child: ConstrainedBox( constraints: BoxConstraints(maxWidth: maxWidth), child: child, ), ); }, ); } }