dialog.dart 82.4 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
  final MediaQueryData? data = MediaQuery.maybeOf(context);
153 154
  return data != null && data.textScaleFactor > _kMaxRegularTextScaleFactor;
}
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
  }) : assert(actions != null);
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 261
    final double textScaleFactor = MediaQuery.of(context).textScaleFactor;

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 Container(
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.of(context).textScaleFactor;
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 334 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
        child: ScrollConfiguration(
          // A CupertinoScrollbar is built-in below.
          behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
          child: LayoutBuilder(
            builder: (BuildContext context, BoxConstraints constraints) {
              return AnimatedPadding(
                padding: MediaQuery.of(context).viewInsets +
                    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 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574
        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));
    }

    return Container(
      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 614 615 616 617 618 619 620 621 622
          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(),
    ];

    final Orientation orientation = MediaQuery.of(context).orientation;
    final double actionSheetWidth;
    if (orientation == Orientation.portrait) {
      actionSheetWidth = MediaQuery.of(context).size.width - (_kActionSheetEdgeHorizontalPadding * 2);
    } else {
      actionSheetWidth = MediaQuery.of(context).size.height - (_kActionSheetEdgeHorizontalPadding * 2);
    }

    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 668
    required this.onPressed,
    this.isDefaultAction = false,
    this.isDestructiveAction = false,
    required this.child,
  }) : assert(child != null),
669
       assert(onPressed != null);
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 702

  /// 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);
    }

703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724
    return MouseRegion(
      cursor: onPressed != null && kIsWeb ? SystemMouseCursors.click : MouseCursor.defer,
      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,
              ),
725 726 727 728 729 730 731 732 733 734 735
            ),
          ),
        ),
      ),
    );
  }
}

class _CupertinoActionSheetCancelButton extends StatefulWidget {
  const _CupertinoActionSheetCancelButton({
    this.child,
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 771

  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),
772
          borderRadius: const BorderRadius.all(Radius.circular(_kCornerRadius)),
773 774 775 776 777 778 779
        ),
        child: widget.child,
      ),
    );
  }
}

780 781 782 783 784 785
// 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({
786 787
    required this.contentSection,
    required this.actionsSection,
788 789
    required this.dividerColor,
    this.isActionSheet = false,
790
  });
791

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

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

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

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

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

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

828 829
  Element? _contentElement;
  Element? _actionsElement;
830

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

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

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

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

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

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

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

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

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

  void _placeChildInSlot(RenderObject child, _AlertDialogSections slot) {
    assert(slot != null);
    switch (slot) {
      case _AlertDialogSections.contentSection:
        renderObject.contentSection = child as RenderBox;
        break;
      case _AlertDialogSections.actionsSection:
        renderObject.actionsSection = child as RenderBox;
        break;
    }
  }
909 910 911 912 913 914 915 916 917 918 919 920 921
}

// 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
922
// button section will not be rendered shorter than this minimum. See
923 924 925 926 927 928 929 930 931 932 933 934 935 936
// [_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({
937 938
    RenderBox? contentSection,
    RenderBox? actionsSection,
939 940
    double dividerThickness = 0.0,
    bool isInAccessibilityMode = false,
941
    bool isActionSheet = false,
942
    required Color dividerColor,
943 944 945
  }) : _contentSection = contentSection,
       _actionsSection = actionsSection,
       _dividerThickness = dividerThickness,
946
       _isInAccessibilityMode = isInAccessibilityMode,
947
       _isActionSheet = isActionSheet,
948
       _dividerPaint = Paint()
949 950
         ..color = dividerColor
         ..style = PaintingStyle.fill;
951

952 953 954
  RenderBox? get contentSection => _contentSection;
  RenderBox? _contentSection;
  set contentSection(RenderBox? newContentSection) {
955 956
    if (newContentSection != _contentSection) {
      if (_contentSection != null) {
957
        dropChild(_contentSection!);
958 959 960
      }
      _contentSection = newContentSection;
      if (_contentSection != null) {
961
        adoptChild(_contentSection!);
962 963 964 965
      }
    }
  }

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

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

989 990 991 992 993 994 995 996 997
  bool _isActionSheet;
  bool get isActionSheet => _isActionSheet;
  set isActionSheet(bool newValue) {
    if (newValue != _isActionSheet) {
      _isActionSheet = newValue;
      markNeedsLayout();
    }
  }

998 999 1000 1001 1002
  double get _dialogWidth => isInAccessibilityMode
      ? _kAccessibilityCupertinoDialogWidth
      : _kCupertinoDialogWidth;

  final double _dividerThickness;
1003 1004 1005 1006 1007 1008 1009
  final Paint _dividerPaint;

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

1011 1012 1013
    _dividerPaint.color = newValue;
    markNeedsPaint();
  }
1014 1015 1016 1017 1018

  @override
  void attach(PipelineOwner owner) {
    super.attach(owner);
    if (null != contentSection) {
1019
      contentSection!.attach(owner);
1020 1021
    }
    if (null != actionsSection) {
1022
      actionsSection!.attach(owner);
1023 1024 1025 1026 1027 1028 1029
    }
  }

  @override
  void detach() {
    super.detach();
    if (null != contentSection) {
1030
      contentSection!.detach();
1031 1032
    }
    if (null != actionsSection) {
1033
      actionsSection!.detach();
1034 1035 1036 1037 1038 1039
    }
  }

  @override
  void redepthChildren() {
    if (null != contentSection) {
1040
      redepthChild(contentSection!);
1041 1042
    }
    if (null != actionsSection) {
1043
      redepthChild(actionsSection!);
1044 1045 1046 1047 1048
    }
  }

  @override
  void setupParentData(RenderBox child) {
1049
    if (!isActionSheet && child.parentData is! BoxParentData) {
1050
      child.parentData = BoxParentData();
1051 1052
    } else if (child.parentData is! MultiChildLayoutParentData) {
      child.parentData = MultiChildLayoutParentData();
1053 1054 1055 1056 1057 1058
    }
  }

  @override
  void visitChildren(RenderObjectVisitor visitor) {
    if (contentSection != null) {
1059
      visitor(contentSection!);
1060 1061
    }
    if (actionsSection != null) {
1062
      visitor(actionsSection!);
1063 1064 1065 1066
    }
  }

  @override
1067
  List<DiagnosticsNode> debugDescribeChildren() => <DiagnosticsNode>[
1068 1069
    if (contentSection != null) contentSection!.toDiagnosticsNode(name: 'content'),
    if (actionsSection != null) actionsSection!.toDiagnosticsNode(name: 'actions'),
1070
  ];
1071 1072 1073

  @override
  double computeMinIntrinsicWidth(double height) {
1074
    return isActionSheet ? constraints.minWidth : _dialogWidth;
1075 1076 1077 1078
  }

  @override
  double computeMaxIntrinsicWidth(double height) {
1079
    return isActionSheet ? constraints.maxWidth : _dialogWidth;
1080 1081 1082 1083
  }

  @override
  double computeMinIntrinsicHeight(double width) {
1084 1085
    final double contentHeight = contentSection!.getMinIntrinsicHeight(width);
    final double actionsHeight = actionsSection!.getMinIntrinsicHeight(width);
1086
    final bool hasDivider = contentHeight > 0.0 && actionsHeight > 0.0;
1087
    double height = contentHeight + (hasDivider ? _dividerThickness : 0.0) + actionsHeight;
1088

1089 1090 1091 1092
    if (isActionSheet && (actionsHeight > 0 || contentHeight > 0)) {
      height -= 2 * _kActionSheetEdgeVerticalPadding;
    }
    if (height.isFinite) {
1093
      return height;
1094
    }
1095 1096 1097 1098 1099
    return 0.0;
  }

  @override
  double computeMaxIntrinsicHeight(double width) {
1100 1101
    final double contentHeight = contentSection!.getMaxIntrinsicHeight(width);
    final double actionsHeight = actionsSection!.getMaxIntrinsicHeight(width);
1102
    final bool hasDivider = contentHeight > 0.0 && actionsHeight > 0.0;
1103
    double height = contentHeight + (hasDivider ? _dividerThickness : 0.0) + actionsHeight;
1104

1105 1106 1107 1108
    if (isActionSheet && (actionsHeight > 0 || contentHeight > 0)) {
      height -= 2 * _kActionSheetEdgeVerticalPadding;
    }
    if (height.isFinite) {
1109
      return height;
1110
    }
1111 1112 1113
    return 0.0;
  }

1114 1115 1116 1117 1118 1119 1120 1121
  @override
  Size computeDryLayout(BoxConstraints constraints) {
    return _performLayout(
      constraints: constraints,
      layoutChild: ChildLayoutHelper.dryLayoutChild,
    ).size;
  }

1122 1123
  @override
  void performLayout() {
1124
    final _AlertDialogSizes dialogSizes = _performLayout(
1125 1126 1127 1128 1129 1130 1131
      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.
1132 1133 1134 1135
    assert(
      (!isActionSheet && actionsSection!.parentData is BoxParentData) ||
          (isActionSheet && actionsSection!.parentData is MultiChildLayoutParentData),
    );
1136 1137 1138 1139 1140 1141 1142
    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);
    }
1143 1144
  }

1145
  _AlertDialogSizes _performLayout({required BoxConstraints constraints, required ChildLayouter layoutChild}) {
1146 1147
    return isInAccessibilityMode
        ? performAccessibilityLayout(
1148 1149 1150 1151 1152 1153
            constraints: constraints,
            layoutChild: layoutChild,
          ) : performRegularLayout(
            constraints: constraints,
            layoutChild: layoutChild,
          );
1154 1155
  }

1156 1157 1158
  // 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.
1159 1160 1161
  _AlertDialogSizes performRegularLayout({required BoxConstraints constraints, required ChildLayouter layoutChild}) {
    final bool hasDivider = contentSection!.getMaxIntrinsicHeight(computeMaxIntrinsicWidth(0)) > 0.0
        && actionsSection!.getMaxIntrinsicHeight(computeMaxIntrinsicWidth(0)) > 0.0;
1162 1163
    final double dividerThickness = hasDivider ? _dividerThickness : 0.0;

1164
    final double minActionsHeight = actionsSection!.getMinIntrinsicHeight(computeMaxIntrinsicWidth(0));
1165

1166 1167
    final Size contentSize = layoutChild(
      contentSection!,
1168
      constraints.deflate(EdgeInsets.only(bottom: minActionsHeight + dividerThickness)),
1169 1170
    );

1171 1172
    final Size actionsSize = layoutChild(
      actionsSection!,
1173
      constraints.deflate(EdgeInsets.only(top: contentSize.height + dividerThickness)),
1174 1175 1176 1177
    );

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

1178 1179 1180 1181 1182 1183
    return _AlertDialogSizes(
      size: isActionSheet
          ? Size(constraints.maxWidth, dialogHeight)
          : constraints.constrain(Size(_dialogWidth, dialogHeight)),
      contentHeight: contentSize.height,
      dividerThickness: dividerThickness,
1184 1185 1186
    );
  }

1187 1188
  // 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.
1189
  _AlertDialogSizes performAccessibilityLayout({required BoxConstraints constraints, required ChildLayouter layoutChild}) {
1190 1191
    final bool hasDivider = contentSection!.getMaxIntrinsicHeight(_dialogWidth) > 0.0
        && actionsSection!.getMaxIntrinsicHeight(_dialogWidth) > 0.0;
1192 1193
    final double dividerThickness = hasDivider ? _dividerThickness : 0.0;

1194 1195
    final double maxContentHeight = contentSection!.getMaxIntrinsicHeight(_dialogWidth);
    final double maxActionsHeight = actionsSection!.getMaxIntrinsicHeight(_dialogWidth);
1196

1197 1198
    final Size contentSize;
    final Size actionsSize;
1199
    if (maxContentHeight + dividerThickness + maxActionsHeight > constraints.maxHeight) {
1200 1201 1202 1203
      // 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.
1204

1205 1206
      actionsSize = layoutChild(
        actionsSection!,
1207
        constraints.deflate(EdgeInsets.only(top: constraints.maxHeight / 2.0)),
1208 1209
      );

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

1217 1218
      contentSize = layoutChild(
        contentSection!,
1219 1220 1221
        constraints,
      );

1222 1223
      actionsSize = layoutChild(
        actionsSection!,
1224
        constraints.deflate(EdgeInsets.only(top: contentSize.height)),
1225 1226 1227 1228 1229 1230
      );
    }

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

1231
    return _AlertDialogSizes(
1232
      size: constraints.constrain(Size(_dialogWidth, dialogHeight)),
1233 1234
      contentHeight: contentSize.height,
      dividerThickness: dividerThickness,
1235 1236 1237 1238 1239
    );
  }

  @override
  void paint(PaintingContext context, Offset offset) {
1240 1241 1242 1243 1244 1245 1246
    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);
    }
1247

1248
    final bool hasDivider = contentSection!.size.height > 0.0 && actionsSection!.size.height > 0.0;
1249 1250 1251 1252
    if (hasDivider) {
      _paintDividerBetweenContentAndActions(context.canvas, offset);
    }

1253 1254 1255 1256 1257 1258 1259
    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);
    }
1260 1261 1262 1263 1264 1265
  }

  void _paintDividerBetweenContentAndActions(Canvas canvas, Offset offset) {
    canvas.drawRect(
      Rect.fromLTWH(
        offset.dx,
1266
        offset.dy + contentSection!.size.height,
1267 1268
        size.width,
        _dividerThickness,
1269
      ),
1270
      _dividerPaint,
1271 1272
    );
  }
1273 1274

  @override
1275
  bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296
    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);
            },
          );
    }

1297 1298
    final BoxParentData contentSectionParentData = contentSection!.parentData! as BoxParentData;
    final BoxParentData actionsSectionParentData = actionsSection!.parentData! as BoxParentData;
1299
    return result.addWithPaintOffset(
1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314
            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);
            },
          );
1315
  }
1316 1317
}

1318 1319 1320 1321 1322 1323
class _AlertDialogSizes {
  const _AlertDialogSizes({
    required this.size,
    required this.contentHeight,
    required this.dividerThickness,
  });
1324 1325

  final Size size;
1326 1327
  final double contentHeight;
  final double dividerThickness;
1328 1329
}

1330 1331 1332 1333 1334 1335 1336 1337
// 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.
1338
//
1339
// If title is missing, then only content is added. If content is
1340 1341
// missing, then only title is added. If both are missing, then it returns
// a SingleChildScrollView with a zero-sized Container.
1342 1343
class _CupertinoAlertContentSection extends StatelessWidget {
  const _CupertinoAlertContentSection({
1344
    this.title,
1345
    this.message,
1346
    this.scrollController,
1347 1348 1349 1350 1351 1352
    this.titlePadding,
    this.messagePadding,
    this.titleTextStyle,
    this.messageTextStyle,
    this.additionalPaddingBetweenTitleAndMessage,
  }) : assert(title == null || titlePadding != null && titleTextStyle != null),
1353
       assert(message == null || messagePadding != null && messageTextStyle != null);
1354

1355 1356 1357 1358
  // The (optional) title of the dialog is displayed in a large font at the top
  // of the dialog.
  //
  // Typically a Text widget.
1359
  final Widget? title;
1360

1361
  // The (optional) message of the dialog is displayed in the center of the
1362 1363 1364
  // dialog in a lighter font.
  //
  // Typically a Text widget.
1365
  final Widget? message;
1366 1367 1368 1369 1370 1371

  // 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.
1372
  final ScrollController? scrollController;
1373

1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387
  // 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;

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

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

1418 1419 1420 1421 1422
    // Add padding between the widgets if necessary.
    if (additionalPaddingBetweenTitleAndMessage != null && titleContentGroup.length > 1) {
      titleContentGroup.insert(1, Padding(padding: additionalPaddingBetweenTitleAndMessage!));
    }

1423
    return CupertinoScrollbar(
1424
      controller: scrollController,
1425
      child: SingleChildScrollView(
1426
        controller: scrollController,
1427
        child: Column(
1428 1429
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: titleContentGroup,
1430
        ),
1431
      ),
1432 1433 1434 1435
    );
  }
}

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

  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.
1455
  final ScrollController? scrollController;
1456

1457 1458 1459 1460 1461 1462 1463 1464
  // 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;

1465 1466
  @override
  Widget build(BuildContext context) {
1467
    final double devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
1468

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

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

// 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({
1500
    required this.child,
1501 1502 1503 1504 1505
  });

  final Widget child;

  @override
1506
  _PressableActionButtonState createState() => _PressableActionButtonState();
1507 1508 1509 1510 1511 1512 1513
}

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

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

// 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.
1545 1546
class _ActionButtonParentDataWidget
    extends ParentDataWidget<_ActionButtonParentData> {
1547
  const _ActionButtonParentDataWidget({
1548
    required this.isPressed,
1549 1550
    required super.child,
  });
1551 1552 1553 1554 1555 1556

  final bool isPressed;

  @override
  void applyParentData(RenderObject renderObject) {
    assert(renderObject.parentData is _ActionButtonParentData);
1557 1558
    final _ActionButtonParentData parentData =
        renderObject.parentData! as _ActionButtonParentData;
1559 1560 1561 1562
    if (parentData.isPressed != isPressed) {
      parentData.isPressed = isPressed;

      // Force a repaint.
1563
      final AbstractNode? targetParent = renderObject.parent;
1564
      if (targetParent is RenderObject) {
1565
        targetParent.markNeedsPaint();
1566
      }
1567 1568
    }
  }
1569 1570

  @override
1571 1572
  Type get debugTypicalAncestorWidgetClass =>
      _CupertinoDialogActionsRenderWidget;
1573 1574 1575 1576 1577
}

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

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

  /// The callback that is called when the button is tapped or otherwise
  /// activated.
  ///
  /// If this is set to null, the button will be disabled.
1604
  final VoidCallback? onPressed;
1605 1606 1607

  /// Set to true if button is the default choice in the dialog.
  ///
1608 1609 1610 1611
  /// 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].
1612 1613
  ///
  /// This parameters defaults to false and cannot be null.
1614 1615 1616 1617 1618
  final bool isDefaultAction;

  /// Whether this action destroys an object.
  ///
  /// For example, an action that deletes an email is destructive.
1619 1620
  ///
  /// Defaults to false and cannot be null.
1621 1622 1623 1624 1625 1626 1627 1628
  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].
1629
  final TextStyle? textStyle;
1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649

  /// 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({
1650 1651 1652
    required BuildContext context,
    required TextStyle textStyle,
    required Widget content,
1653 1654 1655 1656 1657 1658 1659 1660 1661 1662
  }) {
    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.
1663
    final double fontSizeRatio = (textScaleFactor * textStyle.fontSize!) / _kDialogMinButtonFontSize;
1664 1665
    final double padding = _calculatePadding(context);

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

  // 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({
1696 1697
    required TextStyle textStyle,
    required Widget content,
1698
  }) {
1699
    return DefaultTextStyle(
1700 1701 1702 1703 1704 1705 1706 1707
      style: textStyle,
      textAlign: TextAlign.center,
      child: content,
    );
  }

  @override
  Widget build(BuildContext context) {
1708 1709
    TextStyle style = _kCupertinoDialogActionStyle.copyWith(
      color: CupertinoDynamicColor.resolve(
1710
        isDestructiveAction ? CupertinoColors.systemRed : CupertinoTheme.of(context).primaryColor,
1711
        context,
1712 1713
      ),
    );
1714 1715
    style = style.merge(textStyle);

1716 1717 1718 1719
    if (isDefaultAction) {
      style = style.copyWith(fontWeight: FontWeight.w600);
    }

1720
    if (!enabled) {
1721
      style = style.copyWith(color: style.color!.withOpacity(0.5));
1722
    }
1723 1724 1725 1726 1727 1728 1729 1730

    // 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)
1731 1732 1733 1734 1735 1736 1737 1738 1739
        ? _buildContentWithAccessibilitySizingPolicy(
            textStyle: style,
            content: child,
          )
        : _buildContentWithRegularSizingPolicy(
            context: context,
            textStyle: style,
            content: child,
          );
1740

1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755
    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,
          ),
1756 1757 1758
        ),
      ),
    );
1759 1760 1761
  }
}

1762
// iOS style dialog action button layout.
1763
//
1764 1765 1766 1767 1768 1769 1770
// [_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 {
  _CupertinoDialogActionsRenderWidget({
1771
    required List<Widget> actionButtons,
1772
    double dividerThickness = 0.0,
1773 1774
    bool hasCancelButton = false,
    bool isActionSheet = false,
1775
  }) : _dividerThickness = dividerThickness,
1776 1777
       _hasCancelButton = hasCancelButton,
       _isActionSheet = isActionSheet,
1778
       super(children: actionButtons);
1779

1780
  final double _dividerThickness;
1781 1782
  final bool _hasCancelButton;
  final bool _isActionSheet;
1783 1784

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

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

1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836
// 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.
1837
//
1838 1839 1840 1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851 1852 1853 1854 1855
// 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({
1856
    List<RenderBox>? children,
1857
    double? dialogWidth,
1858
    double dividerThickness = 0.0,
1859 1860 1861
    required Color dialogColor,
    required Color dialogPressedColor,
    required Color dividerColor,
1862 1863 1864 1865
    bool hasCancelButton = false,
    bool isActionSheet = false,
  }) : assert(isActionSheet || dialogWidth != null),
       _dialogWidth = dialogWidth,
1866
       _buttonBackgroundPaint = Paint()
1867 1868 1869 1870 1871 1872 1873 1874 1875 1876 1877
         ..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 {
1878 1879 1880
    addAll(children);
  }

1881 1882 1883
  double? get dialogWidth => _dialogWidth;
  double? _dialogWidth;
  set dialogWidth(double? newWidth) {
1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899
    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();
    }
  }

1900 1901 1902
  bool _hasCancelButton;
  bool get hasCancelButton => _hasCancelButton;
  set hasCancelButton(bool newValue) {
1903
    if (newValue == _hasCancelButton) {
1904
      return;
1905
    }
1906 1907 1908 1909 1910

    _hasCancelButton = newValue;
    markNeedsLayout();
  }

1911
  Color get dialogColor => _buttonBackgroundPaint.color;
1912 1913
  final Paint _buttonBackgroundPaint;
  set dialogColor(Color value) {
1914
    if (value == _buttonBackgroundPaint.color) {
1915
      return;
1916
    }
1917 1918 1919 1920

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

1922
  Color get dialogPressedColor => _pressedButtonBackgroundPaint.color;
1923 1924
  final Paint _pressedButtonBackgroundPaint;
  set dialogPressedColor(Color value) {
1925
    if (value == _pressedButtonBackgroundPaint.color) {
1926
      return;
1927
    }
1928

1929 1930 1931 1932
    _pressedButtonBackgroundPaint.color = value;
    markNeedsPaint();
  }

1933
  Color get dividerColor => _dividerPaint.color;
1934 1935
  final Paint _dividerPaint;
  set dividerColor(Color value) {
1936
    if (value == _dividerPaint.color) {
1937
      return;
1938
    }
1939 1940 1941 1942

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

1944
  bool get isActionSheet => _isActionSheet;
1945
  bool _isActionSheet;
1946
  set isActionSheet(bool value) {
1947
    if (value == _isActionSheet) {
1948
      return;
1949
    }
1950 1951 1952 1953 1954

    _isActionSheet = value;
    markNeedsPaint();
  }

1955 1956
  Iterable<RenderBox> get _pressedButtons {
    final List<RenderBox> boxes = <RenderBox>[];
1957
    RenderBox? currentChild = firstChild;
1958 1959
    while (currentChild != null) {
      assert(currentChild.parentData is _ActionButtonParentData);
1960
      final _ActionButtonParentData parentData = currentChild.parentData! as _ActionButtonParentData;
1961
      if (parentData.isPressed) {
1962
        boxes.add(currentChild);
1963 1964 1965
      }
      currentChild = childAfter(currentChild);
    }
1966
    return boxes;
1967 1968 1969
  }

  bool get _isButtonPressed {
1970
    RenderBox? currentChild = firstChild;
1971 1972
    while (currentChild != null) {
      assert(currentChild.parentData is _ActionButtonParentData);
1973
      final _ActionButtonParentData parentData = currentChild.parentData! as _ActionButtonParentData;
1974 1975 1976 1977 1978 1979 1980
      if (parentData.isPressed) {
        return true;
      }
      currentChild = childAfter(currentChild);
    }
    return false;
  }
1981 1982

  @override
1983
  void setupParentData(RenderBox child) {
1984
    if (child.parentData is! _ActionButtonParentData) {
1985
      child.parentData = _ActionButtonParentData();
1986
    }
1987 1988 1989
  }

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

  @override
  double computeMaxIntrinsicWidth(double height) {
1996
    return isActionSheet ? constraints.maxWidth : dialogWidth!;
1997 1998 1999 2000 2001
  }

  @override
  double computeMinIntrinsicHeight(double width) {
    if (childCount == 0) {
2002 2003
      return 0.0;
    } else if (isActionSheet) {
2004
      if (childCount == 1) {
2005
        return firstChild!.computeMaxIntrinsicHeight(width) + dividerThickness;
2006 2007
      }
      if (hasCancelButton && childCount < 4) {
2008
        return _computeMinIntrinsicHeightWithCancel(width);
2009
      }
2010
      return _computeMinIntrinsicHeightStacked(width);
2011 2012
    } else if (childCount == 1) {
      // If only 1 button, display the button across the entire dialog.
2013 2014 2015 2016
      return _computeMinIntrinsicHeightSideBySide(width);
    } else if (childCount == 2 && _isSingleButtonRow(width)) {
      // The first 2 buttons fit side-by-side. Display them horizontally.
      return _computeMinIntrinsicHeightSideBySide(width);
2017
    }
2018 2019 2020 2021 2022 2023 2024 2025 2026 2027 2028 2029 2030 2031 2032 2033 2034 2035
    // 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);
2036 2037 2038 2039 2040 2041 2042
  }

  // 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);

2043
    final double minHeight;
2044
    if (childCount == 1) {
2045
      minHeight = firstChild!.getMinIntrinsicHeight(width);
2046 2047 2048
    } else {
      final double perButtonWidth = (width - dividerThickness) / 2.0;
      minHeight = math.max(
2049 2050
        firstChild!.getMinIntrinsicHeight(perButtonWidth),
        lastChild!.getMinIntrinsicHeight(perButtonWidth),
2051 2052 2053 2054 2055
      );
    }
    return minHeight;
  }

2056 2057 2058 2059 2060 2061
  // 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.
2062 2063 2064
  double _computeMinIntrinsicHeightStacked(double width) {
    assert(childCount >= 2);

2065
    return firstChild!.getMinIntrinsicHeight(width)
2066 2067
        + dividerThickness
        + (0.5 * childAfter(firstChild!)!.getMinIntrinsicHeight(width));
2068 2069 2070 2071 2072 2073
  }

  @override
  double computeMaxIntrinsicHeight(double width) {
    if (childCount == 0) {
      // No buttons. Zero height.
2074 2075
      return 0.0;
    } else if (isActionSheet) {
2076
      if (childCount == 1) {
2077
        return firstChild!.computeMaxIntrinsicHeight(width) + dividerThickness;
2078
      }
2079
      return _computeMaxIntrinsicHeightStacked(width);
2080 2081
    } else if (childCount == 1) {
      // One button. Our max intrinsic height is equal to the button's.
2082
      return firstChild!.getMaxIntrinsicHeight(width);
2083 2084 2085 2086 2087 2088
    } 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;
2089
        return math.max(
2090 2091
          firstChild!.getMaxIntrinsicHeight(perButtonWidth),
          lastChild!.getMaxIntrinsicHeight(perButtonWidth),
2092 2093 2094 2095
        );
      } else {
        // The 2 buttons do not fit side by side. Measure total height as a
        // vertical stack.
2096
        return _computeMaxIntrinsicHeightStacked(width);
2097 2098
      }
    }
2099 2100 2101
    // Three+ buttons. Stack the buttons vertically with dividers and measure
    // the overall height.
    return _computeMaxIntrinsicHeightStacked(width);
2102 2103 2104 2105 2106 2107 2108 2109 2110
  }

  // 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;
2111
    RenderBox? button = firstChild;
2112 2113 2114 2115 2116 2117 2118 2119
    while (button != null) {
      heightAccumulation += button.getMaxIntrinsicHeight(width);
      button = childAfter(button);
    }
    return heightAccumulation;
  }

  bool _isSingleButtonRow(double width) {
2120
    final bool isSingleButtonRow;
2121 2122 2123 2124 2125
    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.
2126
      final double sideBySideWidth = firstChild!.getMaxIntrinsicWidth(double.infinity)
2127
          + dividerThickness
2128
          + lastChild!.getMaxIntrinsicWidth(double.infinity);
2129 2130 2131 2132 2133 2134 2135
      isSingleButtonRow = sideBySideWidth <= width;
    } else {
      isSingleButtonRow = false;
    }
    return isSingleButtonRow;
  }

2136 2137
  @override
  Size computeDryLayout(BoxConstraints constraints) {
2138
    return _performLayout(constraints: constraints, dry: true);
2139 2140
  }

2141 2142
  @override
  void performLayout() {
2143
    size = _performLayout(constraints: constraints);
2144 2145
  }

2146
  Size _performLayout({required BoxConstraints constraints, bool dry = false}) {
2147 2148 2149 2150
    final ChildLayouter layoutChild = dry
        ? ChildLayoutHelper.dryLayoutChild
        : ChildLayoutHelper.layoutChild;

2151
    if (!isActionSheet && _isSingleButtonRow(dialogWidth!)) {
2152 2153 2154
      if (childCount == 1) {
        // We have 1 button. Our size is the width of the dialog and the height
        // of the single button.
2155 2156
        final Size childSize = layoutChild(
          firstChild!,
2157 2158 2159
          constraints,
        );

2160
        return constraints.constrain(
2161
          Size(dialogWidth!, childSize.height),
2162 2163 2164
        );
      } else {
        // Each button gets half the available width, minus a single divider.
2165
        final BoxConstraints perButtonConstraints = BoxConstraints(
2166 2167 2168 2169 2170
          minWidth: (constraints.minWidth - dividerThickness) / 2.0,
          maxWidth: (constraints.maxWidth - dividerThickness) / 2.0,
        );

        // Layout the 2 buttons.
2171 2172
        final Size firstChildSize = layoutChild(
          firstChild!,
2173 2174
          perButtonConstraints,
        );
2175 2176
        final Size lastChildSize = layoutChild(
          lastChild!,
2177 2178 2179
          perButtonConstraints,
        );

2180 2181 2182 2183 2184 2185
        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);
        }
2186 2187

        // Calculate our size based on the button sizes.
2188
        return constraints.constrain(
2189
          Size(
2190
            dialogWidth!,
2191
            math.max(
2192 2193
              firstChildSize.height,
              lastChildSize.height,
2194
            ),
2195
          ),
2196 2197 2198 2199 2200 2201 2202 2203 2204
        );
      }
    } 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,
      );

2205
      RenderBox? child = firstChild;
2206 2207 2208
      int index = 0;
      double verticalOffset = 0.0;
      while (child != null) {
2209 2210
        final Size childSize = layoutChild(
          child,
2211 2212 2213
          perButtonConstraints,
        );

2214 2215 2216 2217 2218 2219
        if (!dry) {
          assert(child.parentData is MultiChildLayoutParentData);
          final MultiChildLayoutParentData parentData = child.parentData! as MultiChildLayoutParentData;
          parentData.offset = Offset(0.0, verticalOffset);
        }
        verticalOffset += childSize.height;
2220 2221 2222 2223 2224 2225 2226 2227 2228 2229
        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.
2230
      return constraints.constrain(
2231
        Size(computeMaxIntrinsicWidth(0), verticalOffset),
2232 2233 2234 2235 2236 2237 2238 2239
      );
    }
  }

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

2240
    if (!isActionSheet && _isSingleButtonRow(size.width)) {
2241 2242 2243 2244 2245 2246 2247 2248 2249 2250
      _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
2251
    // the dialog has 2 buttons). The vertical divider is hidden if either the
2252 2253
    // left or right button is pressed.
    final Rect verticalDivider = childCount == 2 && !_isButtonPressed
2254 2255 2256 2257 2258 2259 2260 2261 2262 2263
        ? Rect.fromLTWH(
            offset.dx + firstChild!.size.width,
            offset.dy,
            dividerThickness,
            math.max(
              firstChild!.size.height,
              lastChild!.size.height,
            ),
          )
        : Rect.zero;
2264

2265
    final List<Rect> pressedButtonRects = _pressedButtons.map<Rect>((RenderBox pressedButton) {
2266
      final MultiChildLayoutParentData buttonParentData = pressedButton.parentData! as MultiChildLayoutParentData;
2267

2268
      return Rect.fromLTWH(
2269 2270 2271 2272 2273 2274 2275 2276
        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.
2277
    final Path backgroundFillPath = Path()
2278
      ..fillType = PathFillType.evenOdd
2279
      ..addRect(Rect.fromLTWH(0.0, 0.0, size.width, size.height))
2280 2281 2282 2283 2284 2285 2286 2287 2288 2289 2290 2291
      ..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.
2292
    final Path pressedBackgroundFillPath = Path();
2293 2294 2295 2296 2297 2298 2299 2300 2301 2302
    for (int i = 0; i < pressedButtonRects.length; i += 1) {
      pressedBackgroundFillPath.addRect(pressedButtonRects[i]);
    }

    canvas.drawPath(
      pressedBackgroundFillPath,
      _pressedButtonBackgroundPaint,
    );

    // Create the dividers path and paint it.
2303
    final Path dividersPath = Path()
2304 2305 2306 2307 2308 2309 2310 2311 2312
      ..addRect(verticalDivider);

    canvas.drawPath(
      dividersPath,
      _dividerPaint,
    );
  }

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

2315
    final Path backgroundFillPath = Path()
2316
      ..fillType = PathFillType.evenOdd
2317
      ..addRect(Rect.fromLTWH(0.0, 0.0, size.width, size.height));
2318

2319
    final Path pressedBackgroundFillPath = Path();
2320

2321
    final Path dividersPath = Path();
2322 2323 2324

    Offset accumulatingOffset = offset;

2325 2326
    RenderBox? child = firstChild;
    RenderBox? prevChild;
2327 2328
    while (child != null) {
      assert(child.parentData is _ActionButtonParentData);
2329
      final _ActionButtonParentData currentButtonParentData = child.parentData! as _ActionButtonParentData;
2330 2331 2332 2333 2334
      final bool isButtonPressed = currentButtonParentData.isPressed;

      bool isPrevButtonPressed = false;
      if (prevChild != null) {
        assert(prevChild.parentData is _ActionButtonParentData);
2335
        final _ActionButtonParentData previousButtonParentData = prevChild.parentData! as _ActionButtonParentData;
2336 2337 2338 2339 2340
        isPrevButtonPressed = previousButtonParentData.isPressed;
      }

      final bool isDividerPresent = child != firstChild;
      final bool isDividerPainted = isDividerPresent && !(isButtonPressed || isPrevButtonPressed);
2341
      final Rect dividerRect = Rect.fromLTWH(
2342 2343 2344 2345 2346 2347
        accumulatingOffset.dx,
        accumulatingOffset.dy,
        size.width,
        dividerThickness,
      );

2348
      final Rect buttonBackgroundRect = Rect.fromLTWH(
2349 2350 2351 2352 2353 2354 2355 2356 2357 2358 2359 2360 2361 2362 2363 2364 2365 2366 2367 2368 2369 2370
        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)
2371
          + Offset(0.0, child.size.height);
2372 2373 2374 2375 2376 2377 2378 2379 2380 2381 2382

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

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

  void _drawButtons(PaintingContext context, Offset offset) {
2383
    RenderBox? child = firstChild;
2384
    while (child != null) {
2385
      final MultiChildLayoutParentData childParentData = child.parentData! as MultiChildLayoutParentData;
2386 2387 2388 2389 2390 2391
      context.paintChild(child, childParentData.offset + offset);
      child = childAfter(child);
    }
  }

  @override
2392
  bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
2393 2394
    return defaultHitTestChildren(result, position: position);
  }
2395
}