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

5 6
import 'dart:math' as math;

7
import 'package:flutter/foundation.dart' show clampDouble;
8 9
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
10

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

28 29 30
// Some design constants
const double _kChipHeight = 32.0;
const double _kDeleteIconSize = 18.0;
31 32 33

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

36 37 38 39 40 41
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);
42

43 44
const Color _kSelectScrimColor = Color(0x60191919);
const Icon _kDefaultDeleteIcon = Icon(Icons.cancel, size: _kDeleteIconSize);
45

46
/// An interface defining the base attributes for a Material Design chip.
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
///
/// 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.
65
///  * <https://material.io/design/components/chips.html>
66
abstract interface class ChipAttributes {
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
  /// The default label style is [TextTheme.bodyLarge] from the overall
80
  /// 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

  /// Color of the chip's shadow when the elevation is greater than 0.
  ///
186
  /// The default is null.
187
  Color? get shadowColor;
188 189 190 191 192 193 194 195 196 197 198

  /// Color of the chip's surface tint overlay when its elevation is
  /// greater than 0.
  ///
  /// The default is null.
  Color? get surfaceTintColor;

  /// Theme used for all icons in the chip.
  ///
  /// The default is null.
  IconThemeData? get iconTheme;
199 200
}

201
/// An interface for Material Design chips that can be deleted.
202 203 204 205 206 207 208 209 210 211 212
///
/// 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.
213
///  * <https://material.io/design/components/chips.html>
214
abstract interface class DeletableChipAttributes {
215 216 217
  /// The icon displayed when [onDeleted] is set.
  ///
  /// Defaults to an [Icon] widget set to use [Icons.cancel].
218
  Widget? get deleteIcon;
219 220 221 222 223 224 225

  /// 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
226
  /// have to do something similar to the following sample:
227
  ///
228
  /// {@tool dartpad}
229 230
  /// This sample shows how to use [onDeleted] to remove an entry when the
  /// delete button is tapped.
231
  ///
232
  /// ** See code in examples/api/lib/material/chip/deletable_chip_attributes.on_deleted.0.dart **
233
  /// {@end-tool}
234
  VoidCallback? get onDeleted;
235

236 237 238 239 240 241 242 243 244
  /// 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.
245
  Color? get deleteIconColor;
246 247

  /// The message to be used for the chip's delete button tooltip.
248
  ///
249 250
  /// If provided with an empty string, the tooltip of the delete button will be
  /// disabled.
251
  ///
252 253
  /// If null, the default [MaterialLocalizations.deleteButtonTooltip] will be
  /// used.
254
  String? get deleteButtonTooltipMessage;
255 256 257 258 259 260 261 262 263 264

  /// 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;
265 266
}

267
/// An interface for Material Design chips that can have check marks.
268 269 270 271 272 273 274 275 276 277 278 279
///
/// 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>
280
abstract interface class CheckmarkableChipAttributes {
281 282
  /// Whether or not to show a check mark when
  /// [SelectableChipAttributes.selected] is true.
283 284
  ///
  /// Defaults to true.
285
  bool? get showCheckmark;
286 287 288 289 290 291 292

  /// [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.
293
  Color? get checkmarkColor;
294 295
}

296
/// An interface for Material Design chips that can be selected.
297 298 299 300 301 302 303 304 305 306 307 308 309
///
/// 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.
310
///  * <https://material.io/design/components/chips.html>
311
abstract interface class SelectableChipAttributes {
312 313 314 315 316 317 318 319
  /// 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;

320 321
  /// Called when the chip should change between selected and de-selected
  /// states.
322 323 324 325 326 327 328 329 330 331 332 333 334 335 336
  ///
  /// 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.
  ///
337
  /// {@tool snippet}
338 339
  ///
  /// A [StatefulWidget] that illustrates use of onSelected in an [InputChip].
340 341 342
  ///
  /// ```dart
  /// class Wood extends StatefulWidget {
343
  ///   const Wood({super.key});
344
  ///
345
  ///   @override
346
  ///   State<StatefulWidget> createState() => WoodState();
347 348 349 350 351 352 353
  /// }
  ///
  /// class WoodState extends State<Wood> {
  ///   bool _useChisel = false;
  ///
  ///   @override
  ///   Widget build(BuildContext context) {
354
  ///     return InputChip(
355 356 357 358 359 360 361 362 363 364 365
  ///       label: const Text('Use Chisel'),
  ///       selected: _useChisel,
  ///       onSelected: (bool newValue) {
  ///         setState(() {
  ///           _useChisel = newValue;
  ///         });
  ///       },
  ///     );
  ///   }
  /// }
  /// ```
366
  /// {@end-tool}
367
  ValueChanged<bool>? get onSelected;
368

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

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

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

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

  /// 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;
400 401
}

402
/// An interface for Material Design chips that can be enabled and disabled.
403 404 405 406 407 408 409 410 411 412 413 414 415
///
/// 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.
416
///  * <https://material.io/design/components/chips.html>
417
abstract interface class DisabledChipAttributes {
418 419 420 421
  /// 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],
422
  /// and [DeletableChipAttributes.onDeleted]), then the
423 424 425 426 427 428 429 430 431 432 433
  /// 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;

434 435
  /// The color used for the chip's background to indicate that it is not
  /// enabled.
436 437 438
  ///
  /// The chip is disabled when [isEnabled] is false, or all three of
  /// [SelectableChipAttributes.onSelected], [TappableChipAttributes.onPressed],
439
  /// and [DeletableChipAttributes.onDeleted] are null.
440 441
  ///
  /// It defaults to [Colors.black38].
442
  Color? get disabledColor;
443 444
}

445
/// An interface for Material Design chips that can be tapped.
446 447 448 449 450 451 452 453 454 455 456 457 458 459
///
/// 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.
460
///  * <https://material.io/design/components/chips.html>
461
abstract interface class TappableChipAttributes {
462 463 464 465 466 467
  /// 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.
  ///
468
  /// {@tool snippet}
469 470 471
  ///
  /// ```dart
  /// class Blacksmith extends StatelessWidget {
472
  ///   const Blacksmith({super.key});
473
  ///
474 475 476 477 478 479
  ///   void startHammering() {
  ///     print('bang bang bang');
  ///   }
  ///
  ///   @override
  ///   Widget build(BuildContext context) {
480
  ///     return InputChip(
481 482 483 484 485 486
  ///       label: const Text('Apply Hammer'),
  ///       onPressed: startHammering,
  ///     );
  ///   }
  /// }
  /// ```
487
  /// {@end-tool}
488
  VoidCallback? get onPressed;
489

490 491 492
  /// Elevation to be applied on the chip relative to its parent during the
  /// press motion.
  ///
493 494
  /// This controls the size of the shadow below the chip.
  ///
495
  /// Defaults to 8. The value is always non-negative.
496
  double? get pressElevation;
497

498 499
  /// Tooltip string to be used for the body area (where the label and avatar
  /// are) of the chip.
500
  String? get tooltip;
501 502
}

503
/// A Material Design chip.
504 505 506 507 508 509 510
///
/// 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.
///
511 512 513 514
/// 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.
515
///
516
/// {@tool snippet}
517 518
///
/// ```dart
519 520
/// Chip(
///   avatar: CircleAvatar(
521
///     backgroundColor: Colors.grey.shade800,
522
///     child: const Text('AB'),
523
///   ),
524
///   label: const Text('Aaron Burr'),
525 526
/// )
/// ```
527
/// {@end-tool}
528 529 530 531 532 533 534 535 536 537 538 539 540
///
/// 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.
541
///  * <https://material.io/design/components/chips.html>
542
class Chip extends StatelessWidget implements ChipAttributes, DeletableChipAttributes {
543
  /// Creates a Material Design chip.
544
  ///
545
  /// The [label], [autofocus], and [clipBehavior] arguments must not be null.
546
  /// The [elevation] must be null or non-negative.
547
  const Chip({
548
    super.key,
549
    this.avatar,
550
    required this.label,
551 552 553 554 555 556
    this.labelStyle,
    this.labelPadding,
    this.deleteIcon,
    this.onDeleted,
    this.deleteIconColor,
    this.deleteButtonTooltipMessage,
557
    this.side,
558
    this.shape,
559
    this.clipBehavior = Clip.none,
560 561
    this.focusNode,
    this.autofocus = false,
562 563
    this.backgroundColor,
    this.padding,
564
    this.visualDensity,
565
    this.materialTapTargetSize,
566
    this.elevation,
567
    this.shadowColor,
568 569
    this.surfaceTintColor,
    this.iconTheme,
570 571 572 573 574
    @Deprecated(
      'Migrate to deleteButtonTooltipMessage. '
      'This feature was deprecated after v2.10.0-0.3.pre.'
    )
    this.useDeleteButtonTooltip = true,
575
  }) : assert(elevation == null || elevation >= 0.0);
576 577

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

  @override
  Widget build(BuildContext context) {
    assert(debugCheckHasMaterial(context));
629
    return RawChip(
630 631 632 633 634 635 636
      avatar: avatar,
      label: label,
      labelStyle: labelStyle,
      labelPadding: labelPadding,
      deleteIcon: deleteIcon,
      onDeleted: onDeleted,
      deleteIconColor: deleteIconColor,
637
      useDeleteButtonTooltip: useDeleteButtonTooltip,
638 639
      deleteButtonTooltipMessage: deleteButtonTooltipMessage,
      tapEnabled: false,
640
      side: side,
641
      shape: shape,
642
      clipBehavior: clipBehavior,
643 644
      focusNode: focusNode,
      autofocus: autofocus,
645 646
      backgroundColor: backgroundColor,
      padding: padding,
647
      visualDensity: visualDensity,
648
      materialTapTargetSize: materialTapTargetSize,
649
      elevation: elevation,
650
      shadowColor: shadowColor,
651
      surfaceTintColor: surfaceTintColor,
652 653 654 655
    );
  }
}

656
/// A raw Material Design chip.
657
///
658 659 660
/// 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:
661
///
662 663 664 665 666 667 668
///  * [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.
669
///
670 671
/// Raw chips are typically only used if you want to create your own custom chip
/// type.
672
///
673 674 675 676
/// 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
677
///
678
/// Requires one of its ancestors to be a [Material] widget.
Ian Hickson's avatar
Ian Hickson committed
679
///
680
/// See also:
681
///
Ian Hickson's avatar
Ian Hickson committed
682
///  * [CircleAvatar], which shows images or initials of people.
683 684
///  * [Wrap], A widget that displays its children in multiple horizontal or
///    vertical runs.
685
///  * <https://material.io/design/components/chips.html>
686 687 688 689 690
class RawChip extends StatefulWidget
    implements
        ChipAttributes,
        DeletableChipAttributes,
        SelectableChipAttributes,
691
        CheckmarkableChipAttributes,
692 693
        DisabledChipAttributes,
        TappableChipAttributes {
694
  /// Creates a RawChip.
695
  ///
696 697 698
  /// The [onPressed] and [onSelected] callbacks must not both be specified at
  /// the same time.
  ///
699 700 701 702
  /// 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].
703
  const RawChip({
704
    super.key,
705
    this.defaultProperties,
706
    this.avatar,
707
    required this.label,
708
    this.labelStyle,
709
    this.padding,
710
    this.visualDensity,
711
    this.labelPadding,
712
    Widget? deleteIcon,
713
    this.onDeleted,
714
    this.deleteIconColor,
715 716 717
    this.deleteButtonTooltipMessage,
    this.onPressed,
    this.onSelected,
718
    this.pressElevation,
719
    this.tapEnabled = true,
720
    this.selected = false,
721
    this.isEnabled = true,
722 723
    this.disabledColor,
    this.selectedColor,
724
    this.tooltip,
725
    this.side,
726
    this.shape,
727
    this.clipBehavior = Clip.none,
728 729
    this.focusNode,
    this.autofocus = false,
730
    this.backgroundColor,
731
    this.materialTapTargetSize,
732
    this.elevation,
733
    this.shadowColor,
734 735
    this.surfaceTintColor,
    this.iconTheme,
736
    this.selectedShadowColor,
737 738
    this.showCheckmark = true,
    this.checkmarkColor,
739
    this.avatarBorder = const CircleBorder(),
740 741 742 743 744
    @Deprecated(
      'Migrate to deleteButtonTooltipMessage. '
      'This feature was deprecated after v2.10.0-0.3.pre.'
    )
    this.useDeleteButtonTooltip = true,
745
  }) : assert(pressElevation == null || pressElevation >= 0.0),
746
       assert(elevation == null || elevation >= 0.0),
747
       deleteIcon = deleteIcon ?? _kDefaultDeleteIcon;
748

749 750 751 752 753 754 755
  /// Defines the defaults for the chip properties if
  /// they are not specified elsewhere.
  ///
  /// If null then [ChipThemeData.fromDefaults] will be used
  /// for the default properties.
  final ChipThemeData? defaultProperties;

756
  @override
757
  final Widget? avatar;
758 759 760
  @override
  final Widget label;
  @override
761
  final TextStyle? labelStyle;
762
  @override
763
  final EdgeInsetsGeometry? labelPadding;
764 765 766
  @override
  final Widget deleteIcon;
  @override
767
  final VoidCallback? onDeleted;
768
  @override
769
  final Color? deleteIconColor;
770
  @override
771
  final String? deleteButtonTooltipMessage;
772
  @override
773
  final ValueChanged<bool>? onSelected;
774
  @override
775
  final VoidCallback? onPressed;
776
  @override
777
  final double? pressElevation;
778
  @override
779 780 781 782
  final bool selected;
  @override
  final bool isEnabled;
  @override
783
  final Color? disabledColor;
784
  @override
785
  final Color? selectedColor;
786
  @override
787
  final String? tooltip;
788
  @override
789 790 791
  final BorderSide? side;
  @override
  final OutlinedBorder? shape;
792
  @override
793 794
  final Clip clipBehavior;
  @override
795
  final FocusNode? focusNode;
796 797 798
  @override
  final bool autofocus;
  @override
799
  final Color? backgroundColor;
800
  @override
801
  final EdgeInsetsGeometry? padding;
802
  @override
803
  final VisualDensity? visualDensity;
804
  @override
805
  final MaterialTapTargetSize? materialTapTargetSize;
806
  @override
807
  final double? elevation;
808
  @override
809
  final Color? shadowColor;
810
  @override
811 812 813 814
  final Color? surfaceTintColor;
  @override
  final IconThemeData? iconTheme;
  @override
815
  final Color? selectedShadowColor;
816
  @override
817
  final bool? showCheckmark;
818
  @override
819
  final Color? checkmarkColor;
820
  @override
821
  final ShapeBorder avatarBorder;
822 823 824 825 826 827
  @override
  @Deprecated(
    'Migrate to deleteButtonTooltipMessage. '
    'This feature was deprecated after v2.10.0-0.3.pre.'
  )
  final bool useDeleteButtonTooltip;
828

829 830
  /// If set, this indicates that the chip should be disabled if all of the
  /// tap callbacks ([onSelected], [onPressed]) are null.
831
  ///
832 833 834
  /// 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.
835
  ///
836 837
  /// Defaults to true.
  final bool tapEnabled;
838

839
  @override
840
  State<RawChip> createState() => _RawChipState();
841
}
842

843
class _RawChipState extends State<RawChip> with MaterialStateMixin, TickerProviderStateMixin<RawChip> {
844
  static const Duration pressedAnimationDuration = Duration(milliseconds: 75);
845

846 847 848 849 850 851 852 853 854
  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;
855 856 857 858 859 860 861 862 863

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

865
  bool _isTapping = false;
866
  bool get isTapping => canTap && _isTapping;
867

868 869 870 871
  @override
  void initState() {
    assert(widget.onSelected == null || widget.onPressed == null);
    super.initState();
872 873
    setMaterialState(MaterialState.disabled, !widget.isEnabled);
    setMaterialState(MaterialState.selected, widget.selected);
874
    selectController = AnimationController(
875 876 877 878
      duration: _kSelectDuration,
      value: widget.selected == true ? 1.0 : 0.0,
      vsync: this,
    );
879
    selectionFade = CurvedAnimation(
880 881 882
      parent: selectController,
      curve: Curves.fastOutSlowIn,
    );
883
    avatarDrawerController = AnimationController(
884 885 886 887
      duration: _kDrawerDuration,
      value: hasAvatar || widget.selected == true ? 1.0 : 0.0,
      vsync: this,
    );
888
    deleteDrawerController = AnimationController(
889 890 891 892
      duration: _kDrawerDuration,
      value: hasDeleteButton ? 1.0 : 0.0,
      vsync: this,
    );
893
    enableController = AnimationController(
894 895 896 897
      duration: _kDisableDuration,
      value: widget.isEnabled ? 1.0 : 0.0,
      vsync: this,
    );
898

899 900 901 902 903 904 905 906
    // 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;
907
    checkmarkAnimation = CurvedAnimation(
908
      parent: selectController,
909 910
      curve: Interval(1.0 - checkmarkPercentage, 1.0, curve: Curves.fastOutSlowIn),
      reverseCurve: Interval(
911 912 913 914 915
        1.0 - checkmarkReversePercentage,
        1.0,
        curve: Curves.fastOutSlowIn,
      ),
    );
916
    deleteDrawerAnimation = CurvedAnimation(
917 918 919
      parent: deleteDrawerController,
      curve: Curves.fastOutSlowIn,
    );
920
    avatarDrawerAnimation = CurvedAnimation(
921 922
      parent: avatarDrawerController,
      curve: Curves.fastOutSlowIn,
923
      reverseCurve: Interval(
924 925 926 927 928
        1.0 - avatarDrawerReversePercentage,
        1.0,
        curve: Curves.fastOutSlowIn,
      ),
    );
929
    enableAnimation = CurvedAnimation(
930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947
      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;
    }
948
    setMaterialState(MaterialState.pressed, true);
949 950 951 952 953 954 955 956 957
    setState(() {
      _isTapping = true;
    });
  }

  void _handleTapCancel() {
    if (!canTap) {
      return;
    }
958
    setMaterialState(MaterialState.pressed, false);
959 960 961 962 963 964 965 966 967
    setState(() {
      _isTapping = false;
    });
  }

  void _handleTap() {
    if (!canTap) {
      return;
    }
968
    setMaterialState(MaterialState.pressed, false);
969 970 971 972 973 974 975 976
    setState(() {
      _isTapping = false;
    });
    // Only one of these can be set, so only one will be called.
    widget.onSelected?.call(!widget.selected);
    widget.onPressed?.call();
  }

977
  OutlinedBorder _getShape(ThemeData theme, ChipThemeData chipTheme, ChipThemeData chipDefaults) {
978
    final BorderSide? resolvedSide = MaterialStateProperty.resolveAs<BorderSide?>(widget.side, materialStates)
979 980
      ?? MaterialStateProperty.resolveAs<BorderSide?>(chipTheme.side, materialStates)
      ?? MaterialStateProperty.resolveAs<BorderSide?>(chipDefaults.side, materialStates);
981
    final OutlinedBorder resolvedShape = MaterialStateProperty.resolveAs<OutlinedBorder?>(widget.shape, materialStates)
982 983
      ?? MaterialStateProperty.resolveAs<OutlinedBorder?>(chipTheme.shape, materialStates)
      ?? MaterialStateProperty.resolveAs<OutlinedBorder?>(chipDefaults.shape, materialStates)
984 985 986 987
      ?? const StadiumBorder();
    return resolvedShape.copyWith(side: resolvedSide);
  }

988 989
  /// Picks between three different colors, depending upon the state of two
  /// different animations.
990
  Color? _getBackgroundColor(ThemeData theme, ChipThemeData chipTheme, ChipThemeData chipDefaults) {
991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025
    if (theme.useMaterial3) {
      final ColorTween backgroundTween = ColorTween(
        begin: widget.disabledColor
          ?? chipTheme.disabledColor
          ?? chipDefaults.disabledColor,
        end: widget.backgroundColor
          ?? chipTheme.backgroundColor
          ?? chipDefaults.backgroundColor,
      );
      final ColorTween selectTween = ColorTween(
        begin: backgroundTween.evaluate(enableController),
        end: widget.selectedColor
          ?? chipTheme.selectedColor
          ?? chipDefaults.selectedColor,
      );
      return selectTween.evaluate(selectionFade);
    } else {
      final ColorTween backgroundTween = ColorTween(
        begin: widget.disabledColor
            ?? chipTheme.disabledColor
            ?? theme.disabledColor,
        end: widget.backgroundColor
            ?? chipTheme.backgroundColor
            ?? theme.chipTheme.backgroundColor
            ?? chipDefaults.backgroundColor,
      );
      final ColorTween selectTween = ColorTween(
        begin: backgroundTween.evaluate(enableController),
        end: widget.selectedColor
            ?? chipTheme.selectedColor
            ?? theme.chipTheme.selectedColor
            ?? chipDefaults.selectedColor,
      );
      return selectTween.evaluate(selectionFade);
    }
1026 1027 1028 1029 1030 1031 1032
  }

  @override
  void didUpdateWidget(RawChip oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.isEnabled != widget.isEnabled) {
      setState(() {
1033
        setMaterialState(MaterialState.disabled, !widget.isEnabled);
1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051
        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(() {
1052
        setMaterialState(MaterialState.selected, widget.selected);
1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070
        if (widget.selected == true) {
          selectController.forward();
        } else {
          selectController.reverse();
        }
      });
    }
    if (oldWidget.onDeleted != widget.onDeleted) {
      setState(() {
        if (hasDeleteButton) {
          deleteDrawerController.forward();
        } else {
          deleteDrawerController.reverse();
        }
      });
    }
  }

1071 1072
  Widget? _wrapWithTooltip({String? tooltip, bool enabled = true, Widget? child}) {
    if (child == null || !enabled || tooltip == null) {
1073 1074
      return child;
    }
1075
    return Tooltip(
1076 1077 1078 1079 1080
      message: tooltip,
      child: child,
    );
  }

1081
  Widget? _buildDeleteIcon(
1082 1083 1084
    BuildContext context,
    ThemeData theme,
    ChipThemeData chipTheme,
1085
    ChipThemeData chipDefaults,
1086
  ) {
1087 1088 1089
    if (!hasDeleteButton) {
      return null;
    }
1090 1091 1092 1093
    return Semantics(
      container: true,
      button: true,
      child: _wrapWithTooltip(
1094 1095 1096
        tooltip: widget.useDeleteButtonTooltip
          ? widget.deleteButtonTooltipMessage ?? MaterialLocalizations.of(context).deleteButtonTooltip
          : null,
1097 1098
        enabled: widget.onDeleted != null,
        child: InkWell(
1099 1100 1101 1102 1103
          // 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,
1104 1105
          child: IconTheme(
            data: theme.iconTheme.copyWith(
1106 1107 1108 1109
              color: widget.deleteIconColor
                ?? chipTheme.deleteIconColor
                ?? theme.chipTheme.deleteIconColor
                ?? chipDefaults.deleteIconColor,
1110 1111
            ),
            child: widget.deleteIcon,
1112 1113 1114 1115 1116
          ),
        ),
      ),
    );
  }
1117

1118
  @override
1119
  Widget build(BuildContext context) {
1120
    assert(debugCheckHasMaterial(context));
1121 1122
    assert(debugCheckHasMediaQuery(context));
    assert(debugCheckHasDirectionality(context));
1123
    assert(debugCheckHasMaterialLocalizations(context));
1124

1125 1126 1127 1128
    /// 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.
1129
    final EdgeInsetsGeometry defaultLabelPadding = EdgeInsets.lerp(
1130 1131
      const EdgeInsets.symmetric(horizontal: 8.0),
      const EdgeInsets.symmetric(horizontal: 4.0),
1132
      clampDouble(MediaQuery.textScaleFactorOf(context) - 1.0, 0.0, 1.0),
1133
    )!;
1134

1135
    final ThemeData theme = Theme.of(context);
1136
    final ChipThemeData chipTheme = ChipTheme.of(context);
1137
    final Brightness brightness = chipTheme.brightness ?? theme.brightness;
1138 1139 1140 1141 1142 1143 1144 1145 1146
    final ChipThemeData chipDefaults = widget.defaultProperties ??
      (theme.useMaterial3
        ? _ChipDefaultsM3(context, widget.isEnabled)
        : ChipThemeData.fromDefaults(
            brightness: brightness,
            secondaryColor: brightness == Brightness.dark ? Colors.tealAccent[200]! : theme.primaryColor,
            labelStyle: theme.textTheme.bodyLarge!,
          )
        );
1147
    final TextDirection? textDirection = Directionality.maybeOf(context);
1148 1149 1150 1151
    final OutlinedBorder resolvedShape = _getShape(theme, chipTheme, chipDefaults);

    final double elevation = widget.elevation
      ?? chipTheme.elevation
1152 1153
      ?? chipDefaults.elevation
      ?? 0;
1154 1155
    final double pressElevation = widget.pressElevation
      ?? chipTheme.pressElevation
1156 1157 1158
      ?? chipDefaults.pressElevation
      ?? 0;
    final Color? shadowColor = widget.shadowColor
1159
      ?? chipTheme.shadowColor
1160 1161 1162 1163 1164
      ?? chipDefaults.shadowColor;
    final Color? surfaceTintColor = widget.surfaceTintColor
      ?? chipTheme.surfaceTintColor
      ?? chipDefaults.surfaceTintColor;
    final Color? selectedShadowColor = widget.selectedShadowColor
1165
      ?? chipTheme.selectedShadowColor
1166
      ?? chipDefaults.selectedShadowColor;
1167 1168
    final Color? checkmarkColor = widget.checkmarkColor
      ?? chipTheme.checkmarkColor
1169
      ?? chipDefaults.checkmarkColor;
1170 1171
    final bool showCheckmark = widget.showCheckmark
      ?? chipTheme.showCheckmark
1172
      ?? chipDefaults.showCheckmark!;
1173 1174 1175
    final EdgeInsetsGeometry padding = widget.padding
      ?? chipTheme.padding
      ?? chipDefaults.padding!;
1176
    // Widget's label style is merged with this below.
1177 1178
    final TextStyle labelStyle = chipTheme.labelStyle
      ?? chipDefaults.labelStyle!;
1179 1180
    final EdgeInsetsGeometry labelPadding = widget.labelPadding
      ?? chipTheme.labelPadding
1181
      ?? chipDefaults.labelPadding
1182
      ?? defaultLabelPadding;
1183 1184 1185
    final IconThemeData? iconTheme = widget.iconTheme
      ?? chipTheme.iconTheme
      ?? chipDefaults.iconTheme;
1186

1187
    final TextStyle effectiveLabelStyle = labelStyle.merge(widget.labelStyle);
1188
    final Color? resolvedLabelColor = MaterialStateProperty.resolveAs<Color?>(effectiveLabelStyle.color, materialStates);
1189
    final TextStyle resolvedLabelStyle = effectiveLabelStyle.copyWith(color: resolvedLabelColor);
1190 1191 1192
    final Widget? avatar = iconTheme != null && hasAvatar
      ? IconTheme(data: iconTheme, child: widget.avatar!)
      : widget.avatar;
1193

1194 1195 1196
    Widget result = Material(
      elevation: isTapping ? pressElevation : elevation,
      shadowColor: widget.selected ? selectedShadowColor : shadowColor,
1197
      surfaceTintColor: surfaceTintColor,
1198
      animationDuration: pressedAnimationDuration,
1199
      shape: resolvedShape,
1200 1201
      clipBehavior: widget.clipBehavior,
      child: InkWell(
1202
        onFocusChange: updateMaterialState(MaterialState.focused),
1203 1204 1205 1206 1207 1208
        focusNode: widget.focusNode,
        autofocus: widget.autofocus,
        canRequestFocus: widget.isEnabled,
        onTap: canTap ? _handleTap : null,
        onTapDown: canTap ? _handleTapDown : null,
        onTapCancel: canTap ? _handleTapCancel : null,
1209
        onHover: canTap ? updateMaterialState(MaterialState.hovered) : null,
1210
        customBorder: resolvedShape,
1211 1212
        child: AnimatedBuilder(
          animation: Listenable.merge(<Listenable>[selectController, enableController]),
1213
          builder: (BuildContext context, Widget? child) {
1214
            return Ink(
1215
              decoration: ShapeDecoration(
1216
                shape: resolvedShape,
1217
                color: _getBackgroundColor(theme, chipTheme, chipDefaults),
1218
              ),
1219 1220 1221 1222
              child: child,
            );
          },
          child: _wrapWithTooltip(
1223 1224 1225
            tooltip: widget.tooltip,
            enabled: widget.onPressed != null || widget.onSelected != null,
            child: _ChipRenderWidget(
1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237
              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,
1238
                  child: avatar,
1239
                ),
1240 1241 1242
                deleteIcon: AnimatedSwitcher(
                  duration: _kDrawerDuration,
                  switchInCurve: Curves.fastOutSlowIn,
1243
                  child: _buildDeleteIcon(context, theme, chipTheme, chipDefaults),
1244
                ),
1245 1246
                brightness: brightness,
                padding: padding.resolve(textDirection),
1247
                visualDensity: widget.visualDensity ?? theme.visualDensity,
1248
                labelPadding: labelPadding.resolve(textDirection),
1249 1250 1251 1252
                showAvatar: hasAvatar,
                showCheckmark: showCheckmark,
                checkmarkColor: checkmarkColor,
                canTapBody: canTap,
1253
              ),
1254 1255 1256 1257 1258 1259 1260
              value: widget.selected,
              checkmarkAnimation: checkmarkAnimation,
              enableAnimation: enableAnimation,
              avatarDrawerAnimation: avatarDrawerAnimation,
              deleteDrawerAnimation: deleteDrawerAnimation,
              isEnabled: widget.isEnabled,
              avatarBorder: widget.avatarBorder,
1261 1262
            ),
          ),
1263 1264 1265
        ),
      ),
    );
1266

1267
    final BoxConstraints constraints;
1268
    final Offset densityAdjustment = (widget.visualDensity ?? theme.visualDensity).baseSizeAdjustment;
1269 1270
    switch (widget.materialTapTargetSize ?? theme.materialTapTargetSize) {
      case MaterialTapTargetSize.padded:
1271 1272 1273 1274
        constraints = BoxConstraints(
          minWidth: kMinInteractiveDimension + densityAdjustment.dx,
          minHeight: kMinInteractiveDimension + densityAdjustment.dy,
        );
1275 1276 1277 1278 1279
      case MaterialTapTargetSize.shrinkWrap:
        constraints = const BoxConstraints();
    }
    result = _ChipRedirectingHitDetectionWidget(
      constraints: constraints,
1280
      child: Center(
1281 1282
        widthFactor: 1.0,
        heightFactor: 1.0,
1283
        child: result,
1284 1285
      ),
    );
1286
    return Semantics(
1287
      button: widget.tapEnabled,
1288 1289
      container: true,
      selected: widget.selected,
1290
      enabled: widget.tapEnabled ? canTap : null,
1291 1292 1293 1294 1295
      child: result,
    );
  }
}

1296
/// Redirects the [buttonRect.dy] passed to [RenderBox.hitTest] to the vertical
1297 1298 1299 1300 1301 1302
/// 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({
1303
    super.child,
1304
    required this.constraints,
1305
  });
1306 1307 1308 1309 1310

  final BoxConstraints constraints;

  @override
  RenderObject createRenderObject(BuildContext context) {
1311
    return _RenderChipRedirectingHitDetection(constraints);
1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323
  }

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

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

  @override
1324
  bool hitTest(BoxHitTestResult result, { required Offset position }) {
1325
    if (!size.contains(position)) {
1326
      return false;
1327
    }
1328 1329 1330
    // 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.
1331 1332 1333 1334
    final Offset offset = Offset(position.dx, size.height / 2);
    return result.addWithRawTransform(
      transform: MatrixUtils.forceToPoint(offset),
      position: position,
1335
      hitTest: (BoxHitTestResult result, Offset position) {
1336
        assert(position == offset);
1337
        return child!.hitTest(result, position: offset);
1338 1339
      },
    );
1340 1341 1342
  }
}

1343
class _ChipRenderWidget extends RenderObjectWidget with SlottedMultiChildRenderObjectWidgetMixin<_ChipSlot> {
1344
  const _ChipRenderWidget({
1345
    required this.theme,
1346 1347
    this.value,
    this.isEnabled,
1348 1349 1350 1351
    required this.checkmarkAnimation,
    required this.avatarDrawerAnimation,
    required this.deleteDrawerAnimation,
    required this.enableAnimation,
1352
    this.avatarBorder,
1353
  });
1354 1355

  final _ChipRenderTheme theme;
1356 1357
  final bool? value;
  final bool? isEnabled;
1358 1359 1360 1361
  final Animation<double> checkmarkAnimation;
  final Animation<double> avatarDrawerAnimation;
  final Animation<double> deleteDrawerAnimation;
  final Animation<double> enableAnimation;
1362
  final ShapeBorder? avatarBorder;
1363 1364

  @override
1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377
  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;
    }
  }
1378 1379 1380 1381 1382 1383

  @override
  void updateRenderObject(BuildContext context, _RenderChip renderObject) {
    renderObject
      ..theme = theme
      ..textDirection = Directionality.of(context)
1384 1385 1386 1387 1388
      ..value = value
      ..isEnabled = isEnabled
      ..checkmarkAnimation = checkmarkAnimation
      ..avatarDrawerAnimation = avatarDrawerAnimation
      ..deleteDrawerAnimation = deleteDrawerAnimation
1389 1390
      ..enableAnimation = enableAnimation
      ..avatarBorder = avatarBorder;
1391 1392 1393
  }

  @override
1394
  SlottedContainerRenderObjectMixin<_ChipSlot> createRenderObject(BuildContext context) {
1395
    return _RenderChip(
1396
      theme: theme,
1397
      textDirection: Directionality.of(context),
1398 1399 1400 1401 1402 1403
      value: value,
      isEnabled: isEnabled,
      checkmarkAnimation: checkmarkAnimation,
      avatarDrawerAnimation: avatarDrawerAnimation,
      deleteDrawerAnimation: deleteDrawerAnimation,
      enableAnimation: enableAnimation,
1404
      avatarBorder: avatarBorder,
1405 1406 1407 1408 1409 1410 1411 1412 1413 1414
    );
  }
}

enum _ChipSlot {
  label,
  avatar,
  deleteIcon,
}

1415
@immutable
1416 1417
class _ChipRenderTheme {
  const _ChipRenderTheme({
1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428
    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,
1429 1430 1431 1432 1433
  });

  final Widget avatar;
  final Widget label;
  final Widget deleteIcon;
1434
  final Brightness brightness;
1435
  final EdgeInsets padding;
1436
  final VisualDensity visualDensity;
1437
  final EdgeInsets labelPadding;
1438 1439
  final bool showAvatar;
  final bool showCheckmark;
1440
  final Color? checkmarkColor;
1441
  final bool canTapBody;
1442 1443

  @override
1444
  bool operator ==(Object other) {
1445 1446 1447 1448 1449 1450
    if (identical(this, other)) {
      return true;
    }
    if (other.runtimeType != runtimeType) {
      return false;
    }
1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461
    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;
1462 1463 1464
  }

  @override
1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476
  int get hashCode => Object.hash(
    avatar,
    label,
    deleteIcon,
    brightness,
    padding,
    labelPadding,
    showAvatar,
    showCheckmark,
    checkmarkColor,
    canTapBody,
  );
1477 1478
}

1479
class _RenderChip extends RenderBox with SlottedContainerRenderObjectMixin<_ChipSlot> {
1480
  _RenderChip({
1481 1482
    required _ChipRenderTheme theme,
    required TextDirection textDirection,
1483 1484
    this.value,
    this.isEnabled,
1485 1486 1487 1488
    required this.checkmarkAnimation,
    required this.avatarDrawerAnimation,
    required this.deleteDrawerAnimation,
    required this.enableAnimation,
1489
    this.avatarBorder,
1490
  }) : _theme = theme,
1491
       _textDirection = textDirection {
1492 1493 1494 1495
    checkmarkAnimation.addListener(markNeedsPaint);
    avatarDrawerAnimation.addListener(markNeedsLayout);
    deleteDrawerAnimation.addListener(markNeedsLayout);
    enableAnimation.addListener(markNeedsPaint);
1496 1497
  }

1498 1499 1500 1501
  bool? value;
  bool? isEnabled;
  late Rect _deleteButtonRect;
  late Rect _pressRect;
1502 1503 1504 1505
  Animation<double> checkmarkAnimation;
  Animation<double> avatarDrawerAnimation;
  Animation<double> deleteDrawerAnimation;
  Animation<double> enableAnimation;
1506
  ShapeBorder? avatarBorder;
1507

1508 1509 1510
  RenderBox? get avatar => childForSlot(_ChipSlot.avatar);
  RenderBox? get deleteIcon => childForSlot(_ChipSlot.deleteIcon);
  RenderBox? get label => childForSlot(_ChipSlot.label);
1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521

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

1522 1523 1524
  TextDirection? get textDirection => _textDirection;
  TextDirection? _textDirection;
  set textDirection(TextDirection? value) {
1525 1526 1527 1528 1529 1530 1531 1532
    if (_textDirection == value) {
      return;
    }
    _textDirection = value;
    markNeedsLayout();
  }

  // The returned list is ordered for hit testing.
1533
  @override
1534 1535 1536 1537 1538 1539 1540 1541 1542
  Iterable<RenderBox> get children {
    return <RenderBox>[
      if (avatar != null)
        avatar!,
      if (label != null)
        label!,
      if (deleteIcon != null)
        deleteIcon!,
    ];
1543 1544
  }

1545
  bool get isDrawingCheckmark => theme.showCheckmark && !checkmarkAnimation.isDismissed;
1546
  bool get deleteIconShowing => !deleteDrawerAnimation.isDismissed;
1547 1548 1549 1550

  @override
  bool get sizedByParent => false;

1551
  static double _minWidth(RenderBox? box, double height) {
1552 1553 1554
    return box == null ? 0.0 : box.getMinIntrinsicWidth(height);
  }

1555
  static double _maxWidth(RenderBox? box, double height) {
1556 1557 1558
    return box == null ? 0.0 : box.getMaxIntrinsicWidth(height);
  }

1559
  static double _minHeight(RenderBox? box, double width) {
1560
    return box == null ? 0.0 : box.getMinIntrinsicHeight(width);
1561 1562
  }

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

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

1567
  static BoxParentData _boxParentData(RenderBox box) => box.parentData! as BoxParentData;
1568 1569 1570 1571 1572 1573

  @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.
1574
    final double overallPadding = theme.padding.horizontal +
1575
        theme.labelPadding.horizontal;
1576 1577 1578 1579
    return overallPadding +
        _minWidth(avatar, height) +
        _minWidth(label, height) +
        _minWidth(deleteIcon, height);
1580 1581 1582 1583
  }

  @override
  double computeMaxIntrinsicWidth(double height) {
1584
    final double overallPadding = theme.padding.horizontal +
1585
        theme.labelPadding.horizontal;
1586 1587 1588 1589
    return overallPadding +
        _maxWidth(avatar, height) +
        _maxWidth(label, height) +
        _maxWidth(deleteIcon, height);
1590 1591 1592 1593
  }

  @override
  double computeMinIntrinsicHeight(double width) {
1594 1595 1596 1597
    return math.max(
      _kChipHeight,
      theme.padding.vertical + theme.labelPadding.vertical + _minHeight(label, width),
    );
1598 1599 1600 1601 1602 1603
  }

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

  @override
1604
  double? computeDistanceToActualBaseline(TextBaseline baseline) {
1605
    // The baseline of this widget is the baseline of the label.
1606
    return label!.getDistanceToActualBaseline(baseline);
1607 1608
  }

1609
  Size _layoutLabel(BoxConstraints contentConstraints, double iconSizes, Size size, Size rawSize, [ChildLayouter layoutChild = ChildLayoutHelper.layoutChild]) {
1610 1611
    // 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.
1612
    if (contentConstraints.maxWidth.isFinite) {
1613 1614
      final double maxWidth = math.max(
        0.0,
1615
        contentConstraints.maxWidth
1616 1617 1618 1619
        - iconSizes
        - theme.labelPadding.horizontal
        - theme.padding.horizontal,
      );
1620 1621 1622
      final Size updatedSize = layoutChild(
        label!,
        BoxConstraints(
1623
          maxWidth: maxWidth,
1624 1625 1626
          minHeight: rawSize.height,
          maxHeight: size.height,
        ),
1627
      );
1628 1629 1630 1631

      return Size(
        updatedSize.width + theme.labelPadding.horizontal,
        updatedSize.height + theme.labelPadding.vertical,
1632
      );
1633
    }
1634

1635 1636
    final Size updatedSize = layoutChild(
      label!,
1637 1638 1639 1640 1641 1642 1643
      BoxConstraints(
        minHeight: rawSize.height,
        maxHeight: size.height,
        maxWidth: size.width,
      ),
    );

1644
    return Size(
1645 1646
      updatedSize.width + theme.labelPadding.horizontal,
      updatedSize.height + theme.labelPadding.vertical,
1647
    );
1648 1649
  }

1650
  Size _layoutAvatar(BoxConstraints contentConstraints, double contentSize, [ChildLayouter layoutChild = ChildLayoutHelper.layoutChild]) {
1651
    final double requestedSize = math.max(0.0, contentSize);
1652
    final BoxConstraints avatarConstraints = BoxConstraints.tightFor(
1653 1654 1655
      width: requestedSize,
      height: requestedSize,
    );
1656
    final Size avatarBoxSize = layoutChild(avatar!, avatarConstraints);
1657
    if (!theme.showCheckmark && !theme.showAvatar) {
1658
      return Size(0.0, contentSize);
1659
    }
1660 1661
    double avatarWidth = 0.0;
    double avatarHeight = 0.0;
1662 1663 1664 1665
    if (theme.showAvatar) {
      avatarWidth += avatarDrawerAnimation.value * avatarBoxSize.width;
    } else {
      avatarWidth += avatarDrawerAnimation.value * contentSize;
1666
    }
1667
    avatarHeight += avatarBoxSize.height;
1668
    return Size(avatarWidth, avatarHeight);
1669
  }
1670

1671
  Size _layoutDeleteIcon(BoxConstraints contentConstraints, double contentSize, [ChildLayouter layoutChild = ChildLayoutHelper.layoutChild]) {
1672
    final double requestedSize = math.max(0.0, contentSize);
1673
    final BoxConstraints deleteIconConstraints = BoxConstraints.tightFor(
1674 1675 1676
      width: requestedSize,
      height: requestedSize,
    );
1677
    final Size boxSize = layoutChild(deleteIcon!, deleteIconConstraints);
1678
    if (!deleteIconShowing) {
1679
      return Size(0.0, contentSize);
1680
    }
1681 1682
    double deleteIconWidth = 0.0;
    double deleteIconHeight = 0.0;
1683 1684
    deleteIconWidth += deleteDrawerAnimation.value * boxSize.width;
    deleteIconHeight += boxSize.height;
1685
    return Size(deleteIconWidth, deleteIconHeight);
1686
  }
1687

1688
  @override
1689
  bool hitTest(BoxHitTestResult result, { required Offset position }) {
1690
    if (!size.contains(position)) {
1691 1692
      return false;
    }
1693 1694
    final bool hitIsOnDeleteIcon = deleteIcon != null && _hitIsOnDeleteIcon(
      padding: theme.padding,
1695 1696
      tapPosition: position,
      chipSize: size,
1697
      deleteButtonSize: deleteIcon!.size,
1698
      textDirection: textDirection!,
1699
    );
1700
    final RenderBox? hitTestChild = hitIsOnDeleteIcon
1701 1702 1703
        ? (deleteIcon ?? label ?? avatar)
        : (label ?? avatar);

1704 1705 1706 1707 1708
    if (hitTestChild != null) {
      final Offset center = hitTestChild.size.center(Offset.zero);
      return result.addWithRawTransform(
        transform: MatrixUtils.forceToPoint(center),
        position: position,
1709
        hitTest: (BoxHitTestResult result, Offset position) {
1710 1711 1712 1713 1714 1715
          assert(position == center);
          return hitTestChild.hitTest(result, position: center);
        },
      );
    }
    return false;
1716 1717
  }

1718
  @override
1719 1720 1721 1722 1723
  Size computeDryLayout(BoxConstraints constraints) {
    return _computeSizes(constraints, ChildLayoutHelper.dryLayoutChild).size;
  }

  _ChipSizes _computeSizes(BoxConstraints constraints, ChildLayouter layoutChild) {
1724 1725
    final BoxConstraints contentConstraints = constraints.loosen();
    // Find out the height of the label within the constraints.
1726
    final Offset densityAdjustment = Offset(0.0, theme.visualDensity.baseSizeAdjustment.dy / 2.0);
1727
    final Size rawLabelSize = layoutChild(label!, contentConstraints);
1728 1729
    final double contentSize = math.max(
      _kChipHeight - theme.padding.vertical + theme.labelPadding.vertical,
1730 1731 1732 1733 1734 1735 1736 1737 1738 1739
      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,
1740 1741 1742 1743
    );

    // This is the overall size of the content: it doesn't include
    // theme.padding, that is added in at the end.
1744
    final Size overallSize = Size(
1745 1746
      avatarSize.width + labelSize.width + deleteIconSize.width,
      contentSize,
1747
    ) + densityAdjustment;
1748 1749 1750 1751
    final Size paddedSize = Size(
      overallSize.width + theme.padding.horizontal,
      overallSize.height + theme.padding.vertical,
    );
1752

1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766
    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);
1767 1768

    // Now we have all of the dimensions. Place the children where they belong.
1769 1770

    const double left = 0.0;
1771
    final double right = sizes.overall.width;
1772 1773

    Offset centerLayout(Size boxSize, double x) {
1774
      assert(sizes.content >= boxSize.height);
1775
      switch (textDirection!) {
1776
        case TextDirection.rtl:
1777
          return Offset(x - boxSize.width, (sizes.content - boxSize.height + sizes.densityAdjustment.dy) / 2.0);
1778
        case TextDirection.ltr:
1779
          return Offset(x, (sizes.content - boxSize.height + sizes.densityAdjustment.dy) / 2.0);
1780 1781
      }
    }
1782

1783 1784 1785 1786 1787 1788
    // 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;
1789
    switch (textDirection!) {
1790
      case TextDirection.rtl:
1791 1792
        double start = right;
        if (theme.showCheckmark || theme.showAvatar) {
1793 1794
          avatarOffset = centerLayout(sizes.avatar, start);
          start -= sizes.avatar.width;
1795
        }
1796 1797
        labelOffset = centerLayout(sizes.label, start);
        start -= sizes.label.width;
1798
        if (deleteIconShowing) {
1799
          _deleteButtonRect = Rect.fromLTWH(
1800 1801
            0.0,
            0.0,
1802 1803
            sizes.deleteIcon.width + theme.padding.right,
            sizes.overall.height + theme.padding.vertical,
1804
          );
1805
          deleteIconOffset = centerLayout(sizes.deleteIcon, start);
1806
        } else {
1807
          _deleteButtonRect = Rect.zero;
1808
        }
1809
        start -= sizes.deleteIcon.width;
1810
        if (theme.canTapBody) {
1811 1812
          _pressRect = Rect.fromLTWH(
            _deleteButtonRect.width,
1813
            0.0,
1814 1815
            sizes.overall.width - _deleteButtonRect.width + theme.padding.horizontal,
            sizes.overall.height + theme.padding.vertical,
1816
          );
1817
        } else {
1818
          _pressRect = Rect.zero;
1819 1820
        }
      case TextDirection.ltr:
1821 1822
        double start = left;
        if (theme.showCheckmark || theme.showAvatar) {
1823 1824
          avatarOffset = centerLayout(sizes.avatar, start - _boxSize(avatar).width + sizes.avatar.width);
          start += sizes.avatar.width;
1825
        }
1826 1827
        labelOffset = centerLayout(sizes.label, start);
        start += sizes.label.width;
1828
        if (theme.canTapBody) {
1829
          _pressRect = Rect.fromLTWH(
1830 1831
            0.0,
            0.0,
1832 1833
            deleteIconShowing
                ? start + theme.padding.left
1834 1835
                : sizes.overall.width + theme.padding.horizontal,
            sizes.overall.height + theme.padding.vertical,
1836
          );
1837
        } else {
1838
          _pressRect = Rect.zero;
1839
        }
1840
        start -= _boxSize(deleteIcon).width - sizes.deleteIcon.width;
1841
        if (deleteIconShowing) {
1842
          deleteIconOffset = centerLayout(sizes.deleteIcon, start);
1843
          _deleteButtonRect = Rect.fromLTWH(
1844
            start + theme.padding.left,
1845
            0.0,
1846 1847
            sizes.deleteIcon.width + theme.padding.right,
            sizes.overall.height + theme.padding.vertical,
1848
          );
1849
        } else {
1850
          _deleteButtonRect = Rect.zero;
1851 1852
        }
    }
1853 1854
    // Center the label vertically.
    labelOffset = labelOffset +
1855
        Offset(
1856
          0.0,
1857
          ((sizes.label.height - theme.labelPadding.vertical) - _boxSize(label).height) / 2.0,
1858
        );
1859 1860 1861
    _boxParentData(avatar!).offset = theme.padding.topLeft + avatarOffset;
    _boxParentData(label!).offset = theme.padding.topLeft + labelOffset + theme.labelPadding.topLeft;
    _boxParentData(deleteIcon!).offset = theme.padding.topLeft + deleteIconOffset;
1862
    final Size paddedSize = Size(
1863 1864
      sizes.overall.width + theme.padding.horizontal,
      sizes.overall.height + theme.padding.vertical,
1865 1866 1867
    );
    size = constraints.constrain(paddedSize);
    assert(
1868 1869 1870 1871
      size.height == constraints.constrainHeight(paddedSize.height),
      "Constrained height ${size.height} doesn't match expected height "
      '${constraints.constrainWidth(paddedSize.height)}',
    );
1872
    assert(
1873 1874 1875 1876
      size.width == constraints.constrainWidth(paddedSize.width),
      "Constrained width ${size.width} doesn't match expected width "
      '${constraints.constrainWidth(paddedSize.width)}',
    );
1877
  }
1878

1879
  static final ColorTween selectionScrimTween = ColorTween(
1880 1881 1882 1883 1884
    begin: Colors.transparent,
    end: _kSelectScrimColor,
  );

  Color get _disabledColor {
1885
    if (enableAnimation.isCompleted) {
1886 1887
      return Colors.white;
    }
1888
    final ColorTween enableTween;
1889 1890
    switch (theme.brightness) {
      case Brightness.light:
1891
        enableTween = ColorTween(
1892 1893 1894 1895
          begin: Colors.white.withAlpha(_kDisabledAlpha),
          end: Colors.white,
        );
      case Brightness.dark:
1896
        enableTween = ColorTween(
1897 1898 1899 1900
          begin: Colors.black.withAlpha(_kDisabledAlpha),
          end: Colors.black,
        );
    }
1901
    return enableTween.evaluate(enableAnimation)!;
1902 1903
  }

1904
  void _paintCheck(Canvas canvas, Offset origin, double size) {
1905
    Color? paintColor;
1906 1907 1908 1909 1910 1911 1912 1913 1914
    if (theme.checkmarkColor != null) {
      paintColor = theme.checkmarkColor;
    } else {
      switch (theme.brightness) {
        case Brightness.light:
          paintColor = theme.showAvatar ? Colors.white : Colors.black.withAlpha(_kCheckmarkAlpha);
        case Brightness.dark:
          paintColor = theme.showAvatar ? Colors.black : Colors.white.withAlpha(_kCheckmarkAlpha);
      }
1915 1916
    }

1917
    final ColorTween fadeTween = ColorTween(begin: Colors.transparent, end: paintColor);
1918 1919 1920 1921 1922

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

1923
    final Paint paint = Paint()
1924
      ..color = paintColor!
1925
      ..style = PaintingStyle.stroke
1926
      ..strokeWidth = _kCheckmarkStrokeWidth * (avatar != null ? avatar!.size.height / 24.0 : 1.0);
1927 1928 1929 1930 1931 1932 1933 1934 1935 1936
    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.
1937 1938 1939 1940
    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);
1941 1942
    if (t < 0.5) {
      final double strokeT = t * 2.0;
1943
      final Offset drawMid = Offset.lerp(start, mid, strokeT)!;
1944 1945 1946 1947
      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;
1948
      final Offset drawEnd = Offset.lerp(mid, end, strokeT)!;
1949 1950 1951 1952 1953 1954 1955 1956 1957
      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) {
1958 1959
      if (theme.showAvatar) {
        final Rect avatarRect = _boxRect(avatar).shift(offset);
1960
        final Paint darkenPaint = Paint()
1961
          ..color = selectionScrimTween.evaluate(checkmarkAnimation)!
1962
          ..blendMode = BlendMode.srcATop;
1963
        final Path path =  avatarBorder!.getOuterPath(avatarRect);
1964
        context.canvas.drawPath(path, darkenPaint);
1965
      }
1966
      // Need to make the check mark be a little smaller than the avatar.
1967 1968 1969
      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);
1970 1971 1972 1973 1974 1975
      _paintCheck(context.canvas, offset + checkOffset, checkSize);
    }
  }

  void _paintAvatar(PaintingContext context, Offset offset) {
    void paintWithOverlay(PaintingContext context, Offset offset) {
1976
      context.paintChild(avatar!, _boxParentData(avatar!).offset + offset);
1977 1978 1979 1980 1981 1982
      _paintSelectionOverlay(context, offset);
    }

    if (theme.showAvatar == false && avatarDrawerAnimation.isDismissed) {
      return;
    }
1983 1984
    final Color disabledColor = _disabledColor;
    final int disabledColorAlpha = disabledColor.alpha;
1985
    if (needsCompositing) {
1986
      context.pushLayer(OpacityLayer(alpha: disabledColorAlpha), paintWithOverlay, offset);
1987 1988 1989 1990
    } else {
      if (disabledColorAlpha != 0xff) {
        context.canvas.saveLayer(
          _boxRect(avatar).shift(offset).inflate(20.0),
1991
          Paint()..color = disabledColor,
1992 1993 1994 1995 1996 1997 1998 1999 2000
        );
      }
      paintWithOverlay(context, offset);
      if (disabledColorAlpha != 0xff) {
        context.canvas.restore();
      }
    }
  }

2001
  void _paintChild(PaintingContext context, Offset offset, RenderBox? child, bool? isEnabled) {
2002 2003 2004 2005 2006 2007 2008
    if (child == null) {
      return;
    }
    final int disabledColorAlpha = _disabledColor.alpha;
    if (!enableAnimation.isCompleted) {
      if (needsCompositing) {
        context.pushLayer(
2009
          OpacityLayer(alpha: disabledColorAlpha),
2010 2011 2012 2013 2014 2015 2016
          (PaintingContext context, Offset offset) {
            context.paintChild(child, _boxParentData(child).offset + offset);
          },
          offset,
        );
      } else {
        final Rect childRect = _boxRect(child).shift(offset);
2017
        context.canvas.saveLayer(childRect.inflate(20.0), Paint()..color = _disabledColor);
2018
        context.paintChild(child, _boxParentData(child).offset + offset);
2019
        context.canvas.restore();
2020
      }
2021 2022
    } else {
      context.paintChild(child, _boxParentData(child).offset + offset);
2023
    }
2024
  }
2025

2026 2027 2028 2029 2030 2031 2032
  @override
  void paint(PaintingContext context, Offset offset) {
    _paintAvatar(context, offset);
    if (deleteIconShowing) {
      _paintChild(context, offset, deleteIcon, isEnabled);
    }
    _paintChild(context, offset, label, isEnabled);
2033 2034
  }

2035
  // Set this to true to have outlines of the tap targets drawn over
2036
  // the chip. This should never be checked in while set to 'true'.
2037 2038
  static const bool _debugShowTapTargetOutlines = false;

2039 2040
  @override
  void debugPaint(PaintingContext context, Offset offset) {
2041 2042 2043 2044 2045 2046 2047 2048
    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) {
2049
        context.canvas.drawRect(_deleteButtonRect.shift(offset), outlinePaint);
2050 2051
      }
      context.canvas.drawRect(
2052
        _pressRect.shift(offset),
2053 2054 2055 2056
        outlinePaint..color = const Color(0xff008000),
      );
      return true;
    }());
2057 2058 2059
  }

  @override
2060
  bool hitTestSelf(Offset position) => _deleteButtonRect.contains(position) || _pressRect.contains(position);
2061
}
2062

2063 2064 2065 2066 2067 2068 2069 2070 2071 2072 2073 2074 2075 2076 2077 2078 2079 2080 2081
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;
}

2082 2083
class _UnconstrainedInkSplashFactory extends InteractiveInkFeatureFactory {
  const _UnconstrainedInkSplashFactory(this.parentFactory);
2084

2085
  final InteractiveInkFeatureFactory parentFactory;
2086 2087 2088

  @override
  InteractiveInkFeature create({
2089 2090 2091 2092 2093
    required MaterialInkController controller,
    required RenderBox referenceBox,
    required Offset position,
    required Color color,
    required TextDirection textDirection,
2094
    bool containedInkWell = false,
2095 2096 2097 2098 2099
    RectCallback? rectCallback,
    BorderRadius? borderRadius,
    ShapeBorder? customBorder,
    double? radius,
    VoidCallback? onRemoved,
2100
  }) {
2101
    return parentFactory.create(
2102 2103 2104 2105 2106 2107 2108 2109 2110
      controller: controller,
      referenceBox: referenceBox,
      position: position,
      color: color,
      rectCallback: rectCallback,
      borderRadius: borderRadius,
      customBorder: customBorder,
      radius: radius,
      onRemoved: onRemoved,
2111
      textDirection: textDirection,
2112 2113 2114 2115
    );
  }
}

2116 2117
bool _hitIsOnDeleteIcon({
  required EdgeInsetsGeometry padding,
2118 2119
  required Offset tapPosition,
  required Size chipSize,
2120
  required Size deleteButtonSize,
2121
  required TextDirection textDirection,
2122
}) {
2123 2124 2125 2126 2127
  // 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);
2128 2129 2130 2131 2132
  // 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.
2133 2134
  //
  // This isn't affected by materialTapTargetSize because it only applies to the
2135 2136 2137 2138 2139 2140 2141 2142 2143
  // 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),
2144 2145 2146 2147 2148 2149
  );
  switch (textDirection) {
    case TextDirection.ltr:
      return adjustedPosition.dx >= deflatedSize.width - accessibleDeleteButtonWidth;
    case TextDirection.rtl:
      return adjustedPosition.dx <= accessibleDeleteButtonWidth;
2150 2151
  }
}
2152 2153 2154 2155 2156 2157 2158 2159

// BEGIN GENERATED TOKEN PROPERTIES - Chip

// Do not edit by hand. The code between the "BEGIN GENERATED" and
// "END GENERATED" comments are generated from data in the Material
// Design token database by the script:
//   dev/tools/gen_defaults/bin/gen_defaults.dart.

2160
// Token database version: v0_162
2161 2162

class _ChipDefaultsM3 extends ChipThemeData {
2163
  _ChipDefaultsM3(this.context, this.isEnabled)
2164 2165 2166 2167 2168 2169 2170 2171
    : super(
        elevation: 0.0,
        shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(8.0))),
        showCheckmark: true,
      );

  final BuildContext context;
  final bool isEnabled;
2172 2173
  late final ColorScheme _colors = Theme.of(context).colorScheme;
  late final TextTheme _textTheme = Theme.of(context).textTheme;
2174 2175

  @override
2176
  TextStyle? get labelStyle => _textTheme.labelLarge;
2177 2178 2179 2180 2181 2182 2183 2184

  @override
  Color? get backgroundColor => null;

  @override
  Color? get shadowColor => Colors.transparent;

  @override
2185
  Color? get surfaceTintColor => _colors.surfaceTint;
2186 2187 2188 2189 2190 2191 2192 2193 2194 2195 2196 2197 2198 2199 2200

  @override
  Color? get selectedColor => null;

  @override
  Color? get checkmarkColor => null;

  @override
  Color? get disabledColor => null;

  @override
  Color? get deleteIconColor => null;

  @override
  BorderSide? get side => isEnabled
2201 2202
    ? BorderSide(color: _colors.outline)
    : BorderSide(color: _colors.onSurface.withOpacity(0.12));
2203 2204 2205 2206

  @override
  IconThemeData? get iconTheme => IconThemeData(
    color: isEnabled
2207 2208
      ? _colors.primary
      : _colors.onSurface,
2209 2210 2211 2212 2213 2214 2215 2216 2217 2218 2219 2220 2221 2222
    size: 18.0,
  );

  @override
  EdgeInsetsGeometry? get padding => const EdgeInsets.all(8.0);

  /// 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.
  @override
  EdgeInsetsGeometry? get labelPadding => EdgeInsets.lerp(
    const EdgeInsets.symmetric(horizontal: 8.0),
    const EdgeInsets.symmetric(horizontal: 4.0),
2223
    clampDouble(MediaQuery.textScaleFactorOf(context) - 1.0, 0.0, 1.0),
2224 2225 2226 2227
  )!;
}

// END GENERATED TOKEN PROPERTIES - Chip