dialog.dart 82.1 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'dart:math' as math;
xster's avatar
xster committed
6
import 'dart:ui' show ImageFilter;
7 8 9

import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
10 11
import 'package:flutter/widgets.dart';

xster's avatar
xster committed
12
import 'colors.dart';
13
import 'interface_level.dart';
14
import 'localizations.dart';
15
import 'scrollbar.dart';
16
import 'theme.dart';
xster's avatar
xster committed
17

18 19
// TODO(abarth): These constants probably belong somewhere more general.

20 21 22 23 24
// Used XD to flutter plugin(https://github.com/AdobeXD/xd-to-flutter-plugin/)
// to derive values of TextStyle(height and letterSpacing) from
// Adobe XD template for iOS 13, which can be found in
// Apple Design Resources(https://developer.apple.com/design/resources/).
// However the values are not exactly the same as native, so eyeballing is needed.
25
const TextStyle _kCupertinoDialogTitleStyle = TextStyle(
26 27
  fontFamily: '.SF UI Display',
  inherit: false,
28
  fontSize: 17.0,
29
  fontWeight: FontWeight.w600,
30 31
  height: 1.3,
  letterSpacing: -0.5,
32 33 34
  textBaseline: TextBaseline.alphabetic,
);

35
const TextStyle _kCupertinoDialogContentStyle = TextStyle(
36 37
  fontFamily: '.SF UI Text',
  inherit: false,
38
  fontSize: 13.0,
39
  fontWeight: FontWeight.w400,
40 41
  height: 1.35,
  letterSpacing: -0.2,
42 43 44
  textBaseline: TextBaseline.alphabetic,
);

45
const TextStyle _kCupertinoDialogActionStyle = TextStyle(
46 47
  fontFamily: '.SF UI Text',
  inherit: false,
48
  fontSize: 16.8,
49 50 51 52
  fontWeight: FontWeight.w400,
  textBaseline: TextBaseline.alphabetic,
);

53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
// CupertinoActionSheet-specific text styles.
const TextStyle _kActionSheetActionStyle = TextStyle(
  fontFamily: '.SF UI Text',
  inherit: false,
  fontSize: 20.0,
  fontWeight: FontWeight.w400,
  textBaseline: TextBaseline.alphabetic,
);

const TextStyle _kActionSheetContentStyle = TextStyle(
  fontFamily: '.SF UI Text',
  inherit: false,
  fontSize: 13.0,
  fontWeight: FontWeight.w400,
  color: _kActionSheetContentTextColor,
  textBaseline: TextBaseline.alphabetic,
);

// Generic constants shared between Dialog and ActionSheet.
const double _kBlurAmount = 20.0;
const double _kCornerRadius = 14.0;
const double _kDividerThickness = 1.0;

// Dialog specific constants.
77 78 79
// iOS dialogs have a normal display width and another display width that is
// used when the device is in accessibility mode. Each of these widths are
// listed below.
80
const double _kCupertinoDialogWidth = 270.0;
81
const double _kAccessibilityCupertinoDialogWidth = 310.0;
82 83 84 85 86 87 88 89 90 91 92
const double _kDialogEdgePadding = 20.0;
const double _kDialogMinButtonHeight = 45.0;
const double _kDialogMinButtonFontSize = 10.0;

// ActionSheet specific constants.
const double _kActionSheetEdgeHorizontalPadding = 8.0;
const double _kActionSheetCancelButtonPadding = 8.0;
const double _kActionSheetEdgeVerticalPadding = 10.0;
const double _kActionSheetContentHorizontalPadding = 40.0;
const double _kActionSheetContentVerticalPadding = 14.0;
const double _kActionSheetButtonHeight = 56.0;
93

94 95 96 97 98 99 100
// A translucent color that is painted on top of the blurred backdrop as the
// dialog's background color
// Extracted from https://developer.apple.com/design/resources/.
const Color _kDialogColor = CupertinoDynamicColor.withBrightness(
  color: Color(0xCCF2F2F2),
  darkColor: Color(0xBF1E1E1E),
);
101

102
// Translucent light gray that is painted on top of the blurred backdrop as the
103
// background color of a pressed button.
104
// Eyeballed from iOS 13 beta simulator.
105
const Color _kPressedColor = CupertinoDynamicColor.withBrightness(
106 107 108
  color: Color(0xFFE1E1E1),
  darkColor: Color(0xFF2E2E2E),
);
109

110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134
const Color _kActionSheetCancelPressedColor = CupertinoDynamicColor.withBrightness(
  color: Color(0xFFECECEC),
  darkColor: Color(0xFF49494B),
);

// Translucent, very light gray that is painted on top of the blurred backdrop
// as the action sheet's background color.
// TODO(LongCatIsLooong): https://github.com/flutter/flutter/issues/39272. Use
// System Materials once we have them.
// Extracted from https://developer.apple.com/design/resources/.
const Color _kActionSheetBackgroundColor = CupertinoDynamicColor.withBrightness(
  color: Color(0xC7F9F9F9),
  darkColor: Color(0xC7252525),
);

// The gray color used for text that appears in the title area.
// Extracted from https://developer.apple.com/design/resources/.
const Color _kActionSheetContentTextColor = Color(0xFF8F8F8F);

// Translucent gray that is painted on top of the blurred backdrop in the gap
// areas between the content section and actions section, as well as between
// buttons.
// Eye-balled from iOS 13 beta simulator.
const Color _kActionSheetButtonDividerColor = _kActionSheetContentTextColor;

135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151
// The alert dialog layout policy changes depending on whether the user is using
// a "regular" font size vs a "large" font size. This is a spectrum. There are
// many "regular" font sizes and many "large" font sizes. But depending on which
// policy is currently being used, a dialog is laid out differently.
//
// Empirically, the jump from one policy to the other occurs at the following text
// scale factors:
// Largest regular scale factor:  1.3529411764705883
// Smallest large scale factor:   1.6470588235294117
//
// The following constant represents a division in text scale factor beyond which
// we want to change how the dialog is laid out.
const double _kMaxRegularTextScaleFactor = 1.4;

// Accessibility mode on iOS is determined by the text scale factor that the
// user has selected.
bool _isInAccessibilityMode(BuildContext context) {
152 153
  final double? factor = MediaQuery.maybeTextScaleFactorOf(context);
  return factor != null && factor > _kMaxRegularTextScaleFactor;
154
}
155

156 157
/// An iOS-style alert dialog.
///
158 159
/// {@youtube 560 315 https://www.youtube.com/watch?v=75CsnyRXf5I}
///
160
/// An alert dialog informs the user about situations that require
161
/// acknowledgment. An alert dialog has an optional title, optional content,
162 163 164 165 166 167 168 169 170 171
/// and an optional list of actions. The title is displayed above the content
/// and the actions are displayed below the content.
///
/// This dialog styles its title and content (typically a message) to match the
/// standard iOS title and message dialog text style. These default styles can
/// be overridden by explicitly defining [TextStyle]s for [Text] widgets that
/// are part of the title or content.
///
/// To display action buttons that look like standard iOS dialog buttons,
/// provide [CupertinoDialogAction]s for the [actions] given to this dialog.
172 173 174 175
///
/// Typically passed as the child widget to [showDialog], which displays the
/// dialog.
///
176
/// {@tool dartpad}
177 178 179 180
/// This sample shows how to use a [CupertinoAlertDialog].
///	The [CupertinoAlertDialog] shows an alert with a set of two choices
/// when [CupertinoButton] is pressed.
///
181
/// ** See code in examples/api/lib/cupertino/dialog/cupertino_alert_dialog.0.dart **
182 183
/// {@end-tool}
///
184 185
/// See also:
///
186 187
///  * [CupertinoPopupSurface], which is a generic iOS-style popup surface that
///    holds arbitrary content to create custom popups.
188
///  * [CupertinoDialogAction], which is an iOS-style dialog button.
189
///  * [AlertDialog], a Material Design alert dialog.
190
///  * <https://developer.apple.com/ios/human-interface-guidelines/views/alerts/>
191 192
class CupertinoAlertDialog extends StatelessWidget {
  /// Creates an iOS-style alert dialog.
193 194
  ///
  /// The [actions] must not be null.
195
  const CupertinoAlertDialog({
196
    super.key,
197 198
    this.title,
    this.content,
199
    this.actions = const <Widget>[],
200
    this.scrollController,
201
    this.actionScrollController,
202 203
    this.insetAnimationDuration = const Duration(milliseconds: 100),
    this.insetAnimationCurve = Curves.decelerate,
204
  });
205 206 207 208 209

  /// The (optional) title of the dialog is displayed in a large font at the top
  /// of the dialog.
  ///
  /// Typically a [Text] widget.
210
  final Widget? title;
211 212 213 214 215

  /// The (optional) content of the dialog is displayed in the center of the
  /// dialog in a lighter font.
  ///
  /// Typically a [Text] widget.
216
  final Widget? content;
217 218 219 220 221 222 223

  /// The (optional) set of actions that are displayed at the bottom of the
  /// dialog.
  ///
  /// Typically this is a list of [CupertinoDialogAction] widgets.
  final List<Widget> actions;

224 225
  /// A scroll controller that can be used to control the scrolling of the
  /// [content] in the dialog.
226
  ///
227 228 229 230 231 232 233
  /// Defaults to null, and is typically not needed, since most alert messages
  /// are short.
  ///
  /// See also:
  ///
  ///  * [actionScrollController], which can be used for controlling the actions
  ///    section when there are many actions.
234
  final ScrollController? scrollController;
235

236 237 238
  ScrollController get _effectiveScrollController =>
    scrollController ?? ScrollController();

239 240 241 242 243 244 245 246 247
  /// A scroll controller that can be used to control the scrolling of the
  /// actions in the dialog.
  ///
  /// Defaults to null, and is typically not needed.
  ///
  /// See also:
  ///
  ///  * [scrollController], which can be used for controlling the [content]
  ///    section when it is long.
248
  final ScrollController? actionScrollController;
249

250 251 252
  ScrollController get _effectiveActionScrollController =>
    actionScrollController ?? ScrollController();

253 254 255 256 257 258
  /// {@macro flutter.material.dialog.insetAnimationDuration}
  final Duration insetAnimationDuration;

  /// {@macro flutter.material.dialog.insetAnimationCurve}
  final Curve insetAnimationCurve;

259
  Widget _buildContent(BuildContext context) {
260
    final double textScaleFactor = MediaQuery.textScaleFactorOf(context);
261

262 263 264 265 266 267
    final List<Widget> children = <Widget>[
      if (title != null || content != null)
        Flexible(
          flex: 3,
          child: _CupertinoAlertContentSection(
            title: title,
268
            message: content,
269
            scrollController: _effectiveScrollController,
270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287
            titlePadding: EdgeInsets.only(
              left: _kDialogEdgePadding,
              right: _kDialogEdgePadding,
              bottom: content == null ? _kDialogEdgePadding : 1.0,
              top: _kDialogEdgePadding * textScaleFactor,
            ),
            messagePadding: EdgeInsets.only(
              left: _kDialogEdgePadding,
              right: _kDialogEdgePadding,
              bottom: _kDialogEdgePadding * textScaleFactor,
              top: title == null ? _kDialogEdgePadding : 1.0,
            ),
            titleTextStyle: _kCupertinoDialogTitleStyle.copyWith(
              color: CupertinoDynamicColor.resolve(CupertinoColors.label, context),
            ),
            messageTextStyle: _kCupertinoDialogContentStyle.copyWith(
              color: CupertinoDynamicColor.resolve(CupertinoColors.label, context),
            ),
288 289 290
          ),
        ),
    ];
291

292
    return ColoredBox(
293
      color: CupertinoDynamicColor.resolve(_kDialogColor, context),
294
      child: Column(
295 296 297 298 299 300 301 302
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: children,
      ),
    );
  }

  Widget _buildActions() {
303
    Widget actionSection = Container(
304 305
      height: 0.0,
    );
306
    if (actions.isNotEmpty) {
307
      actionSection = _CupertinoAlertActionSection(
308
        scrollController: _effectiveActionScrollController,
309
        children: actions,
310
      );
311
    }
312

313 314 315 316 317
    return actionSection;
  }

  @override
  Widget build(BuildContext context) {
318
    final CupertinoLocalizations localizations = CupertinoLocalizations.of(context);
319
    final bool isInAccessibilityMode = _isInAccessibilityMode(context);
320
    final double textScaleFactor = MediaQuery.textScaleFactorOf(context);
321 322 323
    return CupertinoUserInterfaceLevel(
      data: CupertinoUserInterfaceLevelData.elevated,
      child: MediaQuery(
324
        data: MediaQuery.of(context).copyWith(
325 326 327
          // iOS does not shrink dialog content below a 1.0 scale factor
          textScaleFactor: math.max(textScaleFactor, 1.0),
        ),
328 329 330 331 332 333
        child: ScrollConfiguration(
          // A CupertinoScrollbar is built-in below.
          behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
          child: LayoutBuilder(
            builder: (BuildContext context, BoxConstraints constraints) {
              return AnimatedPadding(
334
                padding: MediaQuery.viewInsetsOf(context) +
335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361
                    const EdgeInsets.symmetric(horizontal: 40.0, vertical: 24.0),
                duration: insetAnimationDuration,
                curve: insetAnimationCurve,
                child: MediaQuery.removeViewInsets(
                  removeLeft: true,
                  removeTop: true,
                  removeRight: true,
                  removeBottom: true,
                  context: context,
                  child: Center(
                    child: Container(
                      margin: const EdgeInsets.symmetric(vertical: _kDialogEdgePadding),
                      width: isInAccessibilityMode
                          ? _kAccessibilityCupertinoDialogWidth
                          : _kCupertinoDialogWidth,
                      child: CupertinoPopupSurface(
                        isSurfacePainted: false,
                        child: Semantics(
                          namesRoute: true,
                          scopesRoute: true,
                          explicitChildNodes: true,
                          label: localizations.alertDialogLabel,
                          child: _CupertinoDialogRenderWidget(
                            contentSection: _buildContent(context),
                            actionsSection: _buildActions(),
                            dividerColor: CupertinoColors.separator,
                          ),
362 363
                        ),
                      ),
364
                    ),
365
                  ),
366
                ),
367 368 369
              );
            },
          ),
370
        ),
371 372 373 374 375
      ),
    );
  }
}

376 377 378 379 380 381 382 383 384 385 386
/// Rounded rectangle surface that looks like an iOS popup surface, e.g., alert dialog
/// and action sheet.
///
/// A [CupertinoPopupSurface] can be configured to paint or not paint a white
/// color on top of its blurred area. Typical usage should paint white on top
/// of the blur. However, the white paint can be disabled for the purpose of
/// rendering divider gaps for a more complicated layout, e.g., [CupertinoAlertDialog].
/// Additionally, the white paint can be disabled to render a blurred rounded
/// rectangle without any color (similar to iOS's volume control popup).
///
/// See also:
387
///
388 389 390 391 392 393
///  * [CupertinoAlertDialog], which is a dialog with a title, content, and
///    actions.
///  * <https://developer.apple.com/ios/human-interface-guidelines/views/alerts/>
class CupertinoPopupSurface extends StatelessWidget {
  /// Creates an iOS-style rounded rectangle popup surface.
  const CupertinoPopupSurface({
394
    super.key,
395 396
    this.isSurfacePainted = true,
    this.child,
397
  });
398 399 400 401 402 403 404 405 406 407 408 409

  /// Whether or not to paint a translucent white on top of this surface's
  /// blurred background. [isSurfacePainted] should be true for a typical popup
  /// that contains content without any dividers. A popup that requires dividers
  /// should set [isSurfacePainted] to false and then paint its own surface area.
  ///
  /// Some popups, like iOS's volume control popup, choose to render a blurred
  /// area without any white paint covering it. To achieve this effect,
  /// [isSurfacePainted] should be set to false.
  final bool isSurfacePainted;

  /// The widget below this widget in the tree.
410
  final Widget? child;
411 412 413

  @override
  Widget build(BuildContext context) {
414
    return ClipRRect(
415
      borderRadius: const BorderRadius.all(Radius.circular(_kCornerRadius)),
416 417 418
      child: BackdropFilter(
        filter: ImageFilter.blur(sigmaX: _kBlurAmount, sigmaY: _kBlurAmount),
        child: Container(
419 420
          color: isSurfacePainted ? CupertinoDynamicColor.resolve(_kDialogColor, context) : null,
          child: child,
421 422 423 424 425 426
        ),
      ),
    );
  }
}

427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451
/// An iOS-style action sheet.
///
/// {@youtube 560 315 https://www.youtube.com/watch?v=U-ao8p4A82k}
///
/// An action sheet is a specific style of alert that presents the user
/// with a set of two or more choices related to the current context.
/// An action sheet can have a title, an additional message, and a list
/// of actions. The title is displayed above the message and the actions
/// are displayed below this content.
///
/// This action sheet styles its title and message to match standard iOS action
/// sheet title and message text style.
///
/// To display action buttons that look like standard iOS action sheet buttons,
/// provide [CupertinoActionSheetAction]s for the [actions] given to this action
/// sheet.
///
/// To include a iOS-style cancel button separate from the other buttons,
/// provide an [CupertinoActionSheetAction] for the [cancelButton] given to this
/// action sheet.
///
/// An action sheet is typically passed as the child widget to
/// [showCupertinoModalPopup], which displays the action sheet by sliding it up
/// from the bottom of the screen.
///
452
/// {@tool dartpad}
453 454 455 456
/// This sample shows how to use a [CupertinoActionSheet].
///	The [CupertinoActionSheet] shows a modal popup that slides in from the
/// bottom when [CupertinoButton] is pressed.
///
457
/// ** See code in examples/api/lib/cupertino/dialog/cupertino_action_sheet.0.dart **
458 459 460 461 462 463 464 465 466 467 468 469 470 471 472
/// {@end-tool}
///
/// See also:
///
///  * [CupertinoActionSheetAction], which is an iOS-style action sheet button.
///  * <https://developer.apple.com/design/human-interface-guidelines/ios/views/action-sheets/>
class CupertinoActionSheet extends StatelessWidget {
  /// Creates an iOS-style action sheet.
  ///
  /// An action sheet must have a non-null value for at least one of the
  /// following arguments: [actions], [title], [message], or [cancelButton].
  ///
  /// Generally, action sheets are used to give the user a choice between
  /// two or more choices for the current context.
  const CupertinoActionSheet({
473
    super.key,
474 475 476 477 478 479 480 481 482 483
    this.title,
    this.message,
    this.actions,
    this.messageScrollController,
    this.actionScrollController,
    this.cancelButton,
  }) : assert(
         actions != null || title != null || message != null || cancelButton != null,
         'An action sheet must have a non-null value for at least one of the following arguments: '
         'actions, title, message, or cancelButton',
484
       );
485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509

  /// An optional title of the action sheet. When the [message] is non-null,
  /// the font of the [title] is bold.
  ///
  /// Typically a [Text] widget.
  final Widget? title;

  /// An optional descriptive message that provides more details about the
  /// reason for the alert.
  ///
  /// Typically a [Text] widget.
  final Widget? message;

  /// The set of actions that are displayed for the user to select.
  ///
  /// Typically this is a list of [CupertinoActionSheetAction] widgets.
  final List<Widget>? actions;

  /// A scroll controller that can be used to control the scrolling of the
  /// [message] in the action sheet.
  ///
  /// This attribute is typically not needed, as alert messages should be
  /// short.
  final ScrollController? messageScrollController;

510 511 512
  ScrollController get _effectiveMessageScrollController =>
    messageScrollController ?? ScrollController();

513 514 515 516 517 518
  /// A scroll controller that can be used to control the scrolling of the
  /// [actions] in the action sheet.
  ///
  /// This attribute is typically not needed.
  final ScrollController? actionScrollController;

519 520 521
  ScrollController get _effectiveActionScrollController =>
    actionScrollController ?? ScrollController();

522 523 524 525 526 527 528 529 530 531 532 533
  /// The optional cancel button that is grouped separately from the other
  /// actions.
  ///
  /// Typically this is an [CupertinoActionSheetAction] widget.
  final Widget? cancelButton;

  Widget _buildContent(BuildContext context) {
    final List<Widget> content = <Widget>[];
    if (title != null || message != null) {
      final Widget titleSection = _CupertinoAlertContentSection(
        title: title,
        message: message,
534
        scrollController: _effectiveMessageScrollController,
535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557
        titlePadding: const EdgeInsets.only(
          left: _kActionSheetContentHorizontalPadding,
          right: _kActionSheetContentHorizontalPadding,
          bottom: _kActionSheetContentVerticalPadding,
          top: _kActionSheetContentVerticalPadding,
        ),
        messagePadding: EdgeInsets.only(
          left: _kActionSheetContentHorizontalPadding,
          right: _kActionSheetContentHorizontalPadding,
          bottom: title == null ? _kActionSheetContentVerticalPadding : 22.0,
          top: title == null ? _kActionSheetContentVerticalPadding : 0.0,
        ),
        titleTextStyle: message == null
            ? _kActionSheetContentStyle
            : _kActionSheetContentStyle.copyWith(fontWeight: FontWeight.w600),
        messageTextStyle: title == null
            ? _kActionSheetContentStyle.copyWith(fontWeight: FontWeight.w600)
            : _kActionSheetContentStyle,
        additionalPaddingBetweenTitleAndMessage: const EdgeInsets.only(top: 8.0),
      );
      content.add(Flexible(child: titleSection));
    }

558
    return ColoredBox(
559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574
      color: CupertinoDynamicColor.resolve(_kActionSheetBackgroundColor, context),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: content,
      ),
    );
  }

  Widget _buildActions() {
    if (actions == null || actions!.isEmpty) {
      return Container(
        height: 0.0,
      );
    }
    return _CupertinoAlertActionSection(
575
      scrollController: _effectiveActionScrollController,
576 577
      hasCancelButton: cancelButton != null,
      isActionSheet: true,
578
      children: actions!,
579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598
    );
  }

  Widget _buildCancelButton() {
    final double cancelPadding = (actions != null || message != null || title != null)
        ? _kActionSheetCancelButtonPadding : 0.0;
    return Padding(
      padding: EdgeInsets.only(top: cancelPadding),
      child: _CupertinoActionSheetCancelButton(
        child: cancelButton,
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    assert(debugCheckHasMediaQuery(context));

    final List<Widget> children = <Widget>[
      Flexible(child: ClipRRect(
599
          borderRadius: const BorderRadius.all(Radius.circular(12.0)),
600 601 602 603 604 605 606 607 608 609 610 611 612 613
          child: BackdropFilter(
            filter: ImageFilter.blur(sigmaX: _kBlurAmount, sigmaY: _kBlurAmount),
            child: _CupertinoDialogRenderWidget(
              contentSection: Builder(builder: _buildContent),
              actionsSection: _buildActions(),
              dividerColor: _kActionSheetButtonDividerColor,
              isActionSheet: true,
            ),
          ),
        ),
      ),
      if (cancelButton != null) _buildCancelButton(),
    ];

614
    final Orientation orientation = MediaQuery.orientationOf(context);
615 616
    final double actionSheetWidth;
    if (orientation == Orientation.portrait) {
617
      actionSheetWidth = MediaQuery.sizeOf(context).width - (_kActionSheetEdgeHorizontalPadding * 2);
618
    } else {
619
      actionSheetWidth = MediaQuery.sizeOf(context).height - (_kActionSheetEdgeHorizontalPadding * 2);
620 621 622
    }

    return SafeArea(
623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643
      child: ScrollConfiguration(
        // A CupertinoScrollbar is built-in below
        behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
        child: Semantics(
          namesRoute: true,
          scopesRoute: true,
          explicitChildNodes: true,
          label: 'Alert',
          child: CupertinoUserInterfaceLevel(
            data: CupertinoUserInterfaceLevelData.elevated,
            child: Container(
              width: actionSheetWidth,
              margin: const EdgeInsets.symmetric(
                horizontal: _kActionSheetEdgeHorizontalPadding,
                vertical: _kActionSheetEdgeVerticalPadding,
              ),
              child: Column(
                mainAxisSize: MainAxisSize.min,
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: children,
              ),
644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662
            ),
          ),
        ),
      ),
    );
  }
}

/// A button typically used in a [CupertinoActionSheet].
///
/// See also:
///
///  * [CupertinoActionSheet], an alert that presents the user with a set of two or
///    more choices related to the current context.
class CupertinoActionSheetAction extends StatelessWidget {
  /// Creates an action for an iOS-style action sheet.
  ///
  /// The [child] and [onPressed] arguments must not be null.
  const CupertinoActionSheetAction({
663
    super.key,
664 665 666 667
    required this.onPressed,
    this.isDefaultAction = false,
    this.isDestructiveAction = false,
    required this.child,
668
  });
669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701

  /// The callback that is called when the button is tapped.
  ///
  /// This attribute must not be null.
  final VoidCallback onPressed;

  /// Whether this action is the default choice in the action sheet.
  ///
  /// Default buttons have bold text.
  final bool isDefaultAction;

  /// Whether this action might change or delete data.
  ///
  /// Destructive buttons have red text.
  final bool isDestructiveAction;

  /// The widget below this widget in the tree.
  ///
  /// Typically a [Text] widget.
  final Widget child;

  @override
  Widget build(BuildContext context) {
    TextStyle style = _kActionSheetActionStyle.copyWith(
      color: isDestructiveAction
          ? CupertinoDynamicColor.resolve(CupertinoColors.systemRed, context)
          : CupertinoTheme.of(context).primaryColor,
    );

    if (isDefaultAction) {
      style = style.copyWith(fontWeight: FontWeight.w600);
    }

702
    return MouseRegion(
703
      cursor: kIsWeb ? SystemMouseCursors.click : MouseCursor.defer,
704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723
      child: GestureDetector(
        onTap: onPressed,
        behavior: HitTestBehavior.opaque,
        child: ConstrainedBox(
          constraints: const BoxConstraints(
            minHeight: _kActionSheetButtonHeight,
          ),
          child: Semantics(
            button: true,
            child: Container(
              alignment: Alignment.center,
              padding: const EdgeInsets.symmetric(
                vertical: 16.0,
                horizontal: 10.0,
              ),
              child: DefaultTextStyle(
                style: style,
                textAlign: TextAlign.center,
                child: child,
              ),
724 725 726 727 728 729 730 731 732 733 734
            ),
          ),
        ),
      ),
    );
  }
}

class _CupertinoActionSheetCancelButton extends StatefulWidget {
  const _CupertinoActionSheetCancelButton({
    this.child,
735
  });
736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770

  final Widget? child;

  @override
  _CupertinoActionSheetCancelButtonState createState() => _CupertinoActionSheetCancelButtonState();
}

class _CupertinoActionSheetCancelButtonState extends State<_CupertinoActionSheetCancelButton> {
  bool isBeingPressed = false;

  void _onTapDown(TapDownDetails event) {
    setState(() { isBeingPressed = true; });
  }

  void _onTapUp(TapUpDetails event) {
    setState(() { isBeingPressed = false; });
  }

  void _onTapCancel() {
    setState(() { isBeingPressed = false; });
  }

  @override
  Widget build(BuildContext context) {
    final Color backgroundColor = isBeingPressed
        ? _kActionSheetCancelPressedColor
        : CupertinoColors.secondarySystemGroupedBackground;
    return GestureDetector(
      excludeFromSemantics: true,
      onTapDown: _onTapDown,
      onTapUp: _onTapUp,
      onTapCancel: _onTapCancel,
      child: Container(
        decoration: BoxDecoration(
          color: CupertinoDynamicColor.resolve(backgroundColor, context),
771
          borderRadius: const BorderRadius.all(Radius.circular(_kCornerRadius)),
772 773 774 775 776 777 778
        ),
        child: widget.child,
      ),
    );
  }
}

779 780 781 782 783 784
// iOS style layout policy widget for sizing an alert dialog's content section and
// action button section.
//
// See [_RenderCupertinoDialog] for specific layout policy details.
class _CupertinoDialogRenderWidget extends RenderObjectWidget {
  const _CupertinoDialogRenderWidget({
785 786
    required this.contentSection,
    required this.actionsSection,
787 788
    required this.dividerColor,
    this.isActionSheet = false,
789
  });
790

791 792
  final Widget contentSection;
  final Widget actionsSection;
793 794
  final Color dividerColor;
  final bool isActionSheet;
795

796 797
  @override
  RenderObject createRenderObject(BuildContext context) {
798
    return _RenderCupertinoDialog(
799
      dividerThickness: _kDividerThickness / MediaQuery.devicePixelRatioOf(context),
800 801 802
      isInAccessibilityMode: _isInAccessibilityMode(context) && !isActionSheet,
      dividerColor: CupertinoDynamicColor.resolve(dividerColor, context),
      isActionSheet: isActionSheet,
803 804
    );
  }
805

806 807
  @override
  void updateRenderObject(BuildContext context, _RenderCupertinoDialog renderObject) {
808
    renderObject
809
      ..isInAccessibilityMode = _isInAccessibilityMode(context) && !isActionSheet
810 811
      ..dividerColor = CupertinoDynamicColor.resolve(dividerColor, context)
      ..isActionSheet = isActionSheet;
812
  }
813

814 815
  @override
  RenderObjectElement createElement() {
816
    return _CupertinoDialogRenderElement(this, allowMoveRenderObjectChild: isActionSheet);
817 818
  }
}
819

820
class _CupertinoDialogRenderElement extends RenderObjectElement {
821
  _CupertinoDialogRenderElement(_CupertinoDialogRenderWidget super.widget, {this.allowMoveRenderObjectChild = false});
822

823
  // Whether to allow overridden method moveRenderObjectChild call or default to super.
824 825
  // CupertinoActionSheet should default to [super] but CupertinoAlertDialog not.
  final bool allowMoveRenderObjectChild;
826

827 828
  Element? _contentElement;
  Element? _actionsElement;
829

830
  @override
831
  _RenderCupertinoDialog get renderObject => super.renderObject as _RenderCupertinoDialog;
832

833 834 835
  @override
  void visitChildren(ElementVisitor visitor) {
    if (_contentElement != null) {
836
      visitor(_contentElement!);
837
    }
838
    if (_actionsElement != null) {
839
      visitor(_actionsElement!);
840 841
    }
  }
842

843
  @override
844
  void mount(Element? parent, Object? newSlot) {
845
    super.mount(parent, newSlot);
846 847 848
    final _CupertinoDialogRenderWidget dialogRenderWidget = widget as _CupertinoDialogRenderWidget;
    _contentElement = updateChild(_contentElement, dialogRenderWidget.contentSection, _AlertDialogSections.contentSection);
    _actionsElement = updateChild(_actionsElement, dialogRenderWidget.actionsSection, _AlertDialogSections.actionsSection);
849 850 851
  }

  @override
852
  void insertRenderObjectChild(RenderObject child, _AlertDialogSections slot) {
853
    _placeChildInSlot(child, slot);
854
  }
855

856
  @override
857
  void moveRenderObjectChild(RenderObject child, _AlertDialogSections oldSlot, _AlertDialogSections newSlot) {
858
    if (!allowMoveRenderObjectChild) {
859
      assert(false);
860 861 862 863
      return;
    }

    _placeChildInSlot(child, newSlot);
864 865 866 867 868
  }

  @override
  void update(RenderObjectWidget newWidget) {
    super.update(newWidget);
869 870 871
    final _CupertinoDialogRenderWidget dialogRenderWidget = widget as _CupertinoDialogRenderWidget;
    _contentElement = updateChild(_contentElement, dialogRenderWidget.contentSection, _AlertDialogSections.contentSection);
    _actionsElement = updateChild(_actionsElement, dialogRenderWidget.actionsSection, _AlertDialogSections.actionsSection);
872 873 874 875 876 877 878 879 880 881
  }

  @override
  void forgetChild(Element child) {
    assert(child == _contentElement || child == _actionsElement);
    if (_contentElement == child) {
      _contentElement = null;
    } else {
      assert(_actionsElement == child);
      _actionsElement = null;
882
    }
883
    super.forgetChild(child);
884
  }
885

886
  @override
887
  void removeRenderObjectChild(RenderObject child, _AlertDialogSections slot) {
888 889 890 891 892 893 894 895
    assert(child == renderObject.contentSection || child == renderObject.actionsSection);
    if (renderObject.contentSection == child) {
      renderObject.contentSection = null;
    } else {
      assert(renderObject.actionsSection == child);
      renderObject.actionsSection = null;
    }
  }
896 897 898 899 900 901 902 903 904

  void _placeChildInSlot(RenderObject child, _AlertDialogSections slot) {
    switch (slot) {
      case _AlertDialogSections.contentSection:
        renderObject.contentSection = child as RenderBox;
      case _AlertDialogSections.actionsSection:
        renderObject.actionsSection = child as RenderBox;
    }
  }
905 906 907 908 909 910 911 912 913 914 915 916 917
}

// iOS style layout policy for sizing an alert dialog's content section and action
// button section.
//
// The policy is as follows:
//
// If all content and buttons fit on screen:
// The content section and action button section are sized intrinsically and centered
// vertically on screen.
//
// If all content and buttons do not fit on screen, and iOS is NOT in accessibility mode:
// A minimum height for the action button section is calculated. The action
918
// button section will not be rendered shorter than this minimum. See
919 920 921 922 923 924 925 926 927 928 929 930 931 932
// [_RenderCupertinoDialogActions] for the minimum height calculation.
//
// With the minimum action button section calculated, the content section can
// take up as much space as is available, up to the point that it hits the
// minimum button height at the bottom.
//
// After the content section is laid out, the action button section is allowed
// to take up any remaining space that was not consumed by the content section.
//
// If all content and buttons do not fit on screen, and iOS IS in accessibility mode:
// The button section is given up to 50% of the available height. Then the content
// section is given whatever height remains.
class _RenderCupertinoDialog extends RenderBox {
  _RenderCupertinoDialog({
933 934
    RenderBox? contentSection,
    RenderBox? actionsSection,
935 936
    double dividerThickness = 0.0,
    bool isInAccessibilityMode = false,
937
    bool isActionSheet = false,
938
    required Color dividerColor,
939 940 941
  }) : _contentSection = contentSection,
       _actionsSection = actionsSection,
       _dividerThickness = dividerThickness,
942
       _isInAccessibilityMode = isInAccessibilityMode,
943
       _isActionSheet = isActionSheet,
944
       _dividerPaint = Paint()
945 946
         ..color = dividerColor
         ..style = PaintingStyle.fill;
947

948 949 950
  RenderBox? get contentSection => _contentSection;
  RenderBox? _contentSection;
  set contentSection(RenderBox? newContentSection) {
951 952
    if (newContentSection != _contentSection) {
      if (_contentSection != null) {
953
        dropChild(_contentSection!);
954 955 956
      }
      _contentSection = newContentSection;
      if (_contentSection != null) {
957
        adoptChild(_contentSection!);
958 959 960 961
      }
    }
  }

962 963 964
  RenderBox? get actionsSection => _actionsSection;
  RenderBox? _actionsSection;
  set actionsSection(RenderBox? newActionsSection) {
965 966
    if (newActionsSection != _actionsSection) {
      if (null != _actionsSection) {
967
        dropChild(_actionsSection!);
968 969 970
      }
      _actionsSection = newActionsSection;
      if (null != _actionsSection) {
971
        adoptChild(_actionsSection!);
972 973 974 975 976 977 978 979 980 981 982 983 984
      }
    }
  }

  bool get isInAccessibilityMode => _isInAccessibilityMode;
  bool _isInAccessibilityMode;
  set isInAccessibilityMode(bool newValue) {
    if (newValue != _isInAccessibilityMode) {
      _isInAccessibilityMode = newValue;
      markNeedsLayout();
    }
  }

985 986 987 988 989 990 991 992 993
  bool _isActionSheet;
  bool get isActionSheet => _isActionSheet;
  set isActionSheet(bool newValue) {
    if (newValue != _isActionSheet) {
      _isActionSheet = newValue;
      markNeedsLayout();
    }
  }

994 995 996 997 998
  double get _dialogWidth => isInAccessibilityMode
      ? _kAccessibilityCupertinoDialogWidth
      : _kCupertinoDialogWidth;

  final double _dividerThickness;
999 1000 1001 1002 1003 1004 1005
  final Paint _dividerPaint;

  Color get dividerColor => _dividerPaint.color;
  set dividerColor(Color newValue) {
    if (dividerColor == newValue) {
      return;
    }
1006

1007 1008 1009
    _dividerPaint.color = newValue;
    markNeedsPaint();
  }
1010 1011 1012 1013 1014

  @override
  void attach(PipelineOwner owner) {
    super.attach(owner);
    if (null != contentSection) {
1015
      contentSection!.attach(owner);
1016 1017
    }
    if (null != actionsSection) {
1018
      actionsSection!.attach(owner);
1019 1020 1021 1022 1023 1024 1025
    }
  }

  @override
  void detach() {
    super.detach();
    if (null != contentSection) {
1026
      contentSection!.detach();
1027 1028
    }
    if (null != actionsSection) {
1029
      actionsSection!.detach();
1030 1031 1032 1033 1034 1035
    }
  }

  @override
  void redepthChildren() {
    if (null != contentSection) {
1036
      redepthChild(contentSection!);
1037 1038
    }
    if (null != actionsSection) {
1039
      redepthChild(actionsSection!);
1040 1041 1042 1043 1044
    }
  }

  @override
  void setupParentData(RenderBox child) {
1045
    if (!isActionSheet && child.parentData is! BoxParentData) {
1046
      child.parentData = BoxParentData();
1047 1048
    } else if (child.parentData is! MultiChildLayoutParentData) {
      child.parentData = MultiChildLayoutParentData();
1049 1050 1051 1052 1053 1054
    }
  }

  @override
  void visitChildren(RenderObjectVisitor visitor) {
    if (contentSection != null) {
1055
      visitor(contentSection!);
1056 1057
    }
    if (actionsSection != null) {
1058
      visitor(actionsSection!);
1059 1060 1061 1062
    }
  }

  @override
1063
  List<DiagnosticsNode> debugDescribeChildren() => <DiagnosticsNode>[
1064 1065
    if (contentSection != null) contentSection!.toDiagnosticsNode(name: 'content'),
    if (actionsSection != null) actionsSection!.toDiagnosticsNode(name: 'actions'),
1066
  ];
1067 1068 1069

  @override
  double computeMinIntrinsicWidth(double height) {
1070
    return isActionSheet ? constraints.minWidth : _dialogWidth;
1071 1072 1073 1074
  }

  @override
  double computeMaxIntrinsicWidth(double height) {
1075
    return isActionSheet ? constraints.maxWidth : _dialogWidth;
1076 1077 1078 1079
  }

  @override
  double computeMinIntrinsicHeight(double width) {
1080 1081
    final double contentHeight = contentSection!.getMinIntrinsicHeight(width);
    final double actionsHeight = actionsSection!.getMinIntrinsicHeight(width);
1082
    final bool hasDivider = contentHeight > 0.0 && actionsHeight > 0.0;
1083
    double height = contentHeight + (hasDivider ? _dividerThickness : 0.0) + actionsHeight;
1084

1085 1086 1087 1088
    if (isActionSheet && (actionsHeight > 0 || contentHeight > 0)) {
      height -= 2 * _kActionSheetEdgeVerticalPadding;
    }
    if (height.isFinite) {
1089
      return height;
1090
    }
1091 1092 1093 1094 1095
    return 0.0;
  }

  @override
  double computeMaxIntrinsicHeight(double width) {
1096 1097
    final double contentHeight = contentSection!.getMaxIntrinsicHeight(width);
    final double actionsHeight = actionsSection!.getMaxIntrinsicHeight(width);
1098
    final bool hasDivider = contentHeight > 0.0 && actionsHeight > 0.0;
1099
    double height = contentHeight + (hasDivider ? _dividerThickness : 0.0) + actionsHeight;
1100

1101 1102 1103 1104
    if (isActionSheet && (actionsHeight > 0 || contentHeight > 0)) {
      height -= 2 * _kActionSheetEdgeVerticalPadding;
    }
    if (height.isFinite) {
1105
      return height;
1106
    }
1107 1108 1109
    return 0.0;
  }

1110 1111 1112 1113 1114 1115 1116 1117
  @override
  Size computeDryLayout(BoxConstraints constraints) {
    return _performLayout(
      constraints: constraints,
      layoutChild: ChildLayoutHelper.dryLayoutChild,
    ).size;
  }

1118 1119
  @override
  void performLayout() {
1120
    final _AlertDialogSizes dialogSizes = _performLayout(
1121 1122 1123 1124 1125 1126 1127
      constraints: constraints,
      layoutChild: ChildLayoutHelper.layoutChild,
    );
    size = dialogSizes.size;

    // Set the position of the actions box to sit at the bottom of the dialog.
    // The content box defaults to the top left, which is where we want it.
1128 1129 1130 1131
    assert(
      (!isActionSheet && actionsSection!.parentData is BoxParentData) ||
          (isActionSheet && actionsSection!.parentData is MultiChildLayoutParentData),
    );
1132 1133 1134 1135 1136 1137 1138
    if (isActionSheet) {
      final MultiChildLayoutParentData actionParentData = actionsSection!.parentData! as MultiChildLayoutParentData;
      actionParentData.offset = Offset(0.0, dialogSizes.contentHeight + dialogSizes.dividerThickness);
    } else {
      final BoxParentData actionParentData = actionsSection!.parentData! as BoxParentData;
      actionParentData.offset = Offset(0.0, dialogSizes.contentHeight + dialogSizes.dividerThickness);
    }
1139 1140
  }

1141
  _AlertDialogSizes _performLayout({required BoxConstraints constraints, required ChildLayouter layoutChild}) {
1142 1143
    return isInAccessibilityMode
        ? performAccessibilityLayout(
1144 1145 1146 1147 1148 1149
            constraints: constraints,
            layoutChild: layoutChild,
          ) : performRegularLayout(
            constraints: constraints,
            layoutChild: layoutChild,
          );
1150 1151
  }

1152 1153 1154
  // When not in accessibility mode, an alert dialog might reduce the space
  // for buttons to just over 1 button's height to make room for the content
  // section.
1155 1156 1157
  _AlertDialogSizes performRegularLayout({required BoxConstraints constraints, required ChildLayouter layoutChild}) {
    final bool hasDivider = contentSection!.getMaxIntrinsicHeight(computeMaxIntrinsicWidth(0)) > 0.0
        && actionsSection!.getMaxIntrinsicHeight(computeMaxIntrinsicWidth(0)) > 0.0;
1158 1159
    final double dividerThickness = hasDivider ? _dividerThickness : 0.0;

1160
    final double minActionsHeight = actionsSection!.getMinIntrinsicHeight(computeMaxIntrinsicWidth(0));
1161

1162 1163
    final Size contentSize = layoutChild(
      contentSection!,
1164
      constraints.deflate(EdgeInsets.only(bottom: minActionsHeight + dividerThickness)),
1165 1166
    );

1167 1168
    final Size actionsSize = layoutChild(
      actionsSection!,
1169
      constraints.deflate(EdgeInsets.only(top: contentSize.height + dividerThickness)),
1170 1171 1172 1173
    );

    final double dialogHeight = contentSize.height + dividerThickness + actionsSize.height;

1174 1175 1176 1177 1178 1179
    return _AlertDialogSizes(
      size: isActionSheet
          ? Size(constraints.maxWidth, dialogHeight)
          : constraints.constrain(Size(_dialogWidth, dialogHeight)),
      contentHeight: contentSize.height,
      dividerThickness: dividerThickness,
1180 1181 1182
    );
  }

1183 1184
  // When in accessibility mode, an alert dialog will allow buttons to take
  // up to 50% of the dialog height, even if the content exceeds available space.
1185
  _AlertDialogSizes performAccessibilityLayout({required BoxConstraints constraints, required ChildLayouter layoutChild}) {
1186 1187
    final bool hasDivider = contentSection!.getMaxIntrinsicHeight(_dialogWidth) > 0.0
        && actionsSection!.getMaxIntrinsicHeight(_dialogWidth) > 0.0;
1188 1189
    final double dividerThickness = hasDivider ? _dividerThickness : 0.0;

1190 1191
    final double maxContentHeight = contentSection!.getMaxIntrinsicHeight(_dialogWidth);
    final double maxActionsHeight = actionsSection!.getMaxIntrinsicHeight(_dialogWidth);
1192

1193 1194
    final Size contentSize;
    final Size actionsSize;
1195
    if (maxContentHeight + dividerThickness + maxActionsHeight > constraints.maxHeight) {
1196 1197 1198 1199
      // AlertDialog: There isn't enough room for everything. Following iOS's
      // accessibility dialog layout policy, first we allow the actions to take
      // up to 50% of the dialog height. Second we fill the rest of the
      // available space with the content section.
1200

1201 1202
      actionsSize = layoutChild(
        actionsSection!,
1203
        constraints.deflate(EdgeInsets.only(top: constraints.maxHeight / 2.0)),
1204 1205
      );

1206 1207
      contentSize = layoutChild(
        contentSection!,
1208
        constraints.deflate(EdgeInsets.only(bottom: actionsSize.height + dividerThickness)),
1209 1210 1211 1212
      );
    } else {
      // Everything fits. Give content and actions all the space they want.

1213 1214
      contentSize = layoutChild(
        contentSection!,
1215 1216 1217
        constraints,
      );

1218 1219
      actionsSize = layoutChild(
        actionsSection!,
1220
        constraints.deflate(EdgeInsets.only(top: contentSize.height)),
1221 1222 1223 1224 1225 1226
      );
    }

    // Calculate overall dialog height.
    final double dialogHeight = contentSize.height + dividerThickness + actionsSize.height;

1227
    return _AlertDialogSizes(
1228
      size: constraints.constrain(Size(_dialogWidth, dialogHeight)),
1229 1230
      contentHeight: contentSize.height,
      dividerThickness: dividerThickness,
1231 1232 1233 1234 1235
    );
  }

  @override
  void paint(PaintingContext context, Offset offset) {
1236 1237 1238 1239 1240 1241 1242
    if (isActionSheet) {
      final MultiChildLayoutParentData contentParentData = contentSection!.parentData! as MultiChildLayoutParentData;
      contentSection!.paint(context, offset + contentParentData.offset);
    } else {
      final BoxParentData contentParentData = contentSection!.parentData! as BoxParentData;
      contentSection!.paint(context, offset + contentParentData.offset);
    }
1243

1244
    final bool hasDivider = contentSection!.size.height > 0.0 && actionsSection!.size.height > 0.0;
1245 1246 1247 1248
    if (hasDivider) {
      _paintDividerBetweenContentAndActions(context.canvas, offset);
    }

1249 1250 1251 1252 1253 1254 1255
    if (isActionSheet) {
      final MultiChildLayoutParentData actionsParentData = actionsSection!.parentData! as MultiChildLayoutParentData;
      actionsSection!.paint(context, offset + actionsParentData.offset);
    } else {
      final BoxParentData actionsParentData = actionsSection!.parentData! as BoxParentData;
      actionsSection!.paint(context, offset + actionsParentData.offset);
    }
1256 1257 1258 1259 1260 1261
  }

  void _paintDividerBetweenContentAndActions(Canvas canvas, Offset offset) {
    canvas.drawRect(
      Rect.fromLTWH(
        offset.dx,
1262
        offset.dy + contentSection!.size.height,
1263 1264
        size.width,
        _dividerThickness,
1265
      ),
1266
      _dividerPaint,
1267 1268
    );
  }
1269 1270

  @override
1271
  bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292
    if (isActionSheet) {
      final MultiChildLayoutParentData contentSectionParentData = contentSection!.parentData! as MultiChildLayoutParentData;
      final MultiChildLayoutParentData actionsSectionParentData = actionsSection!.parentData! as MultiChildLayoutParentData;
      return result.addWithPaintOffset(
            offset: contentSectionParentData.offset,
            position: position,
            hitTest: (BoxHitTestResult result, Offset transformed) {
              assert(transformed == position - contentSectionParentData.offset);
              return contentSection!.hitTest(result, position: transformed);
            },
          ) ||
          result.addWithPaintOffset(
            offset: actionsSectionParentData.offset,
            position: position,
            hitTest: (BoxHitTestResult result, Offset transformed) {
              assert(transformed == position - actionsSectionParentData.offset);
              return actionsSection!.hitTest(result, position: transformed);
            },
          );
    }

1293 1294
    final BoxParentData contentSectionParentData = contentSection!.parentData! as BoxParentData;
    final BoxParentData actionsSectionParentData = actionsSection!.parentData! as BoxParentData;
1295
    return result.addWithPaintOffset(
1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310
            offset: contentSectionParentData.offset,
            position: position,
            hitTest: (BoxHitTestResult result, Offset transformed) {
              assert(transformed == position - contentSectionParentData.offset);
              return contentSection!.hitTest(result, position: transformed);
            },
          ) ||
          result.addWithPaintOffset(
            offset: actionsSectionParentData.offset,
            position: position,
            hitTest: (BoxHitTestResult result, Offset transformed) {
              assert(transformed == position - actionsSectionParentData.offset);
              return actionsSection!.hitTest(result, position: transformed);
            },
          );
1311
  }
1312 1313
}

1314 1315 1316 1317 1318 1319
class _AlertDialogSizes {
  const _AlertDialogSizes({
    required this.size,
    required this.contentHeight,
    required this.dividerThickness,
  });
1320 1321

  final Size size;
1322 1323
  final double contentHeight;
  final double dividerThickness;
1324 1325
}

1326 1327 1328 1329 1330 1331 1332 1333
// Visual components of an alert dialog that need to be explicitly sized and
// laid out at runtime.
enum _AlertDialogSections {
  contentSection,
  actionsSection,
}

// The "content section" of a CupertinoAlertDialog.
1334
//
1335
// If title is missing, then only content is added. If content is
1336 1337
// missing, then only title is added. If both are missing, then it returns
// a SingleChildScrollView with a zero-sized Container.
1338 1339
class _CupertinoAlertContentSection extends StatelessWidget {
  const _CupertinoAlertContentSection({
1340
    this.title,
1341
    this.message,
1342
    this.scrollController,
1343 1344 1345 1346 1347 1348
    this.titlePadding,
    this.messagePadding,
    this.titleTextStyle,
    this.messageTextStyle,
    this.additionalPaddingBetweenTitleAndMessage,
  }) : assert(title == null || titlePadding != null && titleTextStyle != null),
1349
       assert(message == null || messagePadding != null && messageTextStyle != null);
1350

1351 1352 1353 1354
  // The (optional) title of the dialog is displayed in a large font at the top
  // of the dialog.
  //
  // Typically a Text widget.
1355
  final Widget? title;
1356

1357
  // The (optional) message of the dialog is displayed in the center of the
1358 1359 1360
  // dialog in a lighter font.
  //
  // Typically a Text widget.
1361
  final Widget? message;
1362 1363 1364 1365 1366 1367

  // A scroll controller that can be used to control the scrolling of the
  // content in the dialog.
  //
  // Defaults to null, and is typically not needed, since most alert contents
  // are short.
1368
  final ScrollController? scrollController;
1369

1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383
  // Paddings used around title and message.
  // CupertinoAlertDialog and CupertinoActionSheet have different paddings.
  final EdgeInsets? titlePadding;
  final EdgeInsets? messagePadding;

  // Additional padding to be inserted between title and message.
  // Only used for CupertinoActionSheet.
  final EdgeInsets? additionalPaddingBetweenTitleAndMessage;

  // Text styles used for title and message.
  // CupertinoAlertDialog and CupertinoActionSheet have different text styles.
  final TextStyle? titleTextStyle;
  final TextStyle? messageTextStyle;

1384 1385
  @override
  Widget build(BuildContext context) {
1386
    if (title == null && message == null) {
1387 1388
      return SingleChildScrollView(
        controller: scrollController,
1389
        child: const SizedBox.shrink(),
1390
      );
1391 1392
    }

1393 1394 1395
    final List<Widget> titleContentGroup = <Widget>[
      if (title != null)
        Padding(
1396
          padding: titlePadding!,
1397
          child: DefaultTextStyle(
1398
            style: titleTextStyle!,
1399
            textAlign: TextAlign.center,
1400
            child: title!,
1401 1402
          ),
        ),
1403
      if (message != null)
1404
        Padding(
1405
          padding: messagePadding!,
1406
          child: DefaultTextStyle(
1407
            style: messageTextStyle!,
1408
            textAlign: TextAlign.center,
1409
            child: message!,
1410 1411
          ),
        ),
1412
    ];
1413

1414 1415 1416 1417 1418
    // Add padding between the widgets if necessary.
    if (additionalPaddingBetweenTitleAndMessage != null && titleContentGroup.length > 1) {
      titleContentGroup.insert(1, Padding(padding: additionalPaddingBetweenTitleAndMessage!));
    }

1419
    return CupertinoScrollbar(
1420
      controller: scrollController,
1421
      child: SingleChildScrollView(
1422
        controller: scrollController,
1423
        child: Column(
1424 1425
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: titleContentGroup,
1426
        ),
1427
      ),
1428 1429 1430 1431
    );
  }
}

1432
// The "actions section" of a [CupertinoAlertDialog].
1433
//
1434 1435
// See [_RenderCupertinoDialogActions] for details about action button sizing
// and layout.
1436
class _CupertinoAlertActionSection extends StatelessWidget {
1437
  const _CupertinoAlertActionSection({
1438
    required this.children,
1439
    this.scrollController,
1440 1441
    this.hasCancelButton = false,
    this.isActionSheet = false,
1442
  });
1443 1444 1445 1446 1447 1448 1449 1450

  final List<Widget> children;

  // A scroll controller that can be used to control the scrolling of the
  // actions in the dialog.
  //
  // Defaults to null, and is typically not needed, since most alert dialogs
  // don't have many actions.
1451
  final ScrollController? scrollController;
1452

1453 1454 1455 1456 1457 1458 1459 1460
  // Used in ActionSheet to denote if ActionSheet has a separate so-called
  // cancel button.
  //
  // Defaults to false, and is not needed in dialogs.
  final bool hasCancelButton;

  final bool isActionSheet;

1461 1462
  @override
  Widget build(BuildContext context) {
1463
    final double devicePixelRatio = MediaQuery.devicePixelRatioOf(context);
1464

1465
    final List<Widget> interactiveButtons = <Widget>[];
1466
    for (int i = 0; i < children.length; i += 1) {
1467
      interactiveButtons.add(
1468
        _PressableActionButton(
1469
          child: children[i],
1470 1471
        ),
      );
1472
    }
1473

1474
    return CupertinoScrollbar(
1475
      controller: scrollController,
1476
      child: SingleChildScrollView(
1477
        controller: scrollController,
1478
        child: _CupertinoDialogActionsRenderWidget(
1479 1480
          actionButtons: interactiveButtons,
          dividerThickness: _kDividerThickness / devicePixelRatio,
1481 1482
          hasCancelButton: hasCancelButton,
          isActionSheet: isActionSheet,
1483
        ),
1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495
      ),
    );
  }
}

// Button that updates its render state when pressed.
//
// The pressed state is forwarded to an _ActionButtonParentDataWidget. The
// corresponding _ActionButtonParentData is then interpreted and rendered
// appropriately by _RenderCupertinoDialogActions.
class _PressableActionButton extends StatefulWidget {
  const _PressableActionButton({
1496
    required this.child,
1497 1498 1499 1500 1501
  });

  final Widget child;

  @override
1502
  _PressableActionButtonState createState() => _PressableActionButtonState();
1503 1504 1505 1506 1507 1508 1509
}

class _PressableActionButtonState extends State<_PressableActionButton> {
  bool _isPressed = false;

  @override
  Widget build(BuildContext context) {
1510
    return _ActionButtonParentDataWidget(
1511
      isPressed: _isPressed,
1512
      child: MergeSemantics(
1513 1514
        // TODO(mattcarroll): Button press dynamics need overhaul for iOS:
        // https://github.com/flutter/flutter/issues/19786
1515
        child: GestureDetector(
1516 1517 1518 1519 1520 1521 1522 1523
          excludeFromSemantics: true,
          behavior: HitTestBehavior.opaque,
          onTapDown: (TapDownDetails details) => setState(() {
            _isPressed = true;
          }),
          onTapUp: (TapUpDetails details) => setState(() {
            _isPressed = false;
          }),
1524 1525
          // TODO(mattcarroll): Cancel is currently triggered when user moves
          //  past slop instead of off button: https://github.com/flutter/flutter/issues/19783
1526 1527 1528
          onTapCancel: () => setState(() => _isPressed = false),
          child: widget.child,
        ),
1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540
      ),
    );
  }
}

// ParentDataWidget that updates _ActionButtonParentData for an action button.
//
// Each action button requires knowledge of whether or not it is pressed so that
// the dialog can correctly render the button. The pressed state is held within
// _ActionButtonParentData. _ActionButtonParentDataWidget is responsible for
// updating the pressed state of an _ActionButtonParentData based on the
// incoming [isPressed] property.
1541 1542
class _ActionButtonParentDataWidget
    extends ParentDataWidget<_ActionButtonParentData> {
1543
  const _ActionButtonParentDataWidget({
1544
    required this.isPressed,
1545 1546
    required super.child,
  });
1547 1548 1549 1550 1551 1552

  final bool isPressed;

  @override
  void applyParentData(RenderObject renderObject) {
    assert(renderObject.parentData is _ActionButtonParentData);
1553 1554
    final _ActionButtonParentData parentData =
        renderObject.parentData! as _ActionButtonParentData;
1555 1556 1557 1558
    if (parentData.isPressed != isPressed) {
      parentData.isPressed = isPressed;

      // Force a repaint.
1559
      final AbstractNode? targetParent = renderObject.parent;
1560
      if (targetParent is RenderObject) {
1561
        targetParent.markNeedsPaint();
1562
      }
1563 1564
    }
  }
1565 1566

  @override
1567 1568
  Type get debugTypicalAncestorWidgetClass =>
      _CupertinoDialogActionsRenderWidget;
1569 1570 1571 1572 1573
}

// ParentData applied to individual action buttons that report whether or not
// that button is currently pressed by the user.
class _ActionButtonParentData extends MultiChildLayoutParentData {
1574
  bool isPressed = false;
1575 1576 1577 1578 1579 1580 1581
}

/// A button typically used in a [CupertinoAlertDialog].
///
/// See also:
///
///  * [CupertinoAlertDialog], a dialog that informs the user about situations
1582
///    that require acknowledgment.
1583 1584 1585
class CupertinoDialogAction extends StatelessWidget {
  /// Creates an action for an iOS-style dialog.
  const CupertinoDialogAction({
1586
    super.key,
1587 1588 1589 1590
    this.onPressed,
    this.isDefaultAction = false,
    this.isDestructiveAction = false,
    this.textStyle,
1591
    required this.child,
1592
  });
1593 1594 1595 1596 1597

  /// The callback that is called when the button is tapped or otherwise
  /// activated.
  ///
  /// If this is set to null, the button will be disabled.
1598
  final VoidCallback? onPressed;
1599 1600 1601

  /// Set to true if button is the default choice in the dialog.
  ///
1602 1603 1604 1605
  /// Default buttons have bold text. Similar to
  /// [UIAlertController.preferredAction](https://developer.apple.com/documentation/uikit/uialertcontroller/1620102-preferredaction),
  /// but more than one action can have this attribute set to true in the same
  /// [CupertinoAlertDialog].
1606 1607
  ///
  /// This parameters defaults to false and cannot be null.
1608 1609 1610 1611 1612
  final bool isDefaultAction;

  /// Whether this action destroys an object.
  ///
  /// For example, an action that deletes an email is destructive.
1613 1614
  ///
  /// Defaults to false and cannot be null.
1615 1616 1617 1618 1619 1620 1621 1622
  final bool isDestructiveAction;

  /// [TextStyle] to apply to any text that appears in this button.
  ///
  /// Dialog actions have a built-in text resizing policy for long text. To
  /// ensure that this resizing policy always works as expected, [textStyle]
  /// must be used if a text size is desired other than that specified in
  /// [_kCupertinoDialogActionStyle].
1623
  final TextStyle? textStyle;
1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643

  /// The widget below this widget in the tree.
  ///
  /// Typically a [Text] widget.
  final Widget child;

  /// Whether the button is enabled or disabled. Buttons are disabled by
  /// default. To enable a button, set its [onPressed] property to a non-null
  /// value.
  bool get enabled => onPressed != null;

  double _calculatePadding(BuildContext context) {
    return 8.0 * MediaQuery.textScaleFactorOf(context);
  }

  // Dialog action content shrinks to fit, up to a certain point, and if it still
  // cannot fit at the minimum size, the text content is ellipsized.
  //
  // This policy only applies when the device is not in accessibility mode.
  Widget _buildContentWithRegularSizingPolicy({
1644 1645 1646
    required BuildContext context,
    required TextStyle textStyle,
    required Widget content,
1647 1648 1649 1650 1651 1652 1653 1654 1655 1656
  }) {
    final bool isInAccessibilityMode = _isInAccessibilityMode(context);
    final double dialogWidth = isInAccessibilityMode
        ? _kAccessibilityCupertinoDialogWidth
        : _kCupertinoDialogWidth;
    final double textScaleFactor = MediaQuery.textScaleFactorOf(context);
    // The fontSizeRatio is the ratio of the current text size (including any
    // iOS scale factor) vs the minimum text size that we allow in action
    // buttons. This ratio information is used to automatically scale down action
    // button text to fit the available space.
1657
    final double fontSizeRatio = (textScaleFactor * textStyle.fontSize!) / _kDialogMinButtonFontSize;
1658 1659
    final double padding = _calculatePadding(context);

1660 1661
    return IntrinsicHeight(
      child: SizedBox(
1662
        width: double.infinity,
1663
        child: FittedBox(
1664
          fit: BoxFit.scaleDown,
1665 1666
          child: ConstrainedBox(
            constraints: BoxConstraints(
1667 1668
              maxWidth: fontSizeRatio * (dialogWidth - (2 * padding)),
            ),
1669
            child: Semantics(
1670 1671
              button: true,
              onTap: onPressed,
1672
              child: DefaultTextStyle(
1673 1674 1675 1676 1677 1678
                style: textStyle,
                textAlign: TextAlign.center,
                overflow: TextOverflow.ellipsis,
                maxLines: 1,
                child: content,
              ),
1679 1680 1681
            ),
          ),
        ),
1682 1683 1684 1685 1686 1687 1688 1689
      ),
    );
  }

  // Dialog action content is permitted to be as large as it wants when in
  // accessibility mode. If text is used as the content, the text wraps instead
  // of ellipsizing.
  Widget _buildContentWithAccessibilitySizingPolicy({
1690 1691
    required TextStyle textStyle,
    required Widget content,
1692
  }) {
1693
    return DefaultTextStyle(
1694 1695 1696 1697 1698 1699 1700 1701
      style: textStyle,
      textAlign: TextAlign.center,
      child: content,
    );
  }

  @override
  Widget build(BuildContext context) {
1702 1703
    TextStyle style = _kCupertinoDialogActionStyle.copyWith(
      color: CupertinoDynamicColor.resolve(
1704
        isDestructiveAction ? CupertinoColors.systemRed : CupertinoTheme.of(context).primaryColor,
1705
        context,
1706 1707
      ),
    );
1708 1709
    style = style.merge(textStyle);

1710 1711 1712 1713
    if (isDefaultAction) {
      style = style.copyWith(fontWeight: FontWeight.w600);
    }

1714
    if (!enabled) {
1715
      style = style.copyWith(color: style.color!.withOpacity(0.5));
1716
    }
1717 1718 1719 1720 1721 1722 1723 1724

    // Apply a sizing policy to the action button's content based on whether or
    // not the device is in accessibility mode.
    // TODO(mattcarroll): The following logic is not entirely correct. It is also
    // the case that if content text does not contain a space, it should also
    // wrap instead of ellipsizing. We are consciously not implementing that
    // now due to complexity.
    final Widget sizedContent = _isInAccessibilityMode(context)
1725 1726 1727 1728 1729 1730 1731 1732 1733
        ? _buildContentWithAccessibilitySizingPolicy(
            textStyle: style,
            content: child,
          )
        : _buildContentWithRegularSizingPolicy(
            context: context,
            textStyle: style,
            content: child,
          );
1734

1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749
    return MouseRegion(
      cursor: onPressed != null && kIsWeb ? SystemMouseCursors.click : MouseCursor.defer,
      child: GestureDetector(
        excludeFromSemantics: true,
        onTap: onPressed,
        behavior: HitTestBehavior.opaque,
        child: ConstrainedBox(
          constraints: const BoxConstraints(
            minHeight: _kDialogMinButtonHeight,
          ),
          child: Container(
            alignment: Alignment.center,
            padding: EdgeInsets.all(_calculatePadding(context)),
            child: sizedContent,
          ),
1750 1751 1752
        ),
      ),
    );
1753 1754 1755
  }
}

1756
// iOS style dialog action button layout.
1757
//
1758 1759 1760 1761 1762 1763
// [_CupertinoDialogActionsRenderWidget] does not provide any scrolling
// behavior for its buttons. It only handles the sizing and layout of buttons.
// Scrolling behavior can be composed on top of this widget, if desired.
//
// See [_RenderCupertinoDialogActions] for specific layout policy details.
class _CupertinoDialogActionsRenderWidget extends MultiChildRenderObjectWidget {
1764
  const _CupertinoDialogActionsRenderWidget({
1765
    required List<Widget> actionButtons,
1766
    double dividerThickness = 0.0,
1767 1768
    bool hasCancelButton = false,
    bool isActionSheet = false,
1769
  }) : _dividerThickness = dividerThickness,
1770 1771
       _hasCancelButton = hasCancelButton,
       _isActionSheet = isActionSheet,
1772
       super(children: actionButtons);
1773

1774
  final double _dividerThickness;
1775 1776
  final bool _hasCancelButton;
  final bool _isActionSheet;
1777 1778

  @override
1779
  RenderObject createRenderObject(BuildContext context) {
1780
    return _RenderCupertinoDialogActions(
1781 1782 1783 1784 1785
      dialogWidth: _isActionSheet
          ? null
          : _isInAccessibilityMode(context)
              ? _kAccessibilityCupertinoDialogWidth
              : _kCupertinoDialogWidth,
1786
      dividerThickness: _dividerThickness,
1787 1788 1789 1790 1791
      dialogColor: CupertinoDynamicColor.resolve(_isActionSheet ? _kActionSheetBackgroundColor : _kDialogColor, context),
      dialogPressedColor: CupertinoDynamicColor.resolve(_kPressedColor, context),
      dividerColor: CupertinoDynamicColor.resolve(_isActionSheet ? _kActionSheetButtonDividerColor : CupertinoColors.separator, context),
      hasCancelButton: _hasCancelButton,
      isActionSheet: _isActionSheet,
1792
    );
1793 1794 1795
  }

  @override
1796
  void updateRenderObject(BuildContext context, _RenderCupertinoDialogActions renderObject) {
1797
    renderObject
1798 1799 1800 1801 1802
      ..dialogWidth = _isActionSheet
          ? null
          : _isInAccessibilityMode(context)
            ? _kAccessibilityCupertinoDialogWidth
            : _kCupertinoDialogWidth
1803
      ..dividerThickness = _dividerThickness
1804 1805 1806 1807 1808
      ..dialogColor = CupertinoDynamicColor.resolve(_isActionSheet ? _kActionSheetBackgroundColor : _kDialogColor, context)
      ..dialogPressedColor = CupertinoDynamicColor.resolve(_kPressedColor, context)
      ..dividerColor = CupertinoDynamicColor.resolve(_isActionSheet ? _kActionSheetButtonDividerColor : CupertinoColors.separator, context)
      ..hasCancelButton = _hasCancelButton
      ..isActionSheet = _isActionSheet;
1809
  }
1810 1811
}

1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830
// iOS style layout policy for sizing and positioning an alert dialog's action
// buttons.
//
// The policy is as follows:
//
// If a single action button is provided, or if 2 action buttons are provided
// that can fit side-by-side, then action buttons are sized and laid out in a
// single horizontal row. The row is exactly as wide as the dialog, and the row
// is as tall as the tallest action button. A horizontal divider is drawn above
// the button row. If 2 action buttons are provided, a vertical divider is
// drawn between them. The thickness of the divider is set by [dividerThickness].
//
// If 2 action buttons are provided but they cannot fit side-by-side, then the
// 2 buttons are stacked vertically. A horizontal divider is drawn above each
// button. The thickness of the divider is set by [dividerThickness]. The minimum
// height of this [RenderBox] in the case of 2 stacked buttons is as tall as
// the 2 buttons stacked. This is different than the 3+ button case where the
// minimum height is only 1.5 buttons tall. See the 3+ button explanation for
// more info.
1831
//
1832 1833 1834 1835 1836 1837 1838 1839 1840 1841 1842 1843 1844 1845 1846 1847 1848 1849
// If 3+ action buttons are provided then they are all stacked vertically. A
// horizontal divider is drawn above each button. The thickness of the divider
// is set by [dividerThickness]. The minimum height of this [RenderBox] in the case
// of 3+ stacked buttons is as tall as the 1st button + 50% the height of the
// 2nd button. In other words, the minimum height is 1.5 buttons tall. This
// minimum height of 1.5 buttons is expected to work in tandem with a surrounding
// [ScrollView] to match the iOS dialog behavior.
//
// Each button is expected to have an _ActionButtonParentData which reports
// whether or not that button is currently pressed. If a button is pressed,
// then the dividers above and below that pressed button are not drawn - instead
// they are filled with the standard white dialog background color. The one
// exception is the very 1st divider which is always rendered. This policy comes
// from observation of native iOS dialogs.
class _RenderCupertinoDialogActions extends RenderBox
    with ContainerRenderObjectMixin<RenderBox, MultiChildLayoutParentData>,
        RenderBoxContainerDefaultsMixin<RenderBox, MultiChildLayoutParentData> {
  _RenderCupertinoDialogActions({
1850
    List<RenderBox>? children,
1851
    double? dialogWidth,
1852
    double dividerThickness = 0.0,
1853 1854 1855
    required Color dialogColor,
    required Color dialogPressedColor,
    required Color dividerColor,
1856 1857 1858 1859
    bool hasCancelButton = false,
    bool isActionSheet = false,
  }) : assert(isActionSheet || dialogWidth != null),
       _dialogWidth = dialogWidth,
1860
       _buttonBackgroundPaint = Paint()
1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871
         ..color = dialogColor
         ..style = PaintingStyle.fill,
       _pressedButtonBackgroundPaint = Paint()
         ..color = dialogPressedColor
         ..style = PaintingStyle.fill,
       _dividerPaint = Paint()
         ..color = dividerColor
         ..style = PaintingStyle.fill,
       _dividerThickness = dividerThickness,
       _hasCancelButton = hasCancelButton,
       _isActionSheet = isActionSheet {
1872 1873 1874
    addAll(children);
  }

1875 1876 1877
  double? get dialogWidth => _dialogWidth;
  double? _dialogWidth;
  set dialogWidth(double? newWidth) {
1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 1893
    if (newWidth != _dialogWidth) {
      _dialogWidth = newWidth;
      markNeedsLayout();
    }
  }

  // The thickness of the divider between buttons.
  double get dividerThickness => _dividerThickness;
  double _dividerThickness;
  set dividerThickness(double newValue) {
    if (newValue != _dividerThickness) {
      _dividerThickness = newValue;
      markNeedsLayout();
    }
  }

1894 1895 1896
  bool _hasCancelButton;
  bool get hasCancelButton => _hasCancelButton;
  set hasCancelButton(bool newValue) {
1897
    if (newValue == _hasCancelButton) {
1898
      return;
1899
    }
1900 1901 1902 1903 1904

    _hasCancelButton = newValue;
    markNeedsLayout();
  }

1905
  Color get dialogColor => _buttonBackgroundPaint.color;
1906 1907
  final Paint _buttonBackgroundPaint;
  set dialogColor(Color value) {
1908
    if (value == _buttonBackgroundPaint.color) {
1909
      return;
1910
    }
1911 1912 1913 1914

    _buttonBackgroundPaint.color = value;
    markNeedsPaint();
  }
1915

1916
  Color get dialogPressedColor => _pressedButtonBackgroundPaint.color;
1917 1918
  final Paint _pressedButtonBackgroundPaint;
  set dialogPressedColor(Color value) {
1919
    if (value == _pressedButtonBackgroundPaint.color) {
1920
      return;
1921
    }
1922

1923 1924 1925 1926
    _pressedButtonBackgroundPaint.color = value;
    markNeedsPaint();
  }

1927
  Color get dividerColor => _dividerPaint.color;
1928 1929
  final Paint _dividerPaint;
  set dividerColor(Color value) {
1930
    if (value == _dividerPaint.color) {
1931
      return;
1932
    }
1933 1934 1935 1936

    _dividerPaint.color = value;
    markNeedsPaint();
  }
1937

1938
  bool get isActionSheet => _isActionSheet;
1939
  bool _isActionSheet;
1940
  set isActionSheet(bool value) {
1941
    if (value == _isActionSheet) {
1942
      return;
1943
    }
1944 1945 1946 1947 1948

    _isActionSheet = value;
    markNeedsPaint();
  }

1949 1950
  Iterable<RenderBox> get _pressedButtons {
    final List<RenderBox> boxes = <RenderBox>[];
1951
    RenderBox? currentChild = firstChild;
1952 1953
    while (currentChild != null) {
      assert(currentChild.parentData is _ActionButtonParentData);
1954
      final _ActionButtonParentData parentData = currentChild.parentData! as _ActionButtonParentData;
1955
      if (parentData.isPressed) {
1956
        boxes.add(currentChild);
1957 1958 1959
      }
      currentChild = childAfter(currentChild);
    }
1960
    return boxes;
1961 1962 1963
  }

  bool get _isButtonPressed {
1964
    RenderBox? currentChild = firstChild;
1965 1966
    while (currentChild != null) {
      assert(currentChild.parentData is _ActionButtonParentData);
1967
      final _ActionButtonParentData parentData = currentChild.parentData! as _ActionButtonParentData;
1968 1969 1970 1971 1972 1973 1974
      if (parentData.isPressed) {
        return true;
      }
      currentChild = childAfter(currentChild);
    }
    return false;
  }
1975 1976

  @override
1977
  void setupParentData(RenderBox child) {
1978
    if (child.parentData is! _ActionButtonParentData) {
1979
      child.parentData = _ActionButtonParentData();
1980
    }
1981 1982 1983
  }

  @override
1984
  double computeMinIntrinsicWidth(double height) {
1985
    return isActionSheet ? constraints.minWidth : dialogWidth!;
1986 1987 1988 1989
  }

  @override
  double computeMaxIntrinsicWidth(double height) {
1990
    return isActionSheet ? constraints.maxWidth : dialogWidth!;
1991 1992 1993 1994 1995
  }

  @override
  double computeMinIntrinsicHeight(double width) {
    if (childCount == 0) {
1996 1997
      return 0.0;
    } else if (isActionSheet) {
1998
      if (childCount == 1) {
1999
        return firstChild!.computeMaxIntrinsicHeight(width) + dividerThickness;
2000 2001
      }
      if (hasCancelButton && childCount < 4) {
2002
        return _computeMinIntrinsicHeightWithCancel(width);
2003
      }
2004
      return _computeMinIntrinsicHeightStacked(width);
2005 2006
    } else if (childCount == 1) {
      // If only 1 button, display the button across the entire dialog.
2007 2008 2009 2010
      return _computeMinIntrinsicHeightSideBySide(width);
    } else if (childCount == 2 && _isSingleButtonRow(width)) {
      // The first 2 buttons fit side-by-side. Display them horizontally.
      return _computeMinIntrinsicHeightSideBySide(width);
2011
    }
2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 2025 2026 2027 2028 2029
    // 3+ buttons are always stacked. The minimum height when stacked is
    // 1.5 buttons tall.
    return _computeMinIntrinsicHeightStacked(width);
  }

  // The minimum height for more than 2-3 buttons when a cancel button is
  // included is the full height of button stack.
  double _computeMinIntrinsicHeightWithCancel(double width) {
    assert(childCount == 2 || childCount == 3);
    if (childCount == 2) {
      return firstChild!.getMinIntrinsicHeight(width)
          + childAfter(firstChild!)!.getMinIntrinsicHeight(width)
          + dividerThickness;
    }
    return firstChild!.getMinIntrinsicHeight(width)
        + childAfter(firstChild!)!.getMinIntrinsicHeight(width)
        + childAfter(childAfter(firstChild!)!)!.getMinIntrinsicHeight(width)
        + (dividerThickness * 2);
2030 2031 2032 2033 2034 2035 2036
  }

  // The minimum height for a single row of buttons is the larger of the buttons'
  // min intrinsic heights.
  double _computeMinIntrinsicHeightSideBySide(double width) {
    assert(childCount >= 1 && childCount <= 2);

2037
    final double minHeight;
2038
    if (childCount == 1) {
2039
      minHeight = firstChild!.getMinIntrinsicHeight(width);
2040 2041 2042
    } else {
      final double perButtonWidth = (width - dividerThickness) / 2.0;
      minHeight = math.max(
2043 2044
        firstChild!.getMinIntrinsicHeight(perButtonWidth),
        lastChild!.getMinIntrinsicHeight(perButtonWidth),
2045 2046 2047 2048 2049
      );
    }
    return minHeight;
  }

2050 2051 2052 2053 2054 2055
  // Dialog: The minimum height for 2+ stacked buttons is the height of the 1st
  // button + 50% the height of the 2nd button + the divider between the two.
  //
  // ActionSheet: The minimum height for more than 2 buttons when no cancel
  // button or 4+ buttons when a cancel button is included is the height of the
  // 1st button + 50% the height of the 2nd button + 2 dividers.
2056 2057 2058
  double _computeMinIntrinsicHeightStacked(double width) {
    assert(childCount >= 2);

2059
    return firstChild!.getMinIntrinsicHeight(width)
2060 2061
        + dividerThickness
        + (0.5 * childAfter(firstChild!)!.getMinIntrinsicHeight(width));
2062 2063 2064 2065 2066 2067
  }

  @override
  double computeMaxIntrinsicHeight(double width) {
    if (childCount == 0) {
      // No buttons. Zero height.
2068 2069
      return 0.0;
    } else if (isActionSheet) {
2070
      if (childCount == 1) {
2071
        return firstChild!.computeMaxIntrinsicHeight(width) + dividerThickness;
2072
      }
2073
      return _computeMaxIntrinsicHeightStacked(width);
2074 2075
    } else if (childCount == 1) {
      // One button. Our max intrinsic height is equal to the button's.
2076
      return firstChild!.getMaxIntrinsicHeight(width);
2077 2078 2079 2080 2081 2082
    } else if (childCount == 2) {
      // Two buttons...
      if (_isSingleButtonRow(width)) {
        // The 2 buttons fit side by side so our max intrinsic height is equal
        // to the taller of the 2 buttons.
        final double perButtonWidth = (width - dividerThickness) / 2.0;
2083
        return math.max(
2084 2085
          firstChild!.getMaxIntrinsicHeight(perButtonWidth),
          lastChild!.getMaxIntrinsicHeight(perButtonWidth),
2086 2087 2088 2089
        );
      } else {
        // The 2 buttons do not fit side by side. Measure total height as a
        // vertical stack.
2090
        return _computeMaxIntrinsicHeightStacked(width);
2091 2092
      }
    }
2093 2094 2095
    // Three+ buttons. Stack the buttons vertically with dividers and measure
    // the overall height.
    return _computeMaxIntrinsicHeightStacked(width);
2096 2097 2098 2099 2100 2101 2102 2103 2104
  }

  // Max height of a stack of buttons is the sum of all button heights + a
  // divider for each button.
  double _computeMaxIntrinsicHeightStacked(double width) {
    assert(childCount >= 2);

    final double allDividersHeight = (childCount - 1) * dividerThickness;
    double heightAccumulation = allDividersHeight;
2105
    RenderBox? button = firstChild;
2106 2107 2108 2109 2110 2111 2112 2113
    while (button != null) {
      heightAccumulation += button.getMaxIntrinsicHeight(width);
      button = childAfter(button);
    }
    return heightAccumulation;
  }

  bool _isSingleButtonRow(double width) {
2114
    final bool isSingleButtonRow;
2115 2116 2117 2118 2119
    if (childCount == 1) {
      isSingleButtonRow = true;
    } else if (childCount == 2) {
      // There are 2 buttons. If they can fit side-by-side then that's what
      // we want to do. Otherwise, stack them vertically.
2120
      final double sideBySideWidth = firstChild!.getMaxIntrinsicWidth(double.infinity)
2121
          + dividerThickness
2122
          + lastChild!.getMaxIntrinsicWidth(double.infinity);
2123 2124 2125 2126 2127 2128 2129
      isSingleButtonRow = sideBySideWidth <= width;
    } else {
      isSingleButtonRow = false;
    }
    return isSingleButtonRow;
  }

2130 2131
  @override
  Size computeDryLayout(BoxConstraints constraints) {
2132
    return _performLayout(constraints: constraints, dry: true);
2133 2134
  }

2135 2136
  @override
  void performLayout() {
2137
    size = _performLayout(constraints: constraints);
2138 2139
  }

2140
  Size _performLayout({required BoxConstraints constraints, bool dry = false}) {
2141 2142 2143 2144
    final ChildLayouter layoutChild = dry
        ? ChildLayoutHelper.dryLayoutChild
        : ChildLayoutHelper.layoutChild;

2145
    if (!isActionSheet && _isSingleButtonRow(dialogWidth!)) {
2146 2147 2148
      if (childCount == 1) {
        // We have 1 button. Our size is the width of the dialog and the height
        // of the single button.
2149 2150
        final Size childSize = layoutChild(
          firstChild!,
2151 2152 2153
          constraints,
        );

2154
        return constraints.constrain(
2155
          Size(dialogWidth!, childSize.height),
2156 2157 2158
        );
      } else {
        // Each button gets half the available width, minus a single divider.
2159
        final BoxConstraints perButtonConstraints = BoxConstraints(
2160 2161 2162 2163 2164
          minWidth: (constraints.minWidth - dividerThickness) / 2.0,
          maxWidth: (constraints.maxWidth - dividerThickness) / 2.0,
        );

        // Layout the 2 buttons.
2165 2166
        final Size firstChildSize = layoutChild(
          firstChild!,
2167 2168
          perButtonConstraints,
        );
2169 2170
        final Size lastChildSize = layoutChild(
          lastChild!,
2171 2172 2173
          perButtonConstraints,
        );

2174 2175 2176 2177 2178 2179
        if (!dry) {
          // The 2nd button needs to be offset to the right.
          assert(lastChild!.parentData is MultiChildLayoutParentData);
          final MultiChildLayoutParentData secondButtonParentData = lastChild!.parentData! as MultiChildLayoutParentData;
          secondButtonParentData.offset = Offset(firstChildSize.width + dividerThickness, 0.0);
        }
2180 2181

        // Calculate our size based on the button sizes.
2182
        return constraints.constrain(
2183
          Size(
2184
            dialogWidth!,
2185
            math.max(
2186 2187
              firstChildSize.height,
              lastChildSize.height,
2188
            ),
2189
          ),
2190 2191 2192 2193 2194 2195 2196 2197 2198
        );
      }
    } else {
      // We need to stack buttons vertically, plus dividers above each button (except the 1st).
      final BoxConstraints perButtonConstraints = constraints.copyWith(
        minHeight: 0.0,
        maxHeight: double.infinity,
      );

2199
      RenderBox? child = firstChild;
2200 2201 2202
      int index = 0;
      double verticalOffset = 0.0;
      while (child != null) {
2203 2204
        final Size childSize = layoutChild(
          child,
2205 2206 2207
          perButtonConstraints,
        );

2208 2209 2210 2211 2212 2213
        if (!dry) {
          assert(child.parentData is MultiChildLayoutParentData);
          final MultiChildLayoutParentData parentData = child.parentData! as MultiChildLayoutParentData;
          parentData.offset = Offset(0.0, verticalOffset);
        }
        verticalOffset += childSize.height;
2214 2215 2216 2217 2218 2219 2220 2221 2222 2223
        if (index < childCount - 1) {
          // Add a gap for the next divider.
          verticalOffset += dividerThickness;
        }

        index += 1;
        child = childAfter(child);
      }

      // Our height is the accumulated height of all buttons and dividers.
2224
      return constraints.constrain(
2225
        Size(computeMaxIntrinsicWidth(0), verticalOffset),
2226 2227 2228 2229 2230 2231 2232 2233
      );
    }
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    final Canvas canvas = context.canvas;

2234
    if (!isActionSheet && _isSingleButtonRow(size.width)) {
2235 2236 2237 2238 2239 2240 2241 2242 2243 2244
      _drawButtonBackgroundsAndDividersSingleRow(canvas, offset);
    } else {
      _drawButtonBackgroundsAndDividersStacked(canvas, offset);
    }

    _drawButtons(context, offset);
  }

  void _drawButtonBackgroundsAndDividersSingleRow(Canvas canvas, Offset offset) {
    // The vertical divider sits between the left button and right button (if
2245
    // the dialog has 2 buttons). The vertical divider is hidden if either the
2246 2247
    // left or right button is pressed.
    final Rect verticalDivider = childCount == 2 && !_isButtonPressed
2248 2249 2250 2251 2252 2253 2254 2255 2256 2257
        ? Rect.fromLTWH(
            offset.dx + firstChild!.size.width,
            offset.dy,
            dividerThickness,
            math.max(
              firstChild!.size.height,
              lastChild!.size.height,
            ),
          )
        : Rect.zero;
2258

2259
    final List<Rect> pressedButtonRects = _pressedButtons.map<Rect>((RenderBox pressedButton) {
2260
      final MultiChildLayoutParentData buttonParentData = pressedButton.parentData! as MultiChildLayoutParentData;
2261

2262
      return Rect.fromLTWH(
2263 2264 2265 2266 2267 2268 2269 2270
        offset.dx + buttonParentData.offset.dx,
        offset.dy + buttonParentData.offset.dy,
        pressedButton.size.width,
        pressedButton.size.height,
      );
    }).toList();

    // Create the button backgrounds path and paint it.
2271
    final Path backgroundFillPath = Path()
2272
      ..fillType = PathFillType.evenOdd
2273
      ..addRect(Rect.fromLTWH(0.0, 0.0, size.width, size.height))
2274 2275 2276 2277 2278 2279 2280 2281 2282 2283 2284 2285
      ..addRect(verticalDivider);

    for (int i = 0; i < pressedButtonRects.length; i += 1) {
      backgroundFillPath.addRect(pressedButtonRects[i]);
    }

    canvas.drawPath(
      backgroundFillPath,
      _buttonBackgroundPaint,
    );

    // Create the pressed buttons background path and paint it.
2286
    final Path pressedBackgroundFillPath = Path();
2287 2288 2289 2290 2291 2292 2293 2294 2295 2296
    for (int i = 0; i < pressedButtonRects.length; i += 1) {
      pressedBackgroundFillPath.addRect(pressedButtonRects[i]);
    }

    canvas.drawPath(
      pressedBackgroundFillPath,
      _pressedButtonBackgroundPaint,
    );

    // Create the dividers path and paint it.
2297
    final Path dividersPath = Path()
2298 2299 2300 2301 2302 2303 2304 2305 2306
      ..addRect(verticalDivider);

    canvas.drawPath(
      dividersPath,
      _dividerPaint,
    );
  }

  void _drawButtonBackgroundsAndDividersStacked(Canvas canvas, Offset offset) {
2307
    final Offset dividerOffset = Offset(0.0, dividerThickness);
2308

2309
    final Path backgroundFillPath = Path()
2310
      ..fillType = PathFillType.evenOdd
2311
      ..addRect(Rect.fromLTWH(0.0, 0.0, size.width, size.height));
2312

2313
    final Path pressedBackgroundFillPath = Path();
2314

2315
    final Path dividersPath = Path();
2316 2317 2318

    Offset accumulatingOffset = offset;

2319 2320
    RenderBox? child = firstChild;
    RenderBox? prevChild;
2321 2322
    while (child != null) {
      assert(child.parentData is _ActionButtonParentData);
2323
      final _ActionButtonParentData currentButtonParentData = child.parentData! as _ActionButtonParentData;
2324 2325 2326 2327 2328
      final bool isButtonPressed = currentButtonParentData.isPressed;

      bool isPrevButtonPressed = false;
      if (prevChild != null) {
        assert(prevChild.parentData is _ActionButtonParentData);
2329
        final _ActionButtonParentData previousButtonParentData = prevChild.parentData! as _ActionButtonParentData;
2330 2331 2332 2333 2334
        isPrevButtonPressed = previousButtonParentData.isPressed;
      }

      final bool isDividerPresent = child != firstChild;
      final bool isDividerPainted = isDividerPresent && !(isButtonPressed || isPrevButtonPressed);
2335
      final Rect dividerRect = Rect.fromLTWH(
2336 2337 2338 2339 2340 2341
        accumulatingOffset.dx,
        accumulatingOffset.dy,
        size.width,
        dividerThickness,
      );

2342
      final Rect buttonBackgroundRect = Rect.fromLTWH(
2343 2344 2345 2346 2347 2348 2349 2350 2351 2352 2353 2354 2355 2356 2357 2358 2359 2360 2361 2362 2363 2364
        accumulatingOffset.dx,
        accumulatingOffset.dy + (isDividerPresent ? dividerThickness : 0.0),
        size.width,
        child.size.height,
      );

      // If this button is pressed, then we don't want a white background to be
      // painted, so we erase this button from the background path.
      if (isButtonPressed) {
        backgroundFillPath.addRect(buttonBackgroundRect);
        pressedBackgroundFillPath.addRect(buttonBackgroundRect);
      }

      // If this divider is needed, then we erase the divider area from the
      // background path, and on top of that we paint a translucent gray to
      // darken the divider area.
      if (isDividerPainted) {
        backgroundFillPath.addRect(dividerRect);
        dividersPath.addRect(dividerRect);
      }

      accumulatingOffset += (isDividerPresent ? dividerOffset : Offset.zero)
2365
          + Offset(0.0, child.size.height);
2366 2367 2368 2369 2370 2371 2372 2373 2374 2375 2376

      prevChild = child;
      child = childAfter(child);
    }

    canvas.drawPath(backgroundFillPath, _buttonBackgroundPaint);
    canvas.drawPath(pressedBackgroundFillPath, _pressedButtonBackgroundPaint);
    canvas.drawPath(dividersPath, _dividerPaint);
  }

  void _drawButtons(PaintingContext context, Offset offset) {
2377
    RenderBox? child = firstChild;
2378
    while (child != null) {
2379
      final MultiChildLayoutParentData childParentData = child.parentData! as MultiChildLayoutParentData;
2380 2381 2382 2383 2384 2385
      context.paintChild(child, childParentData.offset + offset);
      child = childAfter(child);
    }
  }

  @override
2386
  bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
2387 2388
    return defaultHitTestChildren(result, position: position);
  }
2389
}