chip.dart 93.2 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 6 7 8
import 'dart:math' as math;

import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
9

10
import 'chip_theme.dart';
11
import 'colors.dart';
12
import 'constants.dart';
13
import 'debug.dart';
14
import 'icons.dart';
15 16
import 'ink_well.dart';
import 'material.dart';
17
import 'material_localizations.dart';
18
import 'material_state.dart';
19
import 'material_state_mixin.dart';
20
import 'theme.dart';
21
import 'theme_data.dart';
Hixie's avatar
Hixie committed
22
import 'tooltip.dart';
23

24 25 26
// Some design constants
const double _kChipHeight = 32.0;
const double _kDeleteIconSize = 18.0;
27 28 29

const int _kCheckmarkAlpha = 0xde; // 87%
const int _kDisabledAlpha = 0x61; // 38%
30
const double _kCheckmarkStrokeWidth = 2.0;
31

32 33 34 35 36 37
const Duration _kSelectDuration = Duration(milliseconds: 195);
const Duration _kCheckmarkDuration = Duration(milliseconds: 150);
const Duration _kCheckmarkReverseDuration = Duration(milliseconds: 50);
const Duration _kDrawerDuration = Duration(milliseconds: 150);
const Duration _kReverseDrawerDuration = Duration(milliseconds: 100);
const Duration _kDisableDuration = Duration(milliseconds: 75);
38

39 40
const Color _kSelectScrimColor = Color(0x60191919);
const Icon _kDefaultDeleteIcon = Icon(Icons.cancel, size: _kDeleteIconSize);
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60

/// An interface defining the base attributes for a material design chip.
///
/// Chips are compact elements that represent an attribute, text, entity, or
/// action.
///
/// The defaults mentioned in the documentation for each attribute are what
/// the implementing classes typically use for defaults (but this class doesn't
/// provide or enforce them).
///
/// See also:
///
///  * [Chip], a chip that displays information and can be deleted.
///  * [InputChip], a chip that represents a complex piece of information, such
///    as an entity (person, place, or thing) or conversational text, in a
///    compact form.
///  * [ChoiceChip], allows a single selection from a set of options. Choice
///    chips contain related descriptive text or categories.
///  * [FilterChip], uses tags or descriptive words as a way to filter content.
///  * [ActionChip], represents an action related to primary content.
61
///  * <https://material.io/design/components/chips.html>
62
abstract class ChipAttributes {
63
  // This class is intended to be used as an interface, and should not be
64
  // extended directly; this constructor prevents instantiation and extension.
65
  ChipAttributes._();
66

67 68 69 70 71 72 73 74
  /// The primary content of the chip.
  ///
  /// Typically a [Text] widget.
  Widget get label;

  /// A widget to display prior to the chip's label.
  ///
  /// Typically a [CircleAvatar] widget.
75
  Widget? get avatar;
76 77 78

  /// The style to be applied to the chip's label.
  ///
79 80
  /// The default label style is [TextTheme.bodyText1] from the overall
  /// theme's [ThemeData.textTheme].
81
  //
82 83
  /// This only has an effect on widgets that respect the [DefaultTextStyle],
  /// such as [Text].
84
  ///
85
  /// If [TextStyle.color] is a [MaterialStateProperty<Color>], [MaterialStateProperty.resolve]
86 87 88 89 90 91 92
  /// is used for the following [MaterialState]s:
  ///
  ///  * [MaterialState.disabled].
  ///  * [MaterialState.selected].
  ///  * [MaterialState.hovered].
  ///  * [MaterialState.focused].
  ///  * [MaterialState.pressed].
93
  TextStyle? get labelStyle;
94

95
  /// The color and weight of the chip's outline.
96
  ///
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127
  /// Defaults to the border side in the ambient [ChipThemeData]. If the theme
  /// border side resolves to null, the default is the border side of [shape].
  ///
  /// This value is combined with [shape] to create a shape decorated with an
  /// outline. If it is a [MaterialStateBorderSide],
  /// [MaterialStateProperty.resolve] is used for the following
  /// [MaterialState]s:
  ///
  ///  * [MaterialState.disabled].
  ///  * [MaterialState.selected].
  ///  * [MaterialState.hovered].
  ///  * [MaterialState.focused].
  ///  * [MaterialState.pressed].
  BorderSide? get side;

  /// The [OutlinedBorder] to draw around the chip.
  ///
  /// Defaults to the shape in the ambient [ChipThemeData]. If the theme
  /// shape resolves to null, the default is [StadiumBorder].
  ///
  /// This shape is combined with [side] to create a shape decorated with an
  /// outline. If it is a [MaterialStateOutlinedBorder],
  /// [MaterialStateProperty.resolve] is used for the following
  /// [MaterialState]s:
  ///
  ///  * [MaterialState.disabled].
  ///  * [MaterialState.selected].
  ///  * [MaterialState.hovered].
  ///  * [MaterialState.focused].
  ///  * [MaterialState.pressed].
  OutlinedBorder? get shape;
128

129
  /// {@macro flutter.material.Material.clipBehavior}
130 131
  ///
  /// Defaults to [Clip.none], and must not be null.
132 133
  Clip get clipBehavior;

134
  /// {@macro flutter.widgets.Focus.focusNode}
135
  FocusNode? get focusNode;
136 137 138 139

  /// {@macro flutter.widgets.Focus.autofocus}
  bool get autofocus;

140 141 142
  /// Color to be used for the unselected, enabled chip's background.
  ///
  /// The default is light grey.
143
  Color? get backgroundColor;
144

145
  /// The padding between the contents of the chip and the outside [shape].
146 147
  ///
  /// Defaults to 4 logical pixels on all sides.
148
  EdgeInsetsGeometry? get padding;
149

150 151 152 153 154 155 156 157
  /// Defines how compact the chip's layout will be.
  ///
  /// Chips are unaffected by horizontal density changes.
  ///
  /// {@macro flutter.material.themedata.visualDensity}
  ///
  /// See also:
  ///
158 159
  ///  * [ThemeData.visualDensity], which specifies the [visualDensity] for all
  ///    widgets within a [Theme].
160
  VisualDensity? get visualDensity;
161

162 163 164 165
  /// The padding around the [label] widget.
  ///
  /// By default, this is 4 logical pixels at the beginning and the end of the
  /// label, and zero on top and bottom.
166
  EdgeInsetsGeometry? get labelPadding;
167 168 169 170 171 172 173

  /// Configures the minimum size of the tap target.
  ///
  /// Defaults to [ThemeData.materialTapTargetSize].
  ///
  /// See also:
  ///
174
  ///  * [MaterialTapTargetSize], for a description of how this affects tap targets.
175
  MaterialTapTargetSize? get materialTapTargetSize;
176 177 178 179 180 181

  /// Elevation to be applied on the chip relative to its parent.
  ///
  /// This controls the size of the shadow below the chip.
  ///
  /// Defaults to 0. The value is always non-negative.
182
  double? get elevation;
183 184 185 186

  /// Color of the chip's shadow when the elevation is greater than 0.
  ///
  /// The default is [Colors.black].
187
  Color? get shadowColor;
188 189 190 191 192 193 194 195 196 197 198 199 200 201
}

/// An interface for material design chips that can be deleted.
///
/// The defaults mentioned in the documentation for each attribute are what
/// the implementing classes typically use for defaults (but this class doesn't
/// provide or enforce them).
///
/// See also:
///
///  * [Chip], a chip that displays information and can be deleted.
///  * [InputChip], a chip that represents a complex piece of information, such
///    as an entity (person, place, or thing) or conversational text, in a
///    compact form.
202
///  * <https://material.io/design/components/chips.html>
203
abstract class DeletableChipAttributes {
204
  // This class is intended to be used as an interface, and should not be
205
  // extended directly; this constructor prevents instantiation and extension.
206
  DeletableChipAttributes._();
207

208 209 210
  /// The icon displayed when [onDeleted] is set.
  ///
  /// Defaults to an [Icon] widget set to use [Icons.cancel].
211
  Widget? get deleteIcon;
212 213 214 215 216 217 218

  /// Called when the user taps the [deleteIcon] to delete the chip.
  ///
  /// If null, the delete button will not appear on the chip.
  ///
  /// The chip will not automatically remove itself: this just tells the app
  /// that the user tapped the delete button. In order to delete the chip, you
219
  /// have to do something similar to the following sample:
220
  ///
221
  /// {@tool dartpad}
222 223
  /// This sample shows how to use [onDeleted] to remove an entry when the
  /// delete button is tapped.
224
  ///
225
  /// ** See code in examples/api/lib/material/chip/deletable_chip_attributes.on_deleted.0.dart **
226
  /// {@end-tool}
227
  VoidCallback? get onDeleted;
228

229 230 231 232 233 234 235 236 237
  /// Used to define the delete icon's color with an [IconTheme] that
  /// contains the icon.
  ///
  /// The default is `Color(0xde000000)`
  /// (slightly transparent black) for light themes, and `Color(0xdeffffff)`
  /// (slightly transparent white) for dark themes.
  ///
  /// The delete icon appears if [DeletableChipAttributes.onDeleted] is
  /// non-null.
238
  Color? get deleteIconColor;
239 240

  /// The message to be used for the chip's delete button tooltip.
241
  ///
242 243
  /// If provided with an empty string, the tooltip of the delete button will be
  /// disabled.
244
  ///
245 246
  /// If null, the default [MaterialLocalizations.deleteButtonTooltip] will be
  /// used.
247
  String? get deleteButtonTooltipMessage;
248 249 250 251 252 253 254 255 256 257

  /// Whether to use a tooltip on the chip's delete button showing the
  /// [deleteButtonTooltipMessage].
  ///
  /// Defaults to true.
  @Deprecated(
    'Migrate to deleteButtonTooltipMessage. '
    'This feature was deprecated after v2.10.0-0.3.pre.'
  )
  bool get useDeleteButtonTooltip;
258 259
}

260 261 262 263 264 265 266 267 268 269 270 271 272 273 274
/// An interface for material design chips that can have check marks.
///
/// The defaults mentioned in the documentation for each attribute are what
/// the implementing classes typically use for defaults (but this class doesn't
/// provide or enforce them).
///
/// See also:
///
///  * [InputChip], a chip that represents a complex piece of information, such
///    as an entity (person, place, or thing) or conversational text, in a
///    compact form.
///  * [FilterChip], uses tags or descriptive words as a way to filter content.
///  * <https://material.io/design/components/chips.html>
abstract class CheckmarkableChipAttributes {
  // This class is intended to be used as an interface, and should not be
275
  // extended directly; this constructor prevents instantiation and extension.
276
  CheckmarkableChipAttributes._();
277

278 279
  /// Whether or not to show a check mark when
  /// [SelectableChipAttributes.selected] is true.
280 281
  ///
  /// Defaults to true.
282
  bool? get showCheckmark;
283 284 285 286 287 288 289

  /// [Color] of the chip's check mark when a check mark is visible.
  ///
  /// This will override the color set by the platform's brightness setting.
  ///
  /// If null, it will defer to a color selected by the platform's brightness
  /// setting.
290
  Color? get checkmarkColor;
291 292
}

293 294 295 296 297 298 299 300 301 302 303 304 305 306
/// An interface for material design chips that can be selected.
///
/// The defaults mentioned in the documentation for each attribute are what
/// the implementing classes typically use for defaults (but this class doesn't
/// provide or enforce them).
///
/// See also:
///
///  * [InputChip], a chip that represents a complex piece of information, such
///    as an entity (person, place, or thing) or conversational text, in a
///    compact form.
///  * [ChoiceChip], allows a single selection from a set of options. Choice
///    chips contain related descriptive text or categories.
///  * [FilterChip], uses tags or descriptive words as a way to filter content.
307
///  * <https://material.io/design/components/chips.html>
308
abstract class SelectableChipAttributes {
309
  // This class is intended to be used as an interface, and should not be
310
  // extended directly; this constructor prevents instantiation and extension.
311
  SelectableChipAttributes._();
312

313 314 315 316 317 318 319 320
  /// Whether or not this chip is selected.
  ///
  /// If [onSelected] is not null, this value will be used to determine if the
  /// select check mark will be shown or not.
  ///
  /// Must not be null. Defaults to false.
  bool get selected;

321 322
  /// Called when the chip should change between selected and de-selected
  /// states.
323 324 325 326 327 328 329 330 331 332 333 334 335 336 337
  ///
  /// When the chip is tapped, then the [onSelected] callback, if set, will be
  /// applied to `!selected` (see [selected]).
  ///
  /// The chip passes the new value to the callback but does not actually
  /// change state until the parent widget rebuilds the chip with the new
  /// value.
  ///
  /// The callback provided to [onSelected] should update the state of the
  /// parent [StatefulWidget] using the [State.setState] method, so that the
  /// parent gets rebuilt.
  ///
  /// The [onSelected] and [TappableChipAttributes.onPressed] callbacks must not
  /// both be specified at the same time.
  ///
338
  /// {@tool snippet}
339 340
  ///
  /// A [StatefulWidget] that illustrates use of onSelected in an [InputChip].
341 342 343
  ///
  /// ```dart
  /// class Wood extends StatefulWidget {
344 345
  ///   const Wood({Key? key}) : super(key: key);
  ///
346
  ///   @override
347
  ///   State<StatefulWidget> createState() => WoodState();
348 349 350 351 352 353 354
  /// }
  ///
  /// class WoodState extends State<Wood> {
  ///   bool _useChisel = false;
  ///
  ///   @override
  ///   Widget build(BuildContext context) {
355
  ///     return InputChip(
356 357 358 359 360 361 362 363 364 365 366
  ///       label: const Text('Use Chisel'),
  ///       selected: _useChisel,
  ///       onSelected: (bool newValue) {
  ///         setState(() {
  ///           _useChisel = newValue;
  ///         });
  ///       },
  ///     );
  ///   }
  /// }
  /// ```
367
  /// {@end-tool}
368
  ValueChanged<bool>? get onSelected;
369

370 371 372
  /// Elevation to be applied on the chip relative to its parent during the
  /// press motion.
  ///
373 374
  /// This controls the size of the shadow below the chip.
  ///
375
  /// Defaults to 8. The value is always non-negative.
376
  double? get pressElevation;
377

378 379 380 381
  /// Color to be used for the chip's background, indicating that it is
  /// selected.
  ///
  /// The chip is selected when [selected] is true.
382
  Color? get selectedColor;
383

384 385 386 387
  /// Color of the chip's shadow when the elevation is greater than 0 and the
  /// chip is selected.
  ///
  /// The default is [Colors.black].
388
  Color? get selectedShadowColor;
389

390 391
  /// Tooltip string to be used for the body area (where the label and avatar
  /// are) of the chip.
392
  String? get tooltip;
393 394 395 396 397 398 399 400

  /// The shape of the translucent highlight painted over the avatar when the
  /// [selected] property is true.
  ///
  /// Only the outer path of the shape is used.
  ///
  /// Defaults to [CircleBorder].
  ShapeBorder get avatarBorder;
401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416
}

/// An interface for material design chips that can be enabled and disabled.
///
/// The defaults mentioned in the documentation for each attribute are what
/// the implementing classes typically use for defaults (but this class doesn't
/// provide or enforce them).
///
/// See also:
///
///  * [InputChip], a chip that represents a complex piece of information, such
///    as an entity (person, place, or thing) or conversational text, in a
///    compact form.
///  * [ChoiceChip], allows a single selection from a set of options. Choice
///    chips contain related descriptive text or categories.
///  * [FilterChip], uses tags or descriptive words as a way to filter content.
417
///  * <https://material.io/design/components/chips.html>
418
abstract class DisabledChipAttributes {
419
  // This class is intended to be used as an interface, and should not be
420
  // extended directly; this constructor prevents instantiation and extension.
421
  DisabledChipAttributes._();
422

423 424 425 426
  /// Whether or not this chip is enabled for input.
  ///
  /// If this is true, but all of the user action callbacks are null (i.e.
  /// [SelectableChipAttributes.onSelected], [TappableChipAttributes.onPressed],
427
  /// and [DeletableChipAttributes.onDeleted]), then the
428 429 430 431 432 433 434 435 436 437 438
  /// control will still be shown as disabled.
  ///
  /// This is typically used if you want the chip to be disabled, but also show
  /// a delete button.
  ///
  /// For classes which don't have this as a constructor argument, [isEnabled]
  /// returns true if their user action callback is set.
  ///
  /// Defaults to true. Cannot be null.
  bool get isEnabled;

439 440
  /// The color used for the chip's background to indicate that it is not
  /// enabled.
441 442 443
  ///
  /// The chip is disabled when [isEnabled] is false, or all three of
  /// [SelectableChipAttributes.onSelected], [TappableChipAttributes.onPressed],
444
  /// and [DeletableChipAttributes.onDeleted] are null.
445 446
  ///
  /// It defaults to [Colors.black38].
447
  Color? get disabledColor;
448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464
}

/// An interface for material design chips that can be tapped.
///
/// The defaults mentioned in the documentation for each attribute are what
/// the implementing classes typically use for defaults (but this class doesn't
/// provide or enforce them).
///
/// See also:
///
///  * [InputChip], a chip that represents a complex piece of information, such
///    as an entity (person, place, or thing) or conversational text, in a
///    compact form.
///  * [ChoiceChip], allows a single selection from a set of options. Choice
///    chips contain related descriptive text or categories.
///  * [FilterChip], uses tags or descriptive words as a way to filter content.
///  * [ActionChip], represents an action related to primary content.
465
///  * <https://material.io/design/components/chips.html>
466
abstract class TappableChipAttributes {
467
  // This class is intended to be used as an interface, and should not be
468
  // extended directly; this constructor prevents instantiation and extension.
469
  TappableChipAttributes._();
470

471 472 473 474 475 476
  /// Called when the user taps the chip.
  ///
  /// If [onPressed] is set, then this callback will be called when the user
  /// taps on the label or avatar parts of the chip. If [onPressed] is null,
  /// then the chip will be disabled.
  ///
477
  /// {@tool snippet}
478 479 480
  ///
  /// ```dart
  /// class Blacksmith extends StatelessWidget {
481 482
  ///   const Blacksmith({Key? key}) : super(key: key);
  ///
483 484 485 486 487 488
  ///   void startHammering() {
  ///     print('bang bang bang');
  ///   }
  ///
  ///   @override
  ///   Widget build(BuildContext context) {
489
  ///     return InputChip(
490 491 492 493 494 495
  ///       label: const Text('Apply Hammer'),
  ///       onPressed: startHammering,
  ///     );
  ///   }
  /// }
  /// ```
496
  /// {@end-tool}
497
  VoidCallback? get onPressed;
498

499 500 501
  /// Elevation to be applied on the chip relative to its parent during the
  /// press motion.
  ///
502 503
  /// This controls the size of the shadow below the chip.
  ///
504
  /// Defaults to 8. The value is always non-negative.
505
  double? get pressElevation;
506

507 508
  /// Tooltip string to be used for the body area (where the label and avatar
  /// are) of the chip.
509
  String? get tooltip;
510 511 512 513 514 515 516 517 518 519
}

/// A material design chip.
///
/// Chips are compact elements that represent an attribute, text, entity, or
/// action.
///
/// Supplying a non-null [onDeleted] callback will cause the chip to include a
/// button for deleting the chip.
///
520 521 522 523
/// Its ancestors must include [Material], [MediaQuery], [Directionality], and
/// [MaterialLocalizations]. Typically all of these widgets are provided by
/// [MaterialApp] and [Scaffold]. The [label] and [clipBehavior] arguments must
/// not be null.
524
///
525
/// {@tool snippet}
526 527
///
/// ```dart
528 529
/// Chip(
///   avatar: CircleAvatar(
530
///     backgroundColor: Colors.grey.shade800,
531
///     child: const Text('AB'),
532
///   ),
533
///   label: const Text('Aaron Burr'),
534 535
/// )
/// ```
536
/// {@end-tool}
537 538 539 540 541 542 543 544 545 546 547 548 549
///
/// See also:
///
///  * [InputChip], a chip that represents a complex piece of information, such
///    as an entity (person, place, or thing) or conversational text, in a
///    compact form.
///  * [ChoiceChip], allows a single selection from a set of options. Choice
///    chips contain related descriptive text or categories.
///  * [FilterChip], uses tags or descriptive words as a way to filter content.
///  * [ActionChip], represents an action related to primary content.
///  * [CircleAvatar], which shows images or initials of entities.
///  * [Wrap], A widget that displays its children in multiple horizontal or
///    vertical runs.
550
///  * <https://material.io/design/components/chips.html>
551 552 553
class Chip extends StatelessWidget implements ChipAttributes, DeletableChipAttributes {
  /// Creates a material design chip.
  ///
554
  /// The [label], [autofocus], and [clipBehavior] arguments must not be null.
555
  /// The [elevation] must be null or non-negative.
556
  const Chip({
557
    Key? key,
558
    this.avatar,
559
    required this.label,
560 561 562 563 564 565
    this.labelStyle,
    this.labelPadding,
    this.deleteIcon,
    this.onDeleted,
    this.deleteIconColor,
    this.deleteButtonTooltipMessage,
566
    this.side,
567
    this.shape,
568
    this.clipBehavior = Clip.none,
569 570
    this.focusNode,
    this.autofocus = false,
571 572
    this.backgroundColor,
    this.padding,
573
    this.visualDensity,
574
    this.materialTapTargetSize,
575
    this.elevation,
576
    this.shadowColor,
577 578 579 580 581
    @Deprecated(
      'Migrate to deleteButtonTooltipMessage. '
      'This feature was deprecated after v2.10.0-0.3.pre.'
    )
    this.useDeleteButtonTooltip = true,
582
  }) : assert(label != null),
583
       assert(autofocus != null),
584
       assert(clipBehavior != null),
585
       assert(elevation == null || elevation >= 0.0),
586
       super(key: key);
587 588

  @override
589
  final Widget? avatar;
590 591 592
  @override
  final Widget label;
  @override
593
  final TextStyle? labelStyle;
594
  @override
595
  final EdgeInsetsGeometry? labelPadding;
596
  @override
597 598 599
  final BorderSide? side;
  @override
  final OutlinedBorder? shape;
600
  @override
601 602
  final Clip clipBehavior;
  @override
603
  final FocusNode? focusNode;
604 605 606
  @override
  final bool autofocus;
  @override
607
  final Color? backgroundColor;
608
  @override
609
  final EdgeInsetsGeometry? padding;
610
  @override
611
  final VisualDensity? visualDensity;
612
  @override
613
  final Widget? deleteIcon;
614
  @override
615
  final VoidCallback? onDeleted;
616
  @override
617
  final Color? deleteIconColor;
618
  @override
619
  final String? deleteButtonTooltipMessage;
620
  @override
621
  final MaterialTapTargetSize? materialTapTargetSize;
622
  @override
623
  final double? elevation;
624
  @override
625
  final Color? shadowColor;
626 627 628 629 630 631
  @override
  @Deprecated(
    'Migrate to deleteButtonTooltipMessage. '
    'This feature was deprecated after v2.10.0-0.3.pre.'
  )
  final bool useDeleteButtonTooltip;
632 633 634 635

  @override
  Widget build(BuildContext context) {
    assert(debugCheckHasMaterial(context));
636
    return RawChip(
637 638 639 640 641 642 643
      avatar: avatar,
      label: label,
      labelStyle: labelStyle,
      labelPadding: labelPadding,
      deleteIcon: deleteIcon,
      onDeleted: onDeleted,
      deleteIconColor: deleteIconColor,
644
      useDeleteButtonTooltip: useDeleteButtonTooltip,
645 646
      deleteButtonTooltipMessage: deleteButtonTooltipMessage,
      tapEnabled: false,
647
      side: side,
648
      shape: shape,
649
      clipBehavior: clipBehavior,
650 651
      focusNode: focusNode,
      autofocus: autofocus,
652 653
      backgroundColor: backgroundColor,
      padding: padding,
654
      visualDensity: visualDensity,
655
      materialTapTargetSize: materialTapTargetSize,
656
      elevation: elevation,
657
      shadowColor: shadowColor,
658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679
    );
  }
}

/// A material design input chip.
///
/// Input chips represent a complex piece of information, such as an entity
/// (person, place, or thing) or conversational text, in a compact form.
///
/// Input chips can be made selectable by setting [onSelected], deletable by
/// setting [onDeleted], and pressable like a button with [onPressed]. They have
/// a [label], and they can have a leading icon (see [avatar]) and a trailing
/// icon ([deleteIcon]). Colors and padding can be customized.
///
/// Requires one of its ancestors to be a [Material] widget.
///
/// Input chips work together with other UI elements. They can appear:
///
///  * In a [Wrap] widget.
///  * In a horizontally scrollable list, like a [ListView] whose
///    scrollDirection is [Axis.horizontal].
///
680
/// {@tool snippet}
681 682
///
/// ```dart
683 684
/// InputChip(
///   avatar: CircleAvatar(
685
///     backgroundColor: Colors.grey.shade800,
686
///     child: const Text('AB'),
687
///   ),
688
///   label: const Text('Aaron Burr'),
689 690 691 692 693
///   onPressed: () {
///     print('I am the one thing in life.');
///   }
/// )
/// ```
694
/// {@end-tool}
695 696 697 698 699 700 701 702 703 704 705
///
/// See also:
///
///  * [Chip], a chip that displays information and can be deleted.
///  * [ChoiceChip], allows a single selection from a set of options. Choice
///    chips contain related descriptive text or categories.
///  * [FilterChip], uses tags or descriptive words as a way to filter content.
///  * [ActionChip], represents an action related to primary content.
///  * [CircleAvatar], which shows images or initials of people.
///  * [Wrap], A widget that displays its children in multiple horizontal or
///    vertical runs.
706
///  * <https://material.io/design/components/chips.html>
707 708 709 710 711
class InputChip extends StatelessWidget
    implements
        ChipAttributes,
        DeletableChipAttributes,
        SelectableChipAttributes,
712
        CheckmarkableChipAttributes,
713 714 715 716 717 718 719
        DisabledChipAttributes,
        TappableChipAttributes {
  /// Creates an [InputChip].
  ///
  /// The [onPressed] and [onSelected] callbacks must not both be specified at
  /// the same time.
  ///
720 721 722 723
  /// The [label], [isEnabled], [selected], [autofocus], and [clipBehavior]
  /// arguments must not be null. The [pressElevation] and [elevation] must be
  /// null or non-negative. Typically, [pressElevation] is greater than
  /// [elevation].
724
  const InputChip({
725
    Key? key,
726
    this.avatar,
727
    required this.label,
728 729
    this.labelStyle,
    this.labelPadding,
730 731
    this.selected = false,
    this.isEnabled = true,
732 733 734 735 736 737
    this.onSelected,
    this.deleteIcon,
    this.onDeleted,
    this.deleteIconColor,
    this.deleteButtonTooltipMessage,
    this.onPressed,
738
    this.pressElevation,
739 740 741
    this.disabledColor,
    this.selectedColor,
    this.tooltip,
742
    this.side,
743
    this.shape,
744
    this.clipBehavior = Clip.none,
745 746
    this.focusNode,
    this.autofocus = false,
747 748
    this.backgroundColor,
    this.padding,
749
    this.visualDensity,
750
    this.materialTapTargetSize,
751
    this.elevation,
752 753
    this.shadowColor,
    this.selectedShadowColor,
754 755
    this.showCheckmark,
    this.checkmarkColor,
756
    this.avatarBorder = const CircleBorder(),
757 758 759 760 761
    @Deprecated(
      'Migrate to deleteButtonTooltipMessage. '
      'This feature was deprecated after v2.10.0-0.3.pre.'
    )
    this.useDeleteButtonTooltip = true,
762 763 764 765
  }) : assert(selected != null),
       assert(isEnabled != null),
       assert(label != null),
       assert(clipBehavior != null),
766
       assert(autofocus != null),
767 768
       assert(pressElevation == null || pressElevation >= 0.0),
       assert(elevation == null || elevation >= 0.0),
769
       super(key: key);
770 771

  @override
772
  final Widget? avatar;
773 774 775
  @override
  final Widget label;
  @override
776
  final TextStyle? labelStyle;
777
  @override
778
  final EdgeInsetsGeometry? labelPadding;
779 780 781 782 783
  @override
  final bool selected;
  @override
  final bool isEnabled;
  @override
784
  final ValueChanged<bool>? onSelected;
785
  @override
786
  final Widget? deleteIcon;
787
  @override
788
  final VoidCallback? onDeleted;
789
  @override
790
  final Color? deleteIconColor;
791
  @override
792
  final String? deleteButtonTooltipMessage;
793
  @override
794
  final VoidCallback? onPressed;
795
  @override
796
  final double? pressElevation;
797
  @override
798
  final Color? disabledColor;
799
  @override
800
  final Color? selectedColor;
801
  @override
802
  final String? tooltip;
803
  @override
804 805 806
  final BorderSide? side;
  @override
  final OutlinedBorder? shape;
807
  @override
808 809
  final Clip clipBehavior;
  @override
810
  final FocusNode? focusNode;
811 812 813
  @override
  final bool autofocus;
  @override
814
  final Color? backgroundColor;
815
  @override
816
  final EdgeInsetsGeometry? padding;
817
  @override
818
  final VisualDensity? visualDensity;
819
  @override
820
  final MaterialTapTargetSize? materialTapTargetSize;
821
  @override
822
  final double? elevation;
823
  @override
824
  final Color? shadowColor;
825
  @override
826
  final Color? selectedShadowColor;
827
  @override
828
  final bool? showCheckmark;
829
  @override
830
  final Color? checkmarkColor;
831
  @override
832
  final ShapeBorder avatarBorder;
833 834 835 836 837 838
  @override
  @Deprecated(
    'Migrate to deleteButtonTooltipMessage. '
    'This feature was deprecated after v2.10.0-0.3.pre.'
  )
  final bool useDeleteButtonTooltip;
839 840 841 842

  @override
  Widget build(BuildContext context) {
    assert(debugCheckHasMaterial(context));
843
    return RawChip(
844 845 846 847 848 849 850
      avatar: avatar,
      label: label,
      labelStyle: labelStyle,
      labelPadding: labelPadding,
      deleteIcon: deleteIcon,
      onDeleted: onDeleted,
      deleteIconColor: deleteIconColor,
851
      useDeleteButtonTooltip: useDeleteButtonTooltip,
852 853 854
      deleteButtonTooltipMessage: deleteButtonTooltipMessage,
      onSelected: onSelected,
      onPressed: onPressed,
855
      pressElevation: pressElevation,
856 857 858 859
      selected: selected,
      disabledColor: disabledColor,
      selectedColor: selectedColor,
      tooltip: tooltip,
860
      side: side,
861
      shape: shape,
862
      clipBehavior: clipBehavior,
863 864
      focusNode: focusNode,
      autofocus: autofocus,
865 866
      backgroundColor: backgroundColor,
      padding: padding,
867
      visualDensity: visualDensity,
868
      materialTapTargetSize: materialTapTargetSize,
869
      elevation: elevation,
870 871
      shadowColor: shadowColor,
      selectedShadowColor: selectedShadowColor,
872 873
      showCheckmark: showCheckmark,
      checkmarkColor: checkmarkColor,
874
      isEnabled: isEnabled && (onSelected != null || onDeleted != null || onPressed != null),
875
      avatarBorder: avatarBorder,
876 877 878 879 880 881 882 883 884
    );
  }
}

/// A material design choice chip.
///
/// [ChoiceChip]s represent a single choice from a set. Choice chips contain
/// related descriptive text or categories.
///
885 886
/// Requires one of its ancestors to be a [Material] widget. The [selected] and
/// [label] arguments must not be null.
887
///
888
/// {@tool snippet}
889 890 891
///
/// ```dart
/// class MyThreeOptions extends StatefulWidget {
892 893
///   const MyThreeOptions({Key? key}) : super(key: key);
///
894
///   @override
895
///   State<MyThreeOptions> createState() => _MyThreeOptionsState();
896 897 898
/// }
///
/// class _MyThreeOptionsState extends State<MyThreeOptions> {
899
///   int? _value = 1;
900 901 902
///
///   @override
///   Widget build(BuildContext context) {
903 904
///     return Wrap(
///       children: List<Widget>.generate(
905 906
///         3,
///         (int index) {
907 908
///           return ChoiceChip(
///             label: Text('Item $index'),
909 910
///             selected: _value == index,
///             onSelected: (bool selected) {
Ben Hagen's avatar
Ben Hagen committed
911 912 913
///               setState(() {
///                 _value = selected ? index : null;
///               });
914 915 916 917 918 919 920 921
///             },
///           );
///         },
///       ).toList(),
///     );
///   }
/// }
/// ```
922
/// {@end-tool}
923 924 925 926 927 928 929 930 931 932 933 934
///
/// See also:
///
///  * [Chip], a chip that displays information and can be deleted.
///  * [InputChip], a chip that represents a complex piece of information, such
///    as an entity (person, place, or thing) or conversational text, in a
///    compact form.
///  * [FilterChip], uses tags or descriptive words as a way to filter content.
///  * [ActionChip], represents an action related to primary content.
///  * [CircleAvatar], which shows images or initials of people.
///  * [Wrap], A widget that displays its children in multiple horizontal or
///    vertical runs.
935
///  * <https://material.io/design/components/chips.html>
936 937 938 939 940
class ChoiceChip extends StatelessWidget
    implements
        ChipAttributes,
        SelectableChipAttributes,
        DisabledChipAttributes {
941 942
  /// Create a chip that acts like a radio button.
  ///
943 944 945
  /// The [label], [selected], [autofocus], and [clipBehavior] arguments must
  /// not be null. The [pressElevation] and [elevation] must be null or
  /// non-negative. Typically, [pressElevation] is greater than [elevation].
946
  const ChoiceChip({
947
    Key? key,
948
    this.avatar,
949
    required this.label,
950 951 952
    this.labelStyle,
    this.labelPadding,
    this.onSelected,
953
    this.pressElevation,
954
    required this.selected,
955 956 957
    this.selectedColor,
    this.disabledColor,
    this.tooltip,
958
    this.side,
959
    this.shape,
960
    this.clipBehavior = Clip.none,
961 962
    this.focusNode,
    this.autofocus = false,
963 964
    this.backgroundColor,
    this.padding,
965
    this.visualDensity,
966
    this.materialTapTargetSize,
967
    this.elevation,
968 969
    this.shadowColor,
    this.selectedShadowColor,
970
    this.avatarBorder = const CircleBorder(),
971 972 973
  }) : assert(selected != null),
       assert(label != null),
       assert(clipBehavior != null),
974
       assert(autofocus != null),
975 976
       assert(pressElevation == null || pressElevation >= 0.0),
       assert(elevation == null || elevation >= 0.0),
977
       super(key: key);
978 979

  @override
980
  final Widget? avatar;
981 982 983
  @override
  final Widget label;
  @override
984
  final TextStyle? labelStyle;
985
  @override
986
  final EdgeInsetsGeometry? labelPadding;
987
  @override
988
  final ValueChanged<bool>? onSelected;
989
  @override
990
  final double? pressElevation;
991
  @override
992 993
  final bool selected;
  @override
994
  final Color? disabledColor;
995
  @override
996
  final Color? selectedColor;
997
  @override
998
  final String? tooltip;
999
  @override
1000 1001 1002
  final BorderSide? side;
  @override
  final OutlinedBorder? shape;
1003
  @override
1004 1005
  final Clip clipBehavior;
  @override
1006
  final FocusNode? focusNode;
1007 1008 1009
  @override
  final bool autofocus;
  @override
1010
  final Color? backgroundColor;
1011
  @override
1012
  final EdgeInsetsGeometry? padding;
1013
  @override
1014
  final VisualDensity? visualDensity;
1015
  @override
1016
  final MaterialTapTargetSize? materialTapTargetSize;
1017
  @override
1018
  final double? elevation;
1019
  @override
1020
  final Color? shadowColor;
1021
  @override
1022
  final Color? selectedShadowColor;
1023
  @override
1024
  final ShapeBorder avatarBorder;
1025 1026 1027 1028 1029 1030 1031

  @override
  bool get isEnabled => onSelected != null;

  @override
  Widget build(BuildContext context) {
    assert(debugCheckHasMaterial(context));
1032
    final ChipThemeData chipTheme = ChipTheme.of(context);
1033
    return RawChip(
1034 1035
      avatar: avatar,
      label: label,
1036
      labelStyle: labelStyle ?? (selected ? chipTheme.secondaryLabelStyle : null),
1037 1038
      labelPadding: labelPadding,
      onSelected: onSelected,
1039
      pressElevation: pressElevation,
1040 1041 1042
      selected: selected,
      showCheckmark: false,
      tooltip: tooltip,
1043
      side: side,
1044
      shape: shape,
1045
      clipBehavior: clipBehavior,
1046 1047
      focusNode: focusNode,
      autofocus: autofocus,
1048
      disabledColor: disabledColor,
1049
      selectedColor: selectedColor ?? chipTheme.secondarySelectedColor,
1050 1051
      backgroundColor: backgroundColor,
      padding: padding,
1052
      visualDensity: visualDensity,
1053
      isEnabled: isEnabled,
1054
      materialTapTargetSize: materialTapTargetSize,
1055
      elevation: elevation,
1056 1057
      shadowColor: shadowColor,
      selectedShadowColor: selectedShadowColor,
1058
      avatarBorder: avatarBorder,
1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070
    );
  }
}

/// A material design filter chip.
///
/// Filter chips use tags or descriptive words as a way to filter content.
///
/// Filter chips are a good alternative to [Checkbox] or [Switch] widgets.
/// Unlike these alternatives, filter chips allow for clearly delineated and
/// exposed options in a compact area.
///
1071
/// Requires one of its ancestors to be a [Material] widget.
1072
///
1073
/// {@tool snippet}
1074 1075 1076 1077 1078 1079 1080 1081 1082
///
/// ```dart
/// class ActorFilterEntry {
///   const ActorFilterEntry(this.name, this.initials);
///   final String name;
///   final String initials;
/// }
///
/// class CastFilter extends StatefulWidget {
1083 1084
///   const CastFilter({Key? key}) : super(key: key);
///
1085
///   @override
1086
///   State createState() => CastFilterState();
1087 1088 1089 1090 1091 1092 1093 1094 1095
/// }
///
/// class CastFilterState extends State<CastFilter> {
///   final List<ActorFilterEntry> _cast = <ActorFilterEntry>[
///     const ActorFilterEntry('Aaron Burr', 'AB'),
///     const ActorFilterEntry('Alexander Hamilton', 'AH'),
///     const ActorFilterEntry('Eliza Hamilton', 'EH'),
///     const ActorFilterEntry('James Madison', 'JM'),
///   ];
1096
///   final List<String> _filters = <String>[];
1097
///
1098 1099 1100
///   Iterable<Widget> get actorWidgets {
///     return _cast.map((ActorFilterEntry actor) {
///       return Padding(
1101
///         padding: const EdgeInsets.all(4.0),
1102 1103 1104
///         child: FilterChip(
///           avatar: CircleAvatar(child: Text(actor.initials)),
///           label: Text(actor.name),
1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118
///           selected: _filters.contains(actor.name),
///           onSelected: (bool value) {
///             setState(() {
///               if (value) {
///                 _filters.add(actor.name);
///               } else {
///                 _filters.removeWhere((String name) {
///                   return name == actor.name;
///                 });
///               }
///             });
///           },
///         ),
///       );
1119
///     });
1120 1121 1122 1123 1124 1125 1126
///   }
///
///   @override
///   Widget build(BuildContext context) {
///     return Column(
///       mainAxisAlignment: MainAxisAlignment.center,
///       children: <Widget>[
1127
///         Wrap(
1128 1129
///           children: actorWidgets.toList(),
///         ),
1130
///         Text('Look for: ${_filters.join(', ')}'),
1131 1132 1133 1134 1135
///       ],
///     );
///   }
/// }
/// ```
1136
/// {@end-tool}
1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149
///
/// See also:
///
///  * [Chip], a chip that displays information and can be deleted.
///  * [InputChip], a chip that represents a complex piece of information, such
///    as an entity (person, place, or thing) or conversational text, in a
///    compact form.
///  * [ChoiceChip], allows a single selection from a set of options. Choice
///    chips contain related descriptive text or categories.
///  * [ActionChip], represents an action related to primary content.
///  * [CircleAvatar], which shows images or initials of people.
///  * [Wrap], A widget that displays its children in multiple horizontal or
///    vertical runs.
1150
///  * <https://material.io/design/components/chips.html>
1151 1152 1153 1154
class FilterChip extends StatelessWidget
    implements
        ChipAttributes,
        SelectableChipAttributes,
1155
        CheckmarkableChipAttributes,
1156
        DisabledChipAttributes {
1157 1158
  /// Create a chip that acts like a checkbox.
  ///
1159 1160 1161
  /// The [selected], [label], [autofocus], and [clipBehavior] arguments must
  /// not be null. The [pressElevation] and [elevation] must be null or
  /// non-negative. Typically, [pressElevation] is greater than [elevation].
1162
  const FilterChip({
1163
    Key? key,
1164
    this.avatar,
1165
    required this.label,
1166 1167
    this.labelStyle,
    this.labelPadding,
1168
    this.selected = false,
1169
    required this.onSelected,
1170
    this.pressElevation,
1171 1172 1173
    this.disabledColor,
    this.selectedColor,
    this.tooltip,
1174
    this.side,
1175
    this.shape,
1176
    this.clipBehavior = Clip.none,
1177 1178
    this.focusNode,
    this.autofocus = false,
1179 1180
    this.backgroundColor,
    this.padding,
1181
    this.visualDensity,
1182
    this.materialTapTargetSize,
1183
    this.elevation,
1184 1185
    this.shadowColor,
    this.selectedShadowColor,
1186 1187
    this.showCheckmark,
    this.checkmarkColor,
1188
    this.avatarBorder = const CircleBorder(),
1189 1190 1191
  }) : assert(selected != null),
       assert(label != null),
       assert(clipBehavior != null),
1192
       assert(autofocus != null),
1193 1194
       assert(pressElevation == null || pressElevation >= 0.0),
       assert(elevation == null || elevation >= 0.0),
1195
       super(key: key);
1196 1197

  @override
1198
  final Widget? avatar;
1199 1200 1201
  @override
  final Widget label;
  @override
1202
  final TextStyle? labelStyle;
1203
  @override
1204
  final EdgeInsetsGeometry? labelPadding;
1205 1206 1207
  @override
  final bool selected;
  @override
1208
  final ValueChanged<bool>? onSelected;
1209
  @override
1210
  final double? pressElevation;
1211
  @override
1212
  final Color? disabledColor;
1213
  @override
1214
  final Color? selectedColor;
1215
  @override
1216
  final String? tooltip;
1217
  @override
1218 1219 1220
  final BorderSide? side;
  @override
  final OutlinedBorder? shape;
1221
  @override
1222 1223
  final Clip clipBehavior;
  @override
1224
  final FocusNode? focusNode;
1225 1226 1227
  @override
  final bool autofocus;
  @override
1228
  final Color? backgroundColor;
1229
  @override
1230
  final EdgeInsetsGeometry? padding;
1231
  @override
1232
  final VisualDensity? visualDensity;
1233
  @override
1234
  final MaterialTapTargetSize? materialTapTargetSize;
1235
  @override
1236
  final double? elevation;
1237
  @override
1238
  final Color? shadowColor;
1239
  @override
1240
  final Color? selectedShadowColor;
1241
  @override
1242
  final bool? showCheckmark;
1243
  @override
1244
  final Color? checkmarkColor;
1245
  @override
1246
  final ShapeBorder avatarBorder;
1247 1248 1249 1250 1251 1252 1253

  @override
  bool get isEnabled => onSelected != null;

  @override
  Widget build(BuildContext context) {
    assert(debugCheckHasMaterial(context));
1254
    return RawChip(
1255 1256 1257 1258 1259
      avatar: avatar,
      label: label,
      labelStyle: labelStyle,
      labelPadding: labelPadding,
      onSelected: onSelected,
1260
      pressElevation: pressElevation,
1261 1262
      selected: selected,
      tooltip: tooltip,
1263
      side: side,
1264
      shape: shape,
1265
      clipBehavior: clipBehavior,
1266 1267
      focusNode: focusNode,
      autofocus: autofocus,
1268 1269 1270 1271
      backgroundColor: backgroundColor,
      disabledColor: disabledColor,
      selectedColor: selectedColor,
      padding: padding,
1272
      visualDensity: visualDensity,
1273
      isEnabled: isEnabled,
1274
      materialTapTargetSize: materialTapTargetSize,
1275
      elevation: elevation,
1276 1277
      shadowColor: shadowColor,
      selectedShadowColor: selectedShadowColor,
1278 1279
      showCheckmark: showCheckmark,
      checkmarkColor: checkmarkColor,
1280
      avatarBorder: avatarBorder,
1281 1282 1283 1284 1285 1286 1287 1288 1289 1290
    );
  }
}

/// A material design action chip.
///
/// Action chips are a set of options which trigger an action related to primary
/// content. Action chips should appear dynamically and contextually in a UI.
///
/// Action chips can be tapped to trigger an action or show progress and
1291 1292 1293
/// confirmation. They cannot be disabled; if the action is not applicable, the
/// chip should not be included in the interface. (This contrasts with buttons,
/// where unavailable choices are usually represented as disabled controls.)
1294 1295 1296 1297
///
/// Action chips are displayed after primary content, such as below a card or
/// persistently at the bottom of a screen.
///
1298 1299
/// The material button widgets, [ElevatedButton], [TextButton], and
/// [OutlinedButton], are an alternative to action chips, which should appear
1300
/// statically and consistently in a UI.
1301
///
1302
/// Requires one of its ancestors to be a [Material] widget.
1303
///
1304
/// {@tool snippet}
1305 1306
///
/// ```dart
1307 1308
/// ActionChip(
///   avatar: CircleAvatar(
1309
///     backgroundColor: Colors.grey.shade800,
1310
///     child: const Text('AB'),
1311
///   ),
1312
///   label: const Text('Aaron Burr'),
1313
///   onPressed: () {
1314
///     print('If you stand for nothing, Burr, what’ll you fall for?');
1315 1316 1317
///   }
/// )
/// ```
1318
/// {@end-tool}
1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330
///
/// See also:
///
///  * [Chip], a chip that displays information and can be deleted.
///  * [InputChip], a chip that represents a complex piece of information, such
///    as an entity (person, place, or thing) or conversational text, in a
///    compact form.
///  * [ChoiceChip], allows a single selection from a set of options. Choice
///    chips contain related descriptive text or categories.
///  * [CircleAvatar], which shows images or initials of people.
///  * [Wrap], A widget that displays its children in multiple horizontal or
///    vertical runs.
1331
///  * <https://material.io/design/components/chips.html>
1332
class ActionChip extends StatelessWidget implements ChipAttributes, TappableChipAttributes {
1333 1334
  /// Create a chip that acts like a button.
  ///
1335 1336 1337
  /// The [label], [onPressed], [autofocus], and [clipBehavior] arguments must
  /// not be null. The [pressElevation] and [elevation] must be null or
  /// non-negative. Typically, [pressElevation] is greater than [elevation].
1338
  const ActionChip({
1339
    Key? key,
1340
    this.avatar,
1341
    required this.label,
1342 1343
    this.labelStyle,
    this.labelPadding,
1344
    required this.onPressed,
1345
    this.pressElevation,
1346
    this.tooltip,
1347
    this.side,
1348
    this.shape,
1349
    this.clipBehavior = Clip.none,
1350 1351
    this.focusNode,
    this.autofocus = false,
1352 1353
    this.backgroundColor,
    this.padding,
1354
    this.visualDensity,
1355
    this.materialTapTargetSize,
1356
    this.elevation,
1357
    this.shadowColor,
1358
  }) : assert(label != null),
1359
       assert(clipBehavior != null),
1360
       assert(autofocus != null),
1361 1362 1363 1364 1365
       assert(
         onPressed != null,
         'Rather than disabling an ActionChip by setting onPressed to null, '
         'remove it from the interface entirely.',
       ),
1366 1367
       assert(pressElevation == null || pressElevation >= 0.0),
       assert(elevation == null || elevation >= 0.0),
1368
       super(key: key);
1369 1370

  @override
1371
  final Widget? avatar;
1372 1373 1374
  @override
  final Widget label;
  @override
1375
  final TextStyle? labelStyle;
1376
  @override
1377
  final EdgeInsetsGeometry? labelPadding;
1378 1379 1380
  @override
  final VoidCallback onPressed;
  @override
1381
  final double? pressElevation;
1382
  @override
1383
  final String? tooltip;
1384
  @override
1385 1386 1387
  final BorderSide? side;
  @override
  final OutlinedBorder? shape;
1388
  @override
1389 1390
  final Clip clipBehavior;
  @override
1391
  final FocusNode? focusNode;
1392 1393 1394
  @override
  final bool autofocus;
  @override
1395
  final Color? backgroundColor;
1396
  @override
1397
  final EdgeInsetsGeometry? padding;
1398
  @override
1399
  final VisualDensity? visualDensity;
1400
  @override
1401
  final MaterialTapTargetSize? materialTapTargetSize;
1402
  @override
1403
  final double? elevation;
1404
  @override
1405
  final Color? shadowColor;
1406

1407 1408 1409
  @override
  Widget build(BuildContext context) {
    assert(debugCheckHasMaterial(context));
1410
    return RawChip(
1411 1412 1413
      avatar: avatar,
      label: label,
      onPressed: onPressed,
1414
      pressElevation: pressElevation,
1415 1416 1417
      tooltip: tooltip,
      labelStyle: labelStyle,
      backgroundColor: backgroundColor,
1418
      side: side,
1419
      shape: shape,
1420
      clipBehavior: clipBehavior,
1421 1422
      focusNode: focusNode,
      autofocus: autofocus,
1423
      padding: padding,
1424
      visualDensity: visualDensity,
1425
      labelPadding: labelPadding,
1426 1427
      materialTapTargetSize: materialTapTargetSize,
      elevation: elevation,
1428
      shadowColor: shadowColor,
1429 1430 1431 1432 1433
    );
  }
}

/// A raw material design chip.
1434
///
1435 1436 1437
/// This serves as the basis for all of the chip widget types to aggregate.
/// It is typically not created directly, one of the other chip types
/// that are appropriate for the use case are used instead:
1438
///
1439 1440 1441 1442 1443 1444 1445
///  * [Chip] a simple chip that can only display information and be deleted.
///  * [InputChip] represents a complex piece of information, such as an entity
///    (person, place, or thing) or conversational text, in a compact form.
///  * [ChoiceChip] allows a single selection from a set of options.
///  * [FilterChip] a chip that uses tags or descriptive words as a way to
///    filter content.
///  * [ActionChip]s display a set of actions related to primary content.
1446
///
1447 1448
/// Raw chips are typically only used if you want to create your own custom chip
/// type.
1449
///
1450 1451 1452 1453
/// Raw chips can be selected by setting [onSelected], deleted by setting
/// [onDeleted], and pushed like a button with [onPressed]. They have a [label],
/// and they can have a leading icon (see [avatar]) and a trailing icon
/// ([deleteIcon]). Colors and padding can be customized.
Ian Hickson's avatar
Ian Hickson committed
1454
///
1455
/// Requires one of its ancestors to be a [Material] widget.
Ian Hickson's avatar
Ian Hickson committed
1456
///
1457
/// See also:
1458
///
Ian Hickson's avatar
Ian Hickson committed
1459
///  * [CircleAvatar], which shows images or initials of people.
1460 1461
///  * [Wrap], A widget that displays its children in multiple horizontal or
///    vertical runs.
1462
///  * <https://material.io/design/components/chips.html>
1463 1464 1465 1466 1467
class RawChip extends StatefulWidget
    implements
        ChipAttributes,
        DeletableChipAttributes,
        SelectableChipAttributes,
1468
        CheckmarkableChipAttributes,
1469 1470
        DisabledChipAttributes,
        TappableChipAttributes {
1471
  /// Creates a RawChip.
1472
  ///
1473 1474 1475
  /// The [onPressed] and [onSelected] callbacks must not both be specified at
  /// the same time.
  ///
1476 1477 1478 1479
  /// The [label], [isEnabled], [selected], [autofocus], and [clipBehavior]
  /// arguments must not be null. The [pressElevation] and [elevation] must be
  /// null or non-negative. Typically, [pressElevation] is greater than
  /// [elevation].
1480
  const RawChip({
1481
    Key? key,
1482
    this.avatar,
1483
    required this.label,
1484
    this.labelStyle,
1485
    this.padding,
1486
    this.visualDensity,
1487
    this.labelPadding,
1488
    Widget? deleteIcon,
1489
    this.onDeleted,
1490
    this.deleteIconColor,
1491 1492 1493
    this.deleteButtonTooltipMessage,
    this.onPressed,
    this.onSelected,
1494
    this.pressElevation,
1495
    this.tapEnabled = true,
1496
    this.selected = false,
1497
    this.isEnabled = true,
1498 1499
    this.disabledColor,
    this.selectedColor,
1500
    this.tooltip,
1501
    this.side,
1502
    this.shape,
1503
    this.clipBehavior = Clip.none,
1504 1505
    this.focusNode,
    this.autofocus = false,
1506
    this.backgroundColor,
1507
    this.materialTapTargetSize,
1508
    this.elevation,
1509 1510
    this.shadowColor,
    this.selectedShadowColor,
1511 1512
    this.showCheckmark = true,
    this.checkmarkColor,
1513
    this.avatarBorder = const CircleBorder(),
1514 1515 1516 1517 1518
    @Deprecated(
      'Migrate to deleteButtonTooltipMessage. '
      'This feature was deprecated after v2.10.0-0.3.pre.'
    )
    this.useDeleteButtonTooltip = true,
1519 1520
  }) : assert(label != null),
       assert(isEnabled != null),
1521
       assert(selected != null),
1522
       assert(clipBehavior != null),
1523
       assert(autofocus != null),
1524 1525
       assert(pressElevation == null || pressElevation >= 0.0),
       assert(elevation == null || elevation >= 0.0),
1526 1527
       deleteIcon = deleteIcon ?? _kDefaultDeleteIcon,
       super(key: key);
1528

1529
  @override
1530
  final Widget? avatar;
1531 1532 1533
  @override
  final Widget label;
  @override
1534
  final TextStyle? labelStyle;
1535
  @override
1536
  final EdgeInsetsGeometry? labelPadding;
1537 1538 1539
  @override
  final Widget deleteIcon;
  @override
1540
  final VoidCallback? onDeleted;
1541
  @override
1542
  final Color? deleteIconColor;
1543
  @override
1544
  final String? deleteButtonTooltipMessage;
1545
  @override
1546
  final ValueChanged<bool>? onSelected;
1547
  @override
1548
  final VoidCallback? onPressed;
1549
  @override
1550
  final double? pressElevation;
1551
  @override
1552 1553 1554 1555
  final bool selected;
  @override
  final bool isEnabled;
  @override
1556
  final Color? disabledColor;
1557
  @override
1558
  final Color? selectedColor;
1559
  @override
1560
  final String? tooltip;
1561
  @override
1562 1563 1564
  final BorderSide? side;
  @override
  final OutlinedBorder? shape;
1565
  @override
1566 1567
  final Clip clipBehavior;
  @override
1568
  final FocusNode? focusNode;
1569 1570 1571
  @override
  final bool autofocus;
  @override
1572
  final Color? backgroundColor;
1573
  @override
1574
  final EdgeInsetsGeometry? padding;
1575
  @override
1576
  final VisualDensity? visualDensity;
1577
  @override
1578
  final MaterialTapTargetSize? materialTapTargetSize;
1579
  @override
1580
  final double? elevation;
1581
  @override
1582
  final Color? shadowColor;
1583
  @override
1584
  final Color? selectedShadowColor;
1585
  @override
1586
  final bool? showCheckmark;
1587
  @override
1588
  final Color? checkmarkColor;
1589
  @override
1590
  final ShapeBorder avatarBorder;
1591 1592 1593 1594 1595 1596
  @override
  @Deprecated(
    'Migrate to deleteButtonTooltipMessage. '
    'This feature was deprecated after v2.10.0-0.3.pre.'
  )
  final bool useDeleteButtonTooltip;
1597

1598 1599
  /// If set, this indicates that the chip should be disabled if all of the
  /// tap callbacks ([onSelected], [onPressed]) are null.
1600
  ///
1601 1602 1603
  /// For example, the [Chip] class sets this to false because it can't be
  /// disabled, even if no callbacks are set on it, since it is used for
  /// displaying information only.
1604
  ///
1605 1606
  /// Defaults to true.
  final bool tapEnabled;
1607

1608
  @override
1609
  State<RawChip> createState() => _RawChipState();
1610
}
1611

1612
class _RawChipState extends State<RawChip> with MaterialStateMixin, TickerProviderStateMixin<RawChip> {
1613
  static const Duration pressedAnimationDuration = Duration(milliseconds: 75);
1614

1615 1616 1617 1618 1619 1620 1621 1622 1623
  late AnimationController selectController;
  late AnimationController avatarDrawerController;
  late AnimationController deleteDrawerController;
  late AnimationController enableController;
  late Animation<double> checkmarkAnimation;
  late Animation<double> avatarDrawerAnimation;
  late Animation<double> deleteDrawerAnimation;
  late Animation<double> enableAnimation;
  late Animation<double> selectionFade;
1624 1625 1626 1627 1628 1629 1630 1631 1632

  bool get hasDeleteButton => widget.onDeleted != null;
  bool get hasAvatar => widget.avatar != null;

  bool get canTap {
    return widget.isEnabled
        && widget.tapEnabled
        && (widget.onPressed != null || widget.onSelected != null);
  }
1633

1634
  bool _isTapping = false;
1635
  bool get isTapping => canTap && _isTapping;
1636

1637 1638 1639 1640
  @override
  void initState() {
    assert(widget.onSelected == null || widget.onPressed == null);
    super.initState();
1641 1642
    setMaterialState(MaterialState.disabled, !widget.isEnabled);
    setMaterialState(MaterialState.selected, widget.selected);
1643
    selectController = AnimationController(
1644 1645 1646 1647
      duration: _kSelectDuration,
      value: widget.selected == true ? 1.0 : 0.0,
      vsync: this,
    );
1648
    selectionFade = CurvedAnimation(
1649 1650 1651
      parent: selectController,
      curve: Curves.fastOutSlowIn,
    );
1652
    avatarDrawerController = AnimationController(
1653 1654 1655 1656
      duration: _kDrawerDuration,
      value: hasAvatar || widget.selected == true ? 1.0 : 0.0,
      vsync: this,
    );
1657
    deleteDrawerController = AnimationController(
1658 1659 1660 1661
      duration: _kDrawerDuration,
      value: hasDeleteButton ? 1.0 : 0.0,
      vsync: this,
    );
1662
    enableController = AnimationController(
1663 1664 1665 1666
      duration: _kDisableDuration,
      value: widget.isEnabled ? 1.0 : 0.0,
      vsync: this,
    );
1667

1668 1669 1670 1671 1672 1673 1674 1675
    // These will delay the start of some animations, and/or reduce their
    // length compared to the overall select animation, using Intervals.
    final double checkmarkPercentage = _kCheckmarkDuration.inMilliseconds /
        _kSelectDuration.inMilliseconds;
    final double checkmarkReversePercentage = _kCheckmarkReverseDuration.inMilliseconds /
        _kSelectDuration.inMilliseconds;
    final double avatarDrawerReversePercentage = _kReverseDrawerDuration.inMilliseconds /
        _kSelectDuration.inMilliseconds;
1676
    checkmarkAnimation = CurvedAnimation(
1677
      parent: selectController,
1678 1679
      curve: Interval(1.0 - checkmarkPercentage, 1.0, curve: Curves.fastOutSlowIn),
      reverseCurve: Interval(
1680 1681 1682 1683 1684
        1.0 - checkmarkReversePercentage,
        1.0,
        curve: Curves.fastOutSlowIn,
      ),
    );
1685
    deleteDrawerAnimation = CurvedAnimation(
1686 1687 1688
      parent: deleteDrawerController,
      curve: Curves.fastOutSlowIn,
    );
1689
    avatarDrawerAnimation = CurvedAnimation(
1690 1691
      parent: avatarDrawerController,
      curve: Curves.fastOutSlowIn,
1692
      reverseCurve: Interval(
1693 1694 1695 1696 1697
        1.0 - avatarDrawerReversePercentage,
        1.0,
        curve: Curves.fastOutSlowIn,
      ),
    );
1698
    enableAnimation = CurvedAnimation(
1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716
      parent: enableController,
      curve: Curves.fastOutSlowIn,
    );
  }

  @override
  void dispose() {
    selectController.dispose();
    avatarDrawerController.dispose();
    deleteDrawerController.dispose();
    enableController.dispose();
    super.dispose();
  }

  void _handleTapDown(TapDownDetails details) {
    if (!canTap) {
      return;
    }
1717
    setMaterialState(MaterialState.pressed, true);
1718 1719 1720 1721 1722 1723 1724 1725 1726
    setState(() {
      _isTapping = true;
    });
  }

  void _handleTapCancel() {
    if (!canTap) {
      return;
    }
1727
    setMaterialState(MaterialState.pressed, false);
1728 1729 1730 1731 1732 1733 1734 1735 1736
    setState(() {
      _isTapping = false;
    });
  }

  void _handleTap() {
    if (!canTap) {
      return;
    }
1737
    setMaterialState(MaterialState.pressed, false);
1738 1739 1740 1741 1742 1743 1744 1745
    setState(() {
      _isTapping = false;
    });
    // Only one of these can be set, so only one will be called.
    widget.onSelected?.call(!widget.selected);
    widget.onPressed?.call();
  }

1746
  OutlinedBorder _getShape(ThemeData theme, ChipThemeData chipTheme, ChipThemeData chipDefaults) {
1747
    final BorderSide? resolvedSide = MaterialStateProperty.resolveAs<BorderSide?>(widget.side, materialStates)
1748 1749
      ?? MaterialStateProperty.resolveAs<BorderSide?>(chipTheme.side, materialStates)
      ?? MaterialStateProperty.resolveAs<BorderSide?>(chipDefaults.side, materialStates);
1750
    final OutlinedBorder resolvedShape = MaterialStateProperty.resolveAs<OutlinedBorder?>(widget.shape, materialStates)
1751 1752
      ?? MaterialStateProperty.resolveAs<OutlinedBorder?>(chipTheme.shape, materialStates)
      ?? MaterialStateProperty.resolveAs<OutlinedBorder?>(chipDefaults.shape, materialStates)
1753 1754 1755 1756
      ?? const StadiumBorder();
    return resolvedShape.copyWith(side: resolvedSide);
  }

1757 1758
  /// Picks between three different colors, depending upon the state of two
  /// different animations.
1759
  Color? _getBackgroundColor(ThemeData theme, ChipThemeData chipTheme, ChipThemeData chipDefaults) {
1760
    final ColorTween backgroundTween = ColorTween(
1761 1762 1763 1764 1765 1766 1767
      begin: widget.disabledColor
        ?? chipTheme.disabledColor
        ?? theme.disabledColor,
      end: widget.backgroundColor
        ?? chipTheme.backgroundColor
        ?? theme.chipTheme.backgroundColor
        ?? chipDefaults.backgroundColor,
1768
    );
1769
    final ColorTween selectTween = ColorTween(
1770
      begin: backgroundTween.evaluate(enableController),
1771 1772 1773 1774
      end: widget.selectedColor
        ?? chipTheme.selectedColor
        ?? theme.chipTheme.selectedColor
        ?? chipDefaults.selectedColor,
1775 1776 1777 1778 1779 1780 1781 1782 1783
    );
    return selectTween.evaluate(selectionFade);
  }

  @override
  void didUpdateWidget(RawChip oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.isEnabled != widget.isEnabled) {
      setState(() {
1784
        setMaterialState(MaterialState.disabled, !widget.isEnabled);
1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802
        if (widget.isEnabled) {
          enableController.forward();
        } else {
          enableController.reverse();
        }
      });
    }
    if (oldWidget.avatar != widget.avatar || oldWidget.selected != widget.selected) {
      setState(() {
        if (hasAvatar || widget.selected == true) {
          avatarDrawerController.forward();
        } else {
          avatarDrawerController.reverse();
        }
      });
    }
    if (oldWidget.selected != widget.selected) {
      setState(() {
1803
        setMaterialState(MaterialState.selected, widget.selected);
1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821
        if (widget.selected == true) {
          selectController.forward();
        } else {
          selectController.reverse();
        }
      });
    }
    if (oldWidget.onDeleted != widget.onDeleted) {
      setState(() {
        if (hasDeleteButton) {
          deleteDrawerController.forward();
        } else {
          deleteDrawerController.reverse();
        }
      });
    }
  }

1822 1823
  Widget? _wrapWithTooltip({String? tooltip, bool enabled = true, Widget? child}) {
    if (child == null || !enabled || tooltip == null) {
1824 1825
      return child;
    }
1826
    return Tooltip(
1827 1828 1829 1830 1831
      message: tooltip,
      child: child,
    );
  }

1832
  Widget? _buildDeleteIcon(
1833 1834 1835
    BuildContext context,
    ThemeData theme,
    ChipThemeData chipTheme,
1836
    ChipThemeData chipDefaults,
1837
  ) {
1838 1839 1840
    if (!hasDeleteButton) {
      return null;
    }
1841 1842 1843 1844
    return Semantics(
      container: true,
      button: true,
      child: _wrapWithTooltip(
1845 1846 1847
        tooltip: widget.useDeleteButtonTooltip
          ? widget.deleteButtonTooltipMessage ?? MaterialLocalizations.of(context).deleteButtonTooltip
          : null,
1848 1849
        enabled: widget.onDeleted != null,
        child: InkWell(
1850 1851 1852 1853 1854
          // Radius should be slightly less than the full size of the chip.
          radius: (_kChipHeight + (widget.padding?.vertical ?? 0.0)) * .45,
          // Keeps the splash from being constrained to the icon alone.
          splashFactory: _UnconstrainedInkSplashFactory(Theme.of(context).splashFactory),
          onTap: widget.isEnabled ? widget.onDeleted : null,
1855 1856
          child: IconTheme(
            data: theme.iconTheme.copyWith(
1857 1858 1859 1860
              color: widget.deleteIconColor
                ?? chipTheme.deleteIconColor
                ?? theme.chipTheme.deleteIconColor
                ?? chipDefaults.deleteIconColor,
1861 1862
            ),
            child: widget.deleteIcon,
1863 1864 1865 1866 1867
          ),
        ),
      ),
    );
  }
1868

1869 1870
  static const double _defaultElevation = 0.0;
  static const double _defaultPressElevation = 8.0;
1871
  static const Color _defaultShadowColor = Colors.black;
1872

1873
  @override
1874
  Widget build(BuildContext context) {
1875
    assert(debugCheckHasMaterial(context));
1876 1877
    assert(debugCheckHasMediaQuery(context));
    assert(debugCheckHasDirectionality(context));
1878
    assert(debugCheckHasMaterialLocalizations(context));
1879

1880 1881 1882 1883
    /// The chip at text scale 1 starts with 8px on each side and as text scaling
    /// gets closer to 2 the label padding is linearly interpolated from 8px to 4px.
    /// Once the widget has a text scaling of 2 or higher than the label padding
    /// remains 4px.
1884
    final EdgeInsetsGeometry defaultLabelPadding = EdgeInsets.lerp(
1885 1886
      const EdgeInsets.symmetric(horizontal: 8.0),
      const EdgeInsets.symmetric(horizontal: 4.0),
1887
      (MediaQuery.of(context).textScaleFactor - 1.0).clamp(0.0, 1.0),
1888
    )!;
1889

1890
    final ThemeData theme = Theme.of(context);
1891
    final ChipThemeData chipTheme = ChipTheme.of(context);
1892 1893 1894 1895 1896 1897
    final Brightness brightness = chipTheme.brightness ?? theme.brightness;
    final ChipThemeData chipDefaults = ChipThemeData.fromDefaults(
      brightness: brightness,
      secondaryColor: brightness == Brightness.dark ? Colors.tealAccent[200]! : theme.primaryColor,
      labelStyle: theme.textTheme.bodyText1!,
    );
1898
    final TextDirection? textDirection = Directionality.maybeOf(context);
1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920 1921 1922 1923 1924 1925 1926 1927
    final OutlinedBorder resolvedShape = _getShape(theme, chipTheme, chipDefaults);

    final double elevation = widget.elevation
      ?? chipTheme.elevation
      ?? theme.chipTheme.elevation
      ?? _defaultElevation;
    final double pressElevation = widget.pressElevation
      ?? chipTheme.pressElevation
      ?? theme.chipTheme.pressElevation
      ?? _defaultPressElevation;
    final Color shadowColor = widget.shadowColor
      ?? chipTheme.shadowColor
      ?? theme.chipTheme.shadowColor
      ?? _defaultShadowColor;
    final Color selectedShadowColor = widget.selectedShadowColor
      ?? chipTheme.selectedShadowColor
      ?? theme.chipTheme.selectedShadowColor
      ?? _defaultShadowColor;
    final Color? checkmarkColor = widget.checkmarkColor
      ?? chipTheme.checkmarkColor
      ?? theme.chipTheme.checkmarkColor;
    final bool showCheckmark = widget.showCheckmark
      ?? chipTheme.showCheckmark
      ?? theme.chipTheme.showCheckmark
      ?? true;
    final EdgeInsetsGeometry padding = widget.padding
      ?? chipTheme.padding
      ?? theme.chipTheme.padding
      ?? chipDefaults.padding!;
1928 1929 1930
    final TextStyle labelStyle = chipTheme.labelStyle
      ?? theme.chipTheme.labelStyle
      ?? chipDefaults.labelStyle!;
1931 1932 1933
    final EdgeInsetsGeometry labelPadding = widget.labelPadding
      ?? chipTheme.labelPadding
      ?? theme.chipTheme.labelPadding
1934
      ?? defaultLabelPadding;
1935

1936
    final TextStyle effectiveLabelStyle = labelStyle.merge(widget.labelStyle);
1937
    final Color? resolvedLabelColor = MaterialStateProperty.resolveAs<Color?>(effectiveLabelStyle.color, materialStates);
1938
    final TextStyle resolvedLabelStyle = effectiveLabelStyle.copyWith(color: resolvedLabelColor);
1939

1940 1941 1942 1943
    Widget result = Material(
      elevation: isTapping ? pressElevation : elevation,
      shadowColor: widget.selected ? selectedShadowColor : shadowColor,
      animationDuration: pressedAnimationDuration,
1944
      shape: resolvedShape,
1945 1946
      clipBehavior: widget.clipBehavior,
      child: InkWell(
1947
        onFocusChange: updateMaterialState(MaterialState.focused),
1948 1949 1950 1951 1952 1953
        focusNode: widget.focusNode,
        autofocus: widget.autofocus,
        canRequestFocus: widget.isEnabled,
        onTap: canTap ? _handleTap : null,
        onTapDown: canTap ? _handleTapDown : null,
        onTapCancel: canTap ? _handleTapCancel : null,
1954
        onHover: canTap ? updateMaterialState(MaterialState.hovered) : null,
1955
        customBorder: resolvedShape,
1956 1957
        child: AnimatedBuilder(
          animation: Listenable.merge(<Listenable>[selectController, enableController]),
1958
          builder: (BuildContext context, Widget? child) {
1959
            return Container(
1960
              decoration: ShapeDecoration(
1961
                shape: resolvedShape,
1962
                color: _getBackgroundColor(theme, chipTheme, chipDefaults),
1963
              ),
1964 1965 1966 1967
              child: child,
            );
          },
          child: _wrapWithTooltip(
1968 1969 1970
            tooltip: widget.tooltip,
            enabled: widget.onPressed != null || widget.onSelected != null,
            child: _ChipRenderWidget(
1971 1972 1973 1974 1975 1976 1977 1978 1979 1980 1981 1982
              theme: _ChipRenderTheme(
                label: DefaultTextStyle(
                  overflow: TextOverflow.fade,
                  textAlign: TextAlign.start,
                  maxLines: 1,
                  softWrap: false,
                  style: resolvedLabelStyle,
                  child: widget.label,
                ),
                avatar: AnimatedSwitcher(
                  duration: _kDrawerDuration,
                  switchInCurve: Curves.fastOutSlowIn,
1983
                  child: widget.avatar,
1984
                ),
1985 1986 1987
                deleteIcon: AnimatedSwitcher(
                  duration: _kDrawerDuration,
                  switchInCurve: Curves.fastOutSlowIn,
1988
                  child: _buildDeleteIcon(context, theme, chipTheme, chipDefaults),
1989
                ),
1990 1991
                brightness: brightness,
                padding: padding.resolve(textDirection),
1992
                visualDensity: widget.visualDensity ?? theme.visualDensity,
1993
                labelPadding: labelPadding.resolve(textDirection),
1994 1995 1996 1997
                showAvatar: hasAvatar,
                showCheckmark: showCheckmark,
                checkmarkColor: checkmarkColor,
                canTapBody: canTap,
1998
              ),
1999 2000 2001 2002 2003 2004 2005
              value: widget.selected,
              checkmarkAnimation: checkmarkAnimation,
              enableAnimation: enableAnimation,
              avatarDrawerAnimation: avatarDrawerAnimation,
              deleteDrawerAnimation: deleteDrawerAnimation,
              isEnabled: widget.isEnabled,
              avatarBorder: widget.avatarBorder,
2006 2007
            ),
          ),
2008 2009 2010
        ),
      ),
    );
2011
    final BoxConstraints constraints;
2012
    final Offset densityAdjustment = (widget.visualDensity ?? theme.visualDensity).baseSizeAdjustment;
2013 2014
    switch (widget.materialTapTargetSize ?? theme.materialTapTargetSize) {
      case MaterialTapTargetSize.padded:
2015 2016 2017 2018
        constraints = BoxConstraints(
          minWidth: kMinInteractiveDimension + densityAdjustment.dx,
          minHeight: kMinInteractiveDimension + densityAdjustment.dy,
        );
2019 2020 2021 2022 2023 2024 2025
        break;
      case MaterialTapTargetSize.shrinkWrap:
        constraints = const BoxConstraints();
        break;
    }
    result = _ChipRedirectingHitDetectionWidget(
      constraints: constraints,
2026
      child: Center(
2027 2028
        widthFactor: 1.0,
        heightFactor: 1.0,
2029
        child: result,
2030 2031
      ),
    );
2032
    return Semantics(
2033
      button: widget.tapEnabled,
2034 2035
      container: true,
      selected: widget.selected,
2036
      enabled: widget.tapEnabled ? canTap : null,
2037 2038 2039 2040 2041 2042 2043 2044 2045 2046 2047 2048
      child: result,
    );
  }
}

/// Redirects the [position.dy] passed to [RenderBox.hitTest] to the vertical
/// center of the widget.
///
/// The primary purpose of this widget is to allow padding around the [RawChip]
/// to trigger the child ink feature without increasing the size of the material.
class _ChipRedirectingHitDetectionWidget extends SingleChildRenderObjectWidget {
  const _ChipRedirectingHitDetectionWidget({
2049 2050 2051
    Key? key,
    Widget? child,
    required this.constraints,
2052 2053 2054 2055 2056 2057
  }) : super(key: key, child: child);

  final BoxConstraints constraints;

  @override
  RenderObject createRenderObject(BuildContext context) {
2058
    return _RenderChipRedirectingHitDetection(constraints);
2059 2060 2061 2062 2063 2064 2065 2066 2067 2068 2069 2070
  }

  @override
  void updateRenderObject(BuildContext context, covariant _RenderChipRedirectingHitDetection renderObject) {
    renderObject.additionalConstraints = constraints;
  }
}

class _RenderChipRedirectingHitDetection extends RenderConstrainedBox {
  _RenderChipRedirectingHitDetection(BoxConstraints additionalConstraints) : super(additionalConstraints: additionalConstraints);

  @override
2071
  bool hitTest(BoxHitTestResult result, { required Offset position }) {
2072 2073 2074 2075 2076
    if (!size.contains(position))
      return false;
    // Only redirects hit detection which occurs above and below the render object.
    // In order to make this assumption true, I have removed the minimum width
    // constraints, since any reasonable chip would be at least that wide.
2077 2078 2079 2080
    final Offset offset = Offset(position.dx, size.height / 2);
    return result.addWithRawTransform(
      transform: MatrixUtils.forceToPoint(offset),
      position: position,
2081
      hitTest: (BoxHitTestResult result, Offset position) {
2082
        assert(position == offset);
2083
        return child!.hitTest(result, position: offset);
2084 2085
      },
    );
2086 2087 2088
  }
}

2089
class _ChipRenderWidget extends RenderObjectWidget with SlottedMultiChildRenderObjectWidgetMixin<_ChipSlot> {
2090
  const _ChipRenderWidget({
2091 2092
    Key? key,
    required this.theme,
2093 2094
    this.value,
    this.isEnabled,
2095 2096 2097 2098
    required this.checkmarkAnimation,
    required this.avatarDrawerAnimation,
    required this.deleteDrawerAnimation,
    required this.enableAnimation,
2099
    this.avatarBorder,
2100 2101
  }) : assert(theme != null),
       super(key: key);
2102 2103

  final _ChipRenderTheme theme;
2104 2105
  final bool? value;
  final bool? isEnabled;
2106 2107 2108 2109
  final Animation<double> checkmarkAnimation;
  final Animation<double> avatarDrawerAnimation;
  final Animation<double> deleteDrawerAnimation;
  final Animation<double> enableAnimation;
2110
  final ShapeBorder? avatarBorder;
2111 2112

  @override
2113 2114 2115 2116 2117 2118 2119 2120 2121 2122 2123 2124 2125
  Iterable<_ChipSlot> get slots => _ChipSlot.values;

  @override
  Widget? childForSlot(_ChipSlot slot) {
    switch (slot) {
      case _ChipSlot.label:
        return theme.label;
      case _ChipSlot.avatar:
        return theme.avatar;
      case _ChipSlot.deleteIcon:
        return theme.deleteIcon;
    }
  }
2126 2127 2128 2129 2130 2131

  @override
  void updateRenderObject(BuildContext context, _RenderChip renderObject) {
    renderObject
      ..theme = theme
      ..textDirection = Directionality.of(context)
2132 2133 2134 2135 2136
      ..value = value
      ..isEnabled = isEnabled
      ..checkmarkAnimation = checkmarkAnimation
      ..avatarDrawerAnimation = avatarDrawerAnimation
      ..deleteDrawerAnimation = deleteDrawerAnimation
2137 2138
      ..enableAnimation = enableAnimation
      ..avatarBorder = avatarBorder;
2139 2140 2141
  }

  @override
2142
  SlottedContainerRenderObjectMixin<_ChipSlot> createRenderObject(BuildContext context) {
2143
    return _RenderChip(
2144
      theme: theme,
2145
      textDirection: Directionality.of(context),
2146 2147 2148 2149 2150 2151
      value: value,
      isEnabled: isEnabled,
      checkmarkAnimation: checkmarkAnimation,
      avatarDrawerAnimation: avatarDrawerAnimation,
      deleteDrawerAnimation: deleteDrawerAnimation,
      enableAnimation: enableAnimation,
2152
      avatarBorder: avatarBorder,
2153 2154 2155 2156 2157 2158 2159 2160 2161 2162
    );
  }
}

enum _ChipSlot {
  label,
  avatar,
  deleteIcon,
}

2163
@immutable
2164 2165
class _ChipRenderTheme {
  const _ChipRenderTheme({
2166 2167 2168 2169 2170 2171 2172 2173 2174 2175 2176
    required this.avatar,
    required this.label,
    required this.deleteIcon,
    required this.brightness,
    required this.padding,
    required this.visualDensity,
    required this.labelPadding,
    required this.showAvatar,
    required this.showCheckmark,
    required this.checkmarkColor,
    required this.canTapBody,
2177 2178 2179 2180 2181
  });

  final Widget avatar;
  final Widget label;
  final Widget deleteIcon;
2182
  final Brightness brightness;
2183
  final EdgeInsets padding;
2184
  final VisualDensity visualDensity;
2185
  final EdgeInsets labelPadding;
2186 2187
  final bool showAvatar;
  final bool showCheckmark;
2188
  final Color? checkmarkColor;
2189
  final bool canTapBody;
2190 2191

  @override
2192
  bool operator ==(Object other) {
2193 2194 2195 2196 2197 2198
    if (identical(this, other)) {
      return true;
    }
    if (other.runtimeType != runtimeType) {
      return false;
    }
2199 2200 2201 2202 2203 2204 2205 2206 2207 2208 2209
    return other is _ChipRenderTheme
        && other.avatar == avatar
        && other.label == label
        && other.deleteIcon == deleteIcon
        && other.brightness == brightness
        && other.padding == padding
        && other.labelPadding == labelPadding
        && other.showAvatar == showAvatar
        && other.showCheckmark == showCheckmark
        && other.checkmarkColor == checkmarkColor
        && other.canTapBody == canTapBody;
2210 2211 2212 2213 2214 2215 2216 2217
  }

  @override
  int get hashCode {
    return hashValues(
      avatar,
      label,
      deleteIcon,
2218
      brightness,
2219 2220
      padding,
      labelPadding,
2221 2222
      showAvatar,
      showCheckmark,
2223
      checkmarkColor,
2224
      canTapBody,
2225 2226 2227 2228
    );
  }
}

2229
class _RenderChip extends RenderBox with SlottedContainerRenderObjectMixin<_ChipSlot> {
2230
  _RenderChip({
2231 2232
    required _ChipRenderTheme theme,
    required TextDirection textDirection,
2233 2234
    this.value,
    this.isEnabled,
2235 2236 2237 2238
    required this.checkmarkAnimation,
    required this.avatarDrawerAnimation,
    required this.deleteDrawerAnimation,
    required this.enableAnimation,
2239
    this.avatarBorder,
2240 2241 2242 2243
  }) : assert(theme != null),
       assert(textDirection != null),
       _theme = theme,
       _textDirection = textDirection {
2244 2245 2246 2247
    checkmarkAnimation.addListener(markNeedsPaint);
    avatarDrawerAnimation.addListener(markNeedsLayout);
    deleteDrawerAnimation.addListener(markNeedsLayout);
    enableAnimation.addListener(markNeedsPaint);
2248 2249
  }

2250 2251 2252 2253
  bool? value;
  bool? isEnabled;
  late Rect _deleteButtonRect;
  late Rect _pressRect;
2254 2255 2256 2257
  Animation<double> checkmarkAnimation;
  Animation<double> avatarDrawerAnimation;
  Animation<double> deleteDrawerAnimation;
  Animation<double> enableAnimation;
2258
  ShapeBorder? avatarBorder;
2259

2260 2261 2262
  RenderBox? get avatar => childForSlot(_ChipSlot.avatar);
  RenderBox? get deleteIcon => childForSlot(_ChipSlot.deleteIcon);
  RenderBox? get label => childForSlot(_ChipSlot.label);
2263 2264 2265 2266 2267 2268 2269 2270 2271 2272 2273

  _ChipRenderTheme get theme => _theme;
  _ChipRenderTheme _theme;
  set theme(_ChipRenderTheme value) {
    if (_theme == value) {
      return;
    }
    _theme = value;
    markNeedsLayout();
  }

2274 2275 2276
  TextDirection? get textDirection => _textDirection;
  TextDirection? _textDirection;
  set textDirection(TextDirection? value) {
2277 2278 2279 2280 2281 2282 2283 2284
    if (_textDirection == value) {
      return;
    }
    _textDirection = value;
    markNeedsLayout();
  }

  // The returned list is ordered for hit testing.
2285
  @override
2286 2287 2288 2289 2290 2291 2292 2293 2294
  Iterable<RenderBox> get children {
    return <RenderBox>[
      if (avatar != null)
        avatar!,
      if (label != null)
        label!,
      if (deleteIcon != null)
        deleteIcon!,
    ];
2295 2296
  }

2297
  bool get isDrawingCheckmark => theme.showCheckmark && !checkmarkAnimation.isDismissed;
2298
  bool get deleteIconShowing => !deleteDrawerAnimation.isDismissed;
2299 2300 2301 2302

  @override
  bool get sizedByParent => false;

2303
  static double _minWidth(RenderBox? box, double height) {
2304 2305 2306
    return box == null ? 0.0 : box.getMinIntrinsicWidth(height);
  }

2307
  static double _maxWidth(RenderBox? box, double height) {
2308 2309 2310
    return box == null ? 0.0 : box.getMaxIntrinsicWidth(height);
  }

2311
  static double _minHeight(RenderBox? box, double width) {
2312
    return box == null ? 0.0 : box.getMinIntrinsicHeight(width);
2313 2314
  }

2315
  static Size _boxSize(RenderBox? box) => box == null ? Size.zero : box.size;
2316

2317
  static Rect _boxRect(RenderBox? box) => box == null ? Rect.zero : _boxParentData(box).offset & box.size;
2318

2319
  static BoxParentData _boxParentData(RenderBox box) => box.parentData! as BoxParentData;
2320 2321 2322 2323 2324 2325

  @override
  double computeMinIntrinsicWidth(double height) {
    // The overall padding isn't affected by missing avatar or delete icon
    // because we add the padding regardless to give extra padding for the label
    // when they're missing.
2326
    final double overallPadding = theme.padding.horizontal +
2327
        theme.labelPadding.horizontal;
2328 2329 2330 2331
    return overallPadding +
        _minWidth(avatar, height) +
        _minWidth(label, height) +
        _minWidth(deleteIcon, height);
2332 2333 2334 2335
  }

  @override
  double computeMaxIntrinsicWidth(double height) {
2336
    final double overallPadding = theme.padding.horizontal +
2337
        theme.labelPadding.horizontal;
2338 2339 2340 2341
    return overallPadding +
        _maxWidth(avatar, height) +
        _maxWidth(label, height) +
        _maxWidth(deleteIcon, height);
2342 2343 2344 2345
  }

  @override
  double computeMinIntrinsicHeight(double width) {
2346 2347 2348 2349
    return math.max(
      _kChipHeight,
      theme.padding.vertical + theme.labelPadding.vertical + _minHeight(label, width),
    );
2350 2351 2352 2353 2354 2355
  }

  @override
  double computeMaxIntrinsicHeight(double width) => computeMinIntrinsicHeight(width);

  @override
2356
  double? computeDistanceToActualBaseline(TextBaseline baseline) {
2357
    // The baseline of this widget is the baseline of the label.
2358
    return label!.getDistanceToActualBaseline(baseline);
2359 2360
  }

2361
  Size _layoutLabel(BoxConstraints contentConstraints, double iconSizes, Size size, Size rawSize, [ChildLayouter layoutChild = ChildLayoutHelper.layoutChild]) {
2362 2363
    // Now that we know the label height and the width of the icons, we can
    // determine how much to shrink the width constraints for the "real" layout.
2364
    if (contentConstraints.maxWidth.isFinite) {
2365 2366
      final double maxWidth = math.max(
        0.0,
2367
        contentConstraints.maxWidth
2368 2369 2370 2371
        - iconSizes
        - theme.labelPadding.horizontal
        - theme.padding.horizontal,
      );
2372 2373 2374
      final Size updatedSize = layoutChild(
        label!,
        BoxConstraints(
2375
          maxWidth: maxWidth,
2376 2377 2378
          minHeight: rawSize.height,
          maxHeight: size.height,
        ),
2379
      );
2380 2381 2382 2383

      return Size(
        updatedSize.width + theme.labelPadding.horizontal,
        updatedSize.height + theme.labelPadding.vertical,
2384
      );
2385
    }
2386

2387 2388
    final Size updatedSize = layoutChild(
      label!,
2389 2390 2391 2392 2393 2394 2395
      BoxConstraints(
        minHeight: rawSize.height,
        maxHeight: size.height,
        maxWidth: size.width,
      ),
    );

2396
    return Size(
2397 2398
      updatedSize.width + theme.labelPadding.horizontal,
      updatedSize.height + theme.labelPadding.vertical,
2399
    );
2400 2401
  }

2402
  Size _layoutAvatar(BoxConstraints contentConstraints, double contentSize, [ChildLayouter layoutChild = ChildLayoutHelper.layoutChild]) {
2403
    final double requestedSize = math.max(0.0, contentSize);
2404
    final BoxConstraints avatarConstraints = BoxConstraints.tightFor(
2405 2406 2407
      width: requestedSize,
      height: requestedSize,
    );
2408
    final Size avatarBoxSize = layoutChild(avatar!, avatarConstraints);
2409
    if (!theme.showCheckmark && !theme.showAvatar) {
2410
      return Size(0.0, contentSize);
2411
    }
2412 2413
    double avatarWidth = 0.0;
    double avatarHeight = 0.0;
2414 2415 2416 2417
    if (theme.showAvatar) {
      avatarWidth += avatarDrawerAnimation.value * avatarBoxSize.width;
    } else {
      avatarWidth += avatarDrawerAnimation.value * contentSize;
2418
    }
2419
    avatarHeight += avatarBoxSize.height;
2420
    return Size(avatarWidth, avatarHeight);
2421
  }
2422

2423
  Size _layoutDeleteIcon(BoxConstraints contentConstraints, double contentSize, [ChildLayouter layoutChild = ChildLayoutHelper.layoutChild]) {
2424
    final double requestedSize = math.max(0.0, contentSize);
2425
    final BoxConstraints deleteIconConstraints = BoxConstraints.tightFor(
2426 2427 2428
      width: requestedSize,
      height: requestedSize,
    );
2429
    final Size boxSize = layoutChild(deleteIcon!, deleteIconConstraints);
2430
    if (!deleteIconShowing) {
2431
      return Size(0.0, contentSize);
2432
    }
2433 2434
    double deleteIconWidth = 0.0;
    double deleteIconHeight = 0.0;
2435 2436
    deleteIconWidth += deleteDrawerAnimation.value * boxSize.width;
    deleteIconHeight += boxSize.height;
2437
    return Size(deleteIconWidth, deleteIconHeight);
2438
  }
2439

2440
  @override
2441
  bool hitTest(BoxHitTestResult result, { required Offset position }) {
2442
    if (!size.contains(position)) {
2443 2444
      return false;
    }
2445 2446
    final bool hitIsOnDeleteIcon = deleteIcon != null && _hitIsOnDeleteIcon(
      padding: theme.padding,
2447 2448
      tapPosition: position,
      chipSize: size,
2449
      deleteButtonSize: deleteIcon!.size,
2450
      textDirection: textDirection!,
2451
    );
2452
    final RenderBox? hitTestChild = hitIsOnDeleteIcon
2453 2454 2455
        ? (deleteIcon ?? label ?? avatar)
        : (label ?? avatar);

2456 2457 2458 2459 2460
    if (hitTestChild != null) {
      final Offset center = hitTestChild.size.center(Offset.zero);
      return result.addWithRawTransform(
        transform: MatrixUtils.forceToPoint(center),
        position: position,
2461
        hitTest: (BoxHitTestResult result, Offset position) {
2462 2463 2464 2465 2466 2467
          assert(position == center);
          return hitTestChild.hitTest(result, position: center);
        },
      );
    }
    return false;
2468 2469
  }

2470
  @override
2471 2472 2473 2474 2475
  Size computeDryLayout(BoxConstraints constraints) {
    return _computeSizes(constraints, ChildLayoutHelper.dryLayoutChild).size;
  }

  _ChipSizes _computeSizes(BoxConstraints constraints, ChildLayouter layoutChild) {
2476 2477
    final BoxConstraints contentConstraints = constraints.loosen();
    // Find out the height of the label within the constraints.
2478
    final Offset densityAdjustment = Offset(0.0, theme.visualDensity.baseSizeAdjustment.dy / 2.0);
2479
    final Size rawLabelSize = layoutChild(label!, contentConstraints);
2480 2481
    final double contentSize = math.max(
      _kChipHeight - theme.padding.vertical + theme.labelPadding.vertical,
2482 2483 2484 2485 2486 2487 2488 2489 2490 2491
      rawLabelSize.height + theme.labelPadding.vertical,
    );
    final Size avatarSize = _layoutAvatar(contentConstraints, contentSize, layoutChild);
    final Size deleteIconSize = _layoutDeleteIcon(contentConstraints, contentSize, layoutChild);
    final Size labelSize = _layoutLabel(
      contentConstraints,
      avatarSize.width + deleteIconSize.width,
      Size(rawLabelSize.width, contentSize),
      rawLabelSize,
      layoutChild,
2492 2493 2494 2495
    );

    // This is the overall size of the content: it doesn't include
    // theme.padding, that is added in at the end.
2496
    final Size overallSize = Size(
2497 2498
      avatarSize.width + labelSize.width + deleteIconSize.width,
      contentSize,
2499
    ) + densityAdjustment;
2500 2501 2502 2503
    final Size paddedSize = Size(
      overallSize.width + theme.padding.horizontal,
      overallSize.height + theme.padding.vertical,
    );
2504

2505 2506 2507 2508 2509 2510 2511 2512 2513 2514 2515 2516 2517 2518
    return _ChipSizes(
      size: constraints.constrain(paddedSize),
      overall: overallSize,
      content: contentSize,
      densityAdjustment: densityAdjustment,
      avatar: avatarSize,
      label: labelSize,
      deleteIcon: deleteIconSize,
    );
  }

  @override
  void performLayout() {
    final _ChipSizes sizes = _computeSizes(constraints, ChildLayoutHelper.layoutChild);
2519 2520

    // Now we have all of the dimensions. Place the children where they belong.
2521 2522

    const double left = 0.0;
2523
    final double right = sizes.overall.width;
2524 2525

    Offset centerLayout(Size boxSize, double x) {
2526
      assert(sizes.content >= boxSize.height);
2527
      switch (textDirection!) {
2528
        case TextDirection.rtl:
2529
          return Offset(x - boxSize.width, (sizes.content - boxSize.height + sizes.densityAdjustment.dy) / 2.0);
2530
        case TextDirection.ltr:
2531
          return Offset(x, (sizes.content - boxSize.height + sizes.densityAdjustment.dy) / 2.0);
2532 2533
      }
    }
2534

2535 2536 2537 2538 2539 2540
    // These are the offsets to the upper left corners of the boxes (including
    // the child's padding) containing the children, for each child, but not
    // including the overall padding.
    Offset avatarOffset = Offset.zero;
    Offset labelOffset = Offset.zero;
    Offset deleteIconOffset = Offset.zero;
2541
    switch (textDirection!) {
2542
      case TextDirection.rtl:
2543 2544
        double start = right;
        if (theme.showCheckmark || theme.showAvatar) {
2545 2546
          avatarOffset = centerLayout(sizes.avatar, start);
          start -= sizes.avatar.width;
2547
        }
2548 2549
        labelOffset = centerLayout(sizes.label, start);
        start -= sizes.label.width;
2550
        if (deleteIconShowing) {
2551
          _deleteButtonRect = Rect.fromLTWH(
2552 2553
            0.0,
            0.0,
2554 2555
            sizes.deleteIcon.width + theme.padding.right,
            sizes.overall.height + theme.padding.vertical,
2556
          );
2557
          deleteIconOffset = centerLayout(sizes.deleteIcon, start);
2558
        } else {
2559
          _deleteButtonRect = Rect.zero;
2560
        }
2561
        start -= sizes.deleteIcon.width;
2562
        if (theme.canTapBody) {
2563 2564
          _pressRect = Rect.fromLTWH(
            _deleteButtonRect.width,
2565
            0.0,
2566 2567
            sizes.overall.width - _deleteButtonRect.width + theme.padding.horizontal,
            sizes.overall.height + theme.padding.vertical,
2568
          );
2569
        } else {
2570
          _pressRect = Rect.zero;
2571 2572 2573
        }
        break;
      case TextDirection.ltr:
2574 2575
        double start = left;
        if (theme.showCheckmark || theme.showAvatar) {
2576 2577
          avatarOffset = centerLayout(sizes.avatar, start - _boxSize(avatar).width + sizes.avatar.width);
          start += sizes.avatar.width;
2578
        }
2579 2580
        labelOffset = centerLayout(sizes.label, start);
        start += sizes.label.width;
2581
        if (theme.canTapBody) {
2582
          _pressRect = Rect.fromLTWH(
2583 2584
            0.0,
            0.0,
2585 2586
            deleteIconShowing
                ? start + theme.padding.left
2587 2588
                : sizes.overall.width + theme.padding.horizontal,
            sizes.overall.height + theme.padding.vertical,
2589
          );
2590
        } else {
2591
          _pressRect = Rect.zero;
2592
        }
2593
        start -= _boxSize(deleteIcon).width - sizes.deleteIcon.width;
2594
        if (deleteIconShowing) {
2595
          deleteIconOffset = centerLayout(sizes.deleteIcon, start);
2596
          _deleteButtonRect = Rect.fromLTWH(
2597
            start + theme.padding.left,
2598
            0.0,
2599 2600
            sizes.deleteIcon.width + theme.padding.right,
            sizes.overall.height + theme.padding.vertical,
2601
          );
2602
        } else {
2603
          _deleteButtonRect = Rect.zero;
2604 2605 2606
        }
        break;
    }
2607 2608
    // Center the label vertically.
    labelOffset = labelOffset +
2609
        Offset(
2610
          0.0,
2611
          ((sizes.label.height - theme.labelPadding.vertical) - _boxSize(label).height) / 2.0,
2612
        );
2613 2614 2615
    _boxParentData(avatar!).offset = theme.padding.topLeft + avatarOffset;
    _boxParentData(label!).offset = theme.padding.topLeft + labelOffset + theme.labelPadding.topLeft;
    _boxParentData(deleteIcon!).offset = theme.padding.topLeft + deleteIconOffset;
2616
    final Size paddedSize = Size(
2617 2618
      sizes.overall.width + theme.padding.horizontal,
      sizes.overall.height + theme.padding.vertical,
2619 2620 2621
    );
    size = constraints.constrain(paddedSize);
    assert(
2622 2623 2624 2625
      size.height == constraints.constrainHeight(paddedSize.height),
      "Constrained height ${size.height} doesn't match expected height "
      '${constraints.constrainWidth(paddedSize.height)}',
    );
2626
    assert(
2627 2628 2629 2630
      size.width == constraints.constrainWidth(paddedSize.width),
      "Constrained width ${size.width} doesn't match expected width "
      '${constraints.constrainWidth(paddedSize.width)}',
    );
2631
  }
2632

2633
  static final ColorTween selectionScrimTween = ColorTween(
2634 2635 2636 2637 2638 2639 2640 2641
    begin: Colors.transparent,
    end: _kSelectScrimColor,
  );

  Color get _disabledColor {
    if (enableAnimation == null || enableAnimation.isCompleted) {
      return Colors.white;
    }
2642
    final ColorTween enableTween;
2643 2644
    switch (theme.brightness) {
      case Brightness.light:
2645
        enableTween = ColorTween(
2646 2647 2648 2649 2650
          begin: Colors.white.withAlpha(_kDisabledAlpha),
          end: Colors.white,
        );
        break;
      case Brightness.dark:
2651
        enableTween = ColorTween(
2652 2653 2654 2655 2656
          begin: Colors.black.withAlpha(_kDisabledAlpha),
          end: Colors.black,
        );
        break;
    }
2657
    return enableTween.evaluate(enableAnimation)!;
2658 2659
  }

2660
  void _paintCheck(Canvas canvas, Offset origin, double size) {
2661
    Color? paintColor;
2662 2663 2664 2665 2666 2667 2668 2669 2670 2671 2672
    if (theme.checkmarkColor != null) {
      paintColor = theme.checkmarkColor;
    } else {
      switch (theme.brightness) {
        case Brightness.light:
          paintColor = theme.showAvatar ? Colors.white : Colors.black.withAlpha(_kCheckmarkAlpha);
          break;
        case Brightness.dark:
          paintColor = theme.showAvatar ? Colors.black : Colors.white.withAlpha(_kCheckmarkAlpha);
          break;
      }
2673 2674
    }

2675
    final ColorTween fadeTween = ColorTween(begin: Colors.transparent, end: paintColor);
2676 2677 2678 2679 2680

    paintColor = checkmarkAnimation.status == AnimationStatus.reverse
        ? fadeTween.evaluate(checkmarkAnimation)
        : paintColor;

2681
    final Paint paint = Paint()
2682
      ..color = paintColor!
2683
      ..style = PaintingStyle.stroke
2684
      ..strokeWidth = _kCheckmarkStrokeWidth * (avatar != null ? avatar!.size.height / 24.0 : 1.0);
2685 2686 2687 2688 2689 2690 2691 2692 2693 2694
    final double t = checkmarkAnimation.status == AnimationStatus.reverse
        ? 1.0
        : checkmarkAnimation.value;
    if (t == 0.0) {
      // Nothing to draw.
      return;
    }
    assert(t > 0.0 && t <= 1.0);
    // As t goes from 0.0 to 1.0, animate the two check mark strokes from the
    // short side to the long side.
2695 2696 2697 2698
    final Path path = Path();
    final Offset start = Offset(size * 0.15, size * 0.45);
    final Offset mid = Offset(size * 0.4, size * 0.7);
    final Offset end = Offset(size * 0.85, size * 0.25);
2699 2700
    if (t < 0.5) {
      final double strokeT = t * 2.0;
2701
      final Offset drawMid = Offset.lerp(start, mid, strokeT)!;
2702 2703 2704 2705
      path.moveTo(origin.dx + start.dx, origin.dy + start.dy);
      path.lineTo(origin.dx + drawMid.dx, origin.dy + drawMid.dy);
    } else {
      final double strokeT = (t - 0.5) * 2.0;
2706
      final Offset drawEnd = Offset.lerp(mid, end, strokeT)!;
2707 2708 2709 2710 2711 2712 2713 2714 2715
      path.moveTo(origin.dx + start.dx, origin.dy + start.dy);
      path.lineTo(origin.dx + mid.dx, origin.dy + mid.dy);
      path.lineTo(origin.dx + drawEnd.dx, origin.dy + drawEnd.dy);
    }
    canvas.drawPath(path, paint);
  }

  void _paintSelectionOverlay(PaintingContext context, Offset offset) {
    if (isDrawingCheckmark) {
2716 2717
      if (theme.showAvatar) {
        final Rect avatarRect = _boxRect(avatar).shift(offset);
2718
        final Paint darkenPaint = Paint()
2719
          ..color = selectionScrimTween.evaluate(checkmarkAnimation)!
2720
          ..blendMode = BlendMode.srcATop;
2721
        final Path path =  avatarBorder!.getOuterPath(avatarRect);
2722
        context.canvas.drawPath(path, darkenPaint);
2723
      }
2724
      // Need to make the check mark be a little smaller than the avatar.
2725 2726 2727
      final double checkSize = avatar!.size.height * 0.75;
      final Offset checkOffset = _boxParentData(avatar!).offset +
          Offset(avatar!.size.height * 0.125, avatar!.size.height * 0.125);
2728 2729 2730 2731 2732 2733
      _paintCheck(context.canvas, offset + checkOffset, checkSize);
    }
  }

  void _paintAvatar(PaintingContext context, Offset offset) {
    void paintWithOverlay(PaintingContext context, Offset offset) {
2734
      context.paintChild(avatar!, _boxParentData(avatar!).offset + offset);
2735 2736 2737 2738 2739 2740
      _paintSelectionOverlay(context, offset);
    }

    if (theme.showAvatar == false && avatarDrawerAnimation.isDismissed) {
      return;
    }
2741 2742
    final Color disabledColor = _disabledColor;
    final int disabledColorAlpha = disabledColor.alpha;
2743
    if (needsCompositing) {
2744
      context.pushLayer(OpacityLayer(alpha: disabledColorAlpha), paintWithOverlay, offset);
2745 2746 2747 2748
    } else {
      if (disabledColorAlpha != 0xff) {
        context.canvas.saveLayer(
          _boxRect(avatar).shift(offset).inflate(20.0),
2749
          Paint()..color = disabledColor,
2750 2751 2752 2753 2754 2755 2756 2757 2758
        );
      }
      paintWithOverlay(context, offset);
      if (disabledColorAlpha != 0xff) {
        context.canvas.restore();
      }
    }
  }

2759
  void _paintChild(PaintingContext context, Offset offset, RenderBox? child, bool? isEnabled) {
2760 2761 2762 2763 2764 2765 2766
    if (child == null) {
      return;
    }
    final int disabledColorAlpha = _disabledColor.alpha;
    if (!enableAnimation.isCompleted) {
      if (needsCompositing) {
        context.pushLayer(
2767
          OpacityLayer(alpha: disabledColorAlpha),
2768 2769 2770 2771 2772 2773 2774
          (PaintingContext context, Offset offset) {
            context.paintChild(child, _boxParentData(child).offset + offset);
          },
          offset,
        );
      } else {
        final Rect childRect = _boxRect(child).shift(offset);
2775
        context.canvas.saveLayer(childRect.inflate(20.0), Paint()..color = _disabledColor);
2776
        context.paintChild(child, _boxParentData(child).offset + offset);
2777
        context.canvas.restore();
2778
      }
2779 2780
    } else {
      context.paintChild(child, _boxParentData(child).offset + offset);
2781
    }
2782
  }
2783

2784 2785 2786 2787 2788 2789 2790
  @override
  void paint(PaintingContext context, Offset offset) {
    _paintAvatar(context, offset);
    if (deleteIconShowing) {
      _paintChild(context, offset, deleteIcon, isEnabled);
    }
    _paintChild(context, offset, label, isEnabled);
2791 2792
  }

2793 2794 2795 2796
  // Set this to true to have outlines of the tap targets drawn over
  // the chip.  This should never be checked in while set to 'true'.
  static const bool _debugShowTapTargetOutlines = false;

2797 2798
  @override
  void debugPaint(PaintingContext context, Offset offset) {
2799 2800 2801 2802 2803 2804 2805 2806
    assert(!_debugShowTapTargetOutlines || () {
      // Draws a rect around the tap targets to help with visualizing where
      // they really are.
      final Paint outlinePaint = Paint()
        ..color = const Color(0xff800000)
        ..strokeWidth = 1.0
        ..style = PaintingStyle.stroke;
      if (deleteIconShowing) {
2807
        context.canvas.drawRect(_deleteButtonRect.shift(offset), outlinePaint);
2808 2809
      }
      context.canvas.drawRect(
2810
        _pressRect.shift(offset),
2811 2812 2813 2814
        outlinePaint..color = const Color(0xff008000),
      );
      return true;
    }());
2815 2816 2817
  }

  @override
2818
  bool hitTestSelf(Offset position) => _deleteButtonRect.contains(position) || _pressRect.contains(position);
2819
}
2820

2821 2822 2823 2824 2825 2826 2827 2828 2829 2830 2831 2832 2833 2834 2835 2836 2837 2838 2839
class _ChipSizes {
  _ChipSizes({
    required this.size,
    required this.overall,
    required this.content,
    required this.avatar,
    required this.label,
    required this.deleteIcon,
    required this.densityAdjustment,
});
  final Size size;
  final Size overall;
  final double content;
  final Size avatar;
  final Size label;
  final Size deleteIcon;
  final Offset densityAdjustment;
}

2840 2841
class _UnconstrainedInkSplashFactory extends InteractiveInkFeatureFactory {
  const _UnconstrainedInkSplashFactory(this.parentFactory);
2842

2843
  final InteractiveInkFeatureFactory parentFactory;
2844 2845 2846

  @override
  InteractiveInkFeature create({
2847 2848 2849 2850 2851
    required MaterialInkController controller,
    required RenderBox referenceBox,
    required Offset position,
    required Color color,
    required TextDirection textDirection,
2852
    bool containedInkWell = false,
2853 2854 2855 2856 2857
    RectCallback? rectCallback,
    BorderRadius? borderRadius,
    ShapeBorder? customBorder,
    double? radius,
    VoidCallback? onRemoved,
2858
  }) {
2859
    return parentFactory.create(
2860 2861 2862 2863 2864 2865 2866 2867 2868
      controller: controller,
      referenceBox: referenceBox,
      position: position,
      color: color,
      rectCallback: rectCallback,
      borderRadius: borderRadius,
      customBorder: customBorder,
      radius: radius,
      onRemoved: onRemoved,
2869
      textDirection: textDirection,
2870 2871 2872 2873
    );
  }
}

2874 2875
bool _hitIsOnDeleteIcon({
  required EdgeInsetsGeometry padding,
2876 2877
  required Offset tapPosition,
  required Size chipSize,
2878
  required Size deleteButtonSize,
2879
  required TextDirection textDirection,
2880
}) {
2881 2882 2883 2884 2885
  // The chipSize includes the padding, so we need to deflate the size and adjust the
  // tap position to account for the padding.
  final EdgeInsets resolvedPadding = padding.resolve(textDirection);
  final Size deflatedSize = resolvedPadding.deflateSize(chipSize);
  final Offset adjustedPosition = tapPosition - Offset(resolvedPadding.left, resolvedPadding.top);
2886 2887 2888 2889 2890
  // The delete button hit area should be at least the width of the delete
  // button, but, if there's room, up to 24 pixels from the center of the delete
  // icon (corresponding to part of a 48x48 square that Material would prefer
  // for touch targets), but no more than approximately half of the overall size
  // of the chip when the chip is small.
2891 2892
  //
  // This isn't affected by materialTapTargetSize because it only applies to the
2893 2894 2895 2896 2897 2898 2899 2900 2901
  // width of the tappable region within the chip, not outside of the chip,
  // which is handled elsewhere. Also because delete buttons aren't specified to
  // be used on touch devices, only desktop devices.

  // Max out at not quite half, so that tests that tap on the center of a small
  // chip will still hit the chip, not the delete button.
  final double accessibleDeleteButtonWidth = math.min(
    deflatedSize.width * 0.499,
    math.max(deleteButtonSize.width, 24.0 + deleteButtonSize.width / 2.0),
2902 2903 2904 2905 2906 2907
  );
  switch (textDirection) {
    case TextDirection.ltr:
      return adjustedPosition.dx >= deflatedSize.width - accessibleDeleteButtonWidth;
    case TextDirection.rtl:
      return adjustedPosition.dx <= accessibleDeleteButtonWidth;
2908 2909
  }
}