Repository: flame-engine/flame

Issue:
PaddingComponent
### Problem to solve

Currently insetting (padding) of a child component within another component needs to be done by setting the size of the parent component, and then positioning the child component.

### Proposal

1. Split LayoutComponent into LayoutComponent and LinearLayoutComponent, where the latter subclasses the former. (Meanwhile, RowLayout and ColumnLayout subclass the latter)
2. LayoutComponent owns "shrinkWrap" and possibly other properties
3. PaddingComponent subclasses LayoutComponent
4. If shrinkWrap is true, PaddingLayout shrinks to its smallest size that respects the size of the child.
5. If shrinkWrap is false, PaddingLayout requires an explicit size to be set, and sets the child's size to the maximum allowable size that adheres to the padding constraint.

### More information

_No response_

### Other

- [x] Are you interested in working on a PR for this?


Discussion: I like the idea of a padding component, but maybe this is a bit too complex? I think people would be more familiar with it if it worked more like padding in Flutter. I don't think the shrink wrapping argument is needed here, like if the user wants to do number 5, why wouldn't it just set the size of the child instead?
I'm not attached to item 5; we can drop it. In that case, shrinkWrap won't be an option, and behave as if it were set to `true`, as in item 4.

Fix patch:
diff --git a/examples/lib/stories/experimental/layout_component_example.dart b/examples/lib/stories/experimental/layout_component_example.dart
index 40c028903ac..314e5e0687f 100644
--- a/examples/lib/stories/experimental/layout_component_example.dart
+++ b/examples/lib/stories/experimental/layout_component_example.dart
@@ -88,6 +88,27 @@ layouts to reflect the chosen values.
         }),
       ],
     );
+    final paddingControls = RowComponent(
+      gap: 16,
+      children: [
+        TextComponent(text: 'Padding:'),
+        ...[
+          EdgeInsets.zero,
+          const EdgeInsets.all(16),
+          const EdgeInsets.symmetric(vertical: 8, horizontal: 24),
+        ].map((padding) {
+          return ButtonComponent(
+            button: TextComponent(text: padding.toString()),
+            onPressed: () {
+              rowDemo.children.query<PaddingComponent>().first.padding =
+                  padding;
+              columnDemo.children.query<PaddingComponent>().first.padding =
+                  padding;
+            },
+          );
+        }),
+      ],
+    );
     final rootColumnComponent = ColumnComponent(
       position: Vector2(48, 48),
       gap: 24,
@@ -107,6 +128,7 @@ layouts to reflect the chosen values.
         crossAxisControls,
         gapControls,
         sizeControls,
+        paddingControls,
         rowDemo,
         columnDemo,
       ],
@@ -125,9 +147,12 @@ layouts to reflect the chosen values.
       TextComponent(
         text: 'Perhaps a bit longer text',
       ),
-      CircleComponent(
-        radius: 48,
-        paint: Paint()..color = Colors.blue,
+      PaddingComponent(
+        padding: EdgeInsets.zero,
+        child: CircleComponent(
+          radius: 48,
+          paint: Paint()..color = Colors.blue,
+        ),
       ),
     ];
   }
diff --git a/packages/flame/lib/experimental.dart b/packages/flame/lib/experimental.dart
index 650a6e84450..e37eb276891 100644
--- a/packages/flame/lib/experimental.dart
+++ b/packages/flame/lib/experimental.dart
@@ -18,5 +18,8 @@ export 'src/experimental/geometry/shapes/rectangle.dart' show Rectangle;
 export 'src/experimental/geometry/shapes/rounded_rectangle.dart'
     show RoundedRectangle;
 export 'src/experimental/geometry/shapes/shape.dart' show Shape;
-export 'src/experimental/layout_component.dart' show LayoutComponent, Direction;
+export 'src/experimental/layout_component.dart' show LayoutComponent;
+export 'src/experimental/linear_layout_component.dart'
+    show LinearLayoutComponent, Direction;
+export 'src/experimental/padding_component.dart' show PaddingComponent;
 export 'src/experimental/row_component.dart' show RowComponent;
diff --git a/packages/flame/lib/src/experimental/column_component.dart b/packages/flame/lib/src/experimental/column_component.dart
index a5a0a467257..6991fdcf439 100644
--- a/packages/flame/lib/src/experimental/column_component.dart
+++ b/packages/flame/lib/src/experimental/column_component.dart
@@ -1,4 +1,4 @@
-import 'package:flame/src/experimental/layout_component.dart';
+import 'package:flame/src/experimental/linear_layout_component.dart';
 import 'package:flutter/rendering.dart';
 
 /// ColumnComponent is a layout component that arranges its children in a
@@ -29,7 +29,7 @@ import 'package:flutter/rendering.dart';
 ///   ],
 /// );
 /// ```
-class ColumnComponent extends LayoutComponent {
+class ColumnComponent extends LinearLayoutComponent {
   ColumnComponent({
     super.mainAxisAlignment = MainAxisAlignment.start,
     super.crossAxisAlignment = CrossAxisAlignment.start,
diff --git a/packages/flame/lib/src/experimental/layout_component.dart b/packages/flame/lib/src/experimental/layout_component.dart
index 937def638a9..c0d12c8bfab 100644
--- a/packages/flame/lib/src/experimental/layout_component.dart
+++ b/packages/flame/lib/src/experimental/layout_component.dart
@@ -1,75 +1,14 @@
-import 'package:collection/collection.dart';
 import 'package:flame/components.dart';
-import 'package:flame/experimental.dart';
-import 'package:flame/image_composition.dart';
-import 'package:flutter/rendering.dart';
+import 'package:meta/meta.dart';
 
-enum Direction {
-  horizontal,
-  vertical;
-
-  /// A helper function for returning the index in the various [Vector2]s like
-  /// position and size to get the corresponding axis.
-  int get mainAxisVectorIndex => this == Direction.horizontal ? 0 : 1;
-
-  /// See documentation for [mainAxisVectorIndex].
-  int get crossAxisVectorIndex => this == Direction.horizontal ? 1 : 0;
-
-  /// A helper for returning the main axis for [vector], given this direction.
-  double mainAxis(Vector2 vector) {
-    return vector[mainAxisVectorIndex];
-  }
-
-  /// A helper for returning the cross axis for [vector], given this direction.
-  double crossAxis(Vector2 vector) {
-    return vector[crossAxisVectorIndex];
-  }
-}
-
-/// Superclass for linear layouts.
-/// A re-layout is performed when
-///  - a change in this component's children takes place
-///  - the [gap] parameter is changed
-///  - the [size] parameter is changed
-///  - the [mainAxisAlignment] parameter is changed
-///  - the [crossAxisAlignment] parameter is changed
-///
-/// Property interactions and gotchas
-///  - [gap] is ignored when the [mainAxisAlignment] is set to one of:
-///    - [MainAxisAlignment.spaceAround]
-///    - [MainAxisAlignment.spaceBetween]
-///    - [MainAxisAlignment.spaceEvenly]
-///  - [size] can be set to null to activate shrink-wrap mode.
-///    Instead of top-down sizing and layout, LayoutComponent derives its size
-///    from its children via [inherentSize]. In this mode, certain layout
-///    options become meaningless. The following are the behaviors you should
-///    note:
-///    - [mainAxisAlignment] acts like [MainAxisAlignment.start], regardless
-///      of its original value.
-///    - [crossAxisAlignment] acts like [CrossAxisAlignment.start], only if
-///      its value is [CrossAxisAlignment.stretch].
-///
-/// Notes:
-///  - Currently, [CrossAxisAlignment.baseline] is unsupported, and behaves
-///    exactly like [CrossAxisAlignment.start].
-///  - Because [CrossAxisAlignment.stretch] alters the size of the children,
-///    and there is no uniform interface for getting the inherent sizes of
-///    [PositionComponent]s, using [CrossAxisAlignment.stretch] "permanently"
-///    changes the sizes of the children. Subsequent changes to
-///    [crossAxisAlignment] will work with the new sizes of the children.
 abstract class LayoutComponent extends PositionComponent {
   LayoutComponent({
-    required this.direction,
-    required CrossAxisAlignment crossAxisAlignment,
-    required MainAxisAlignment mainAxisAlignment,
-    required double gap,
     required super.position,
     required Vector2? size,
-    required super.children,
+    super.children,
+    super.anchor,
     super.priority,
-  })  : _crossAxisAlignment = crossAxisAlignment,
-        _mainAxisAlignment = mainAxisAlignment,
-        _gap = gap {
+  }) {
     // use the size setter rather than invoke [layoutChildren] because the
     // latter needs the intent to shrinkwrap pre-set by [_shrinkWrapMode].
     // At the time of construction, [_shrinkWrapMode] only has its default
@@ -77,41 +16,10 @@ abstract class LayoutComponent extends PositionComponent {
     this.size = size;
   }
 
-  factory LayoutComponent.fromDirection(
-    Direction direction, {
-    CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.start,
-    MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
-    double gap = 0.0,
-    Vector2? position,
-    Vector2? size,
-    Iterable<Component> children = const [],
-  }) {
-    switch (direction) {
-      case Direction.horizontal:
-        return RowComponent(
-          crossAxisAlignment: crossAxisAlignment,
-          mainAxisAlignment: mainAxisAlignment,
-          gap: gap,
-          position: position,
-          size: size,
-          children: children,
-        );
-      case Direction.vertical:
-        return ColumnComponent(
-          crossAxisAlignment: crossAxisAlignment,
-          mainAxisAlignment: mainAxisAlignment,
-          gap: gap,
-          position: position,
-          size: size,
-          children: children,
-        );
-    }
-  }
-
   /// This size setter is nullable unlike its superclass, to allow this
   /// [LayoutComponent] to shrink-wrap its children. In other words, it sets
   /// the size to [inherentSize]. This setter also records the intent to
-  /// shrink-wrap via the [_shrinkWrapMode] property, so that [layoutChildren]
+  /// shrink-wrap via the [shrinkWrapMode] property, so that [layoutChildren]
   /// knows whether or not to invoke this setter.
   ///
   /// Internally, this [size] setter should only ever be invoked upon
@@ -122,13 +30,13 @@ abstract class LayoutComponent extends PositionComponent {
   /// should feel free to use this.
   @override
   set size(Vector2? newSize) {
-    final shrinkWrapMode = newSize == null;
-    if (_shrinkWrapMode != shrinkWrapMode) {
+    final newShrinkWrapMode = newSize == null;
+    if (shrinkWrapMode != newShrinkWrapMode) {
       // We only invoke this when [_shrinkWrapMode]'s value is changing.
       // This is so we can avoid accumulation of listeners on the children.
-      _setupSizeListeners(_shrinkWrapMode);
+      _setupSizeListeners(newShrinkWrapMode);
     }
-    _shrinkWrapMode = shrinkWrapMode;
+    shrinkWrapMode = newShrinkWrapMode;
     // we use [super.size] to benefit from the superclass's notifier mechanisms.
     if (newSize == null) {
       super.size = inherentSize;
@@ -137,16 +45,13 @@ abstract class LayoutComponent extends PositionComponent {
     }
     // We might be tempted to use [layoutChildren], but recall that we already
     // have listeners attached to size via [setupSizeListeners].
-    _layoutMainAxis();
-    _layoutCrossAxis();
   }
 
-  final Direction direction;
-
-  bool _shrinkWrapMode = false;
+  @protected
+  bool shrinkWrapMode = false;
 
   /// Attaches or removes size listeners from [positionChildren], depending on
-  /// the mode of operation. [_shrinkWrapMode] is a property accessible to this
+  /// the mode of operation. [shrinkWrapMode] is a property accessible to this
   /// function, so technically we can access the property directly from within
   /// the function, but because this function is very sensitive to the value of
   /// this property, as a safety measure we are making it explicitly a function
@@ -172,50 +77,13 @@ abstract class LayoutComponent extends PositionComponent {
     }
   }
 
-  CrossAxisAlignment _crossAxisAlignment;
-
-  CrossAxisAlignment get crossAxisAlignment {
-    if (_shrinkWrapMode && _crossAxisAlignment == CrossAxisAlignment.stretch) {
-      return CrossAxisAlignment.start;
-    }
-    return _crossAxisAlignment;
-  }
-
-  set crossAxisAlignment(CrossAxisAlignment value) {
-    _crossAxisAlignment = value;
-    layoutChildren();
-  }
-
-  MainAxisAlignment _mainAxisAlignment;
-
-  MainAxisAlignment get mainAxisAlignment {
-    if (_shrinkWrapMode) {
-      return MainAxisAlignment.start;
-    }
-    return _mainAxisAlignment;
-  }
-
-  set mainAxisAlignment(MainAxisAlignment value) {
-    _mainAxisAlignment = value;
-    layoutChildren();
-  }
-
-  double _gap;
-
-  /// gap between components
-  double get gap => _gap;
-  set gap(double gap) {
-    _gap = gap;
-    layoutChildren();
-  }
-
   @override
   void onChildrenChanged(Component child, ChildrenChangeType type) {
     if (child is! PositionComponent) {
       return;
     }
     // setupSizeListeners(), but for a single child
-    if (type == ChildrenChangeType.added && _shrinkWrapMode) {
+    if (type == ChildrenChangeType.added && shrinkWrapMode) {
       child.size.addListener(layoutChildren);
     } else {
       child.size.removeListener(layoutChildren);
@@ -226,147 +94,9 @@ abstract class LayoutComponent extends PositionComponent {
   /// Sets the size of this [LayoutComponent], then lays out the children
   /// along both main and cross axes.
   void layoutChildren() {
-    if (_shrinkWrapMode) {
+    if (shrinkWrapMode) {
       size = null;
     }
-    _layoutMainAxis();
-    _layoutCrossAxis();
-  }
-
-  void _layoutMainAxis() {
-    final mainAxisVectorIndex = direction.mainAxisVectorIndex;
-    final availableSpace = size[mainAxisVectorIndex];
-    final unoccupiedSpace = availableSpace - _mainAxisOccupiedSpace;
-    final freeSpace = unoccupiedSpace - _gapSpace;
-    // Execute gap override. Internally, only does so if applicable.
-    _gapOverride(unoccupiedSpace);
-
-    // If the accessor `[]` operator is implemented for Offset,
-    // can directly work with Offset rather than Vector2.
-    final initialOffsetVector = Vector2.zero();
-    initialOffsetVector[mainAxisVectorIndex] = switch (mainAxisAlignment) {
-      MainAxisAlignment.spaceEvenly => gap,
-      MainAxisAlignment.spaceAround => gap / 2,
-      MainAxisAlignment.spaceBetween => 0,
-      MainAxisAlignment.start => 0,
-      MainAxisAlignment.end => availableSpace,
-      MainAxisAlignment.center => freeSpace / 2,
-    };
-    _layoutMainAxisImpl(
-      components: positionChildren,
-      initialOffset: initialOffsetVector.toOffset(),
-      reverse: mainAxisAlignment == MainAxisAlignment.end,
-    );
-  }
-
-  /// In cases where the [mainAxisAlignment] is a value where gap overrides
-  /// make sense i.e. spaceEvenly, spaceAround, spaceBetween, override the
-  /// value of gap based on the expected behavior of those alignments.
-  /// Otherwise, no-op.
-  void _gapOverride(double freeSpace) {
-    final gapOverridingAlignments = {
-      MainAxisAlignment.spaceEvenly,
-      MainAxisAlignment.spaceAround,
-      MainAxisAlignment.spaceBetween,
-    };
-    if (!gapOverridingAlignments.contains(mainAxisAlignment)) {
-      // mainAxisAlignment is not an alignment that can override gaps, so no-op.
-      return;
-    }
-
-    final numberOfGaps = switch (mainAxisAlignment) {
-      MainAxisAlignment.spaceEvenly => children.length + 1,
-      MainAxisAlignment.spaceAround => children.length,
-      MainAxisAlignment.spaceBetween => children.length - 1,
-      _ =>
-        // this should never happen because of
-        // the guard at the start of this method.
-        throw Exception('Unexpected call to _gapOverride'),
-    };
-    _gap = freeSpace / numberOfGaps;
-  }
-
-  void _layoutMainAxisImpl({
-    required List<PositionComponent> components,
-
-    /// - Zero when [MainAxisAlignment.start].
-    /// - [LayoutComponent.size] when [MainAxisAlignment.end] and
-    /// [MainAxisAlignment.spaceBetween]
-    /// - Half of free space when [MainAxisAlignment.center]
-    /// - [LayoutComponent.gap] when [MainAxisAlignment.spaceEvenly]
-    /// - Half of [LayoutComponent.gap] when [MainAxisAlignment.spaceAround]
-    Offset initialOffset = Offset.zero,
-
-    /// true if laying out from the end (bottom/right)
-    bool reverse = false,
-  }) {
-    final mainAxisVectorIndex = direction.mainAxisVectorIndex;
-    final componentList = reverse ? components.reversed : components;
-    for (final (index, component) in componentList.indexed) {
-      final previousChild =
-          index > 0 ? componentList.elementAt(index - 1) : null;
-      final reference = previousChild == null
-          // Essentially the same as start, but gap is set.
-          ? initialOffset.toVector2()
-          // The "end" at any loop other than the first is the previous
-          // child's top left position minus the gap.
-          : previousChild.topLeftPosition +
-              (reverse
-                  ? -Vector2.all(gap)
-                  : previousChild.size + Vector2.all(gap));
-      final positionOffset = reverse ? component.size : Vector2.zero();
-      final newPosition = Vector2.zero();
-      newPosition[mainAxisVectorIndex] =
-          (reference - positionOffset)[mainAxisVectorIndex];
-      component.topLeftPosition.setFrom(newPosition);
-    }
-  }
-
-  void _layoutCrossAxis() {
-    final crossAxisVectorIndex = direction.crossAxisVectorIndex;
-    final positionChildren = this.positionChildren;
-    final newPosition = Vector2.zero();
-    final newSize = Vector2.zero();
-    // There is no need to track index because cross axis positioning is
-    // not influenced by sibling components.
-    for (final component in positionChildren) {
-      newPosition.setFrom(component.topLeftPosition);
-      final crossAxisLength = size[crossAxisVectorIndex];
-      final componentCrossAxisLength = component.size[crossAxisVectorIndex];
-      newPosition[crossAxisVectorIndex] = switch (crossAxisAlignment) {
-        CrossAxisAlignment.start => 0,
-        CrossAxisAlignment.end => crossAxisLength - componentCrossAxisLength,
-        CrossAxisAlignment.center =>
-          (crossAxisLength - componentCrossAxisLength) / 2,
-        CrossAxisAlignment.stretch => 0,
-        CrossAxisAlignment.baseline => 0,
-      };
-      component.topLeftPosition.setFrom(newPosition);
-
-      // Stretch is the only CrossAxisAlignment that involves resizing
-      if (crossAxisAlignment == CrossAxisAlignment.stretch) {
-        newSize.setFrom(component.size);
-        newSize[crossAxisVectorIndex] = size[crossAxisVectorIndex];
-        // Don't use setFrom because children might have their own resizing
-        // logic, such as a nested LayoutComponent.
-        component.size = newSize;
-      }
-    }
-  }
-
-  /// The total space along the main axis occupied by the [positionChildren]
-  /// without the [gap]s. This is so named because we expect to
-  /// implement crossAxisOccupiedSpace for shrink wrapping.
-  double get _mainAxisOccupiedSpace {
-    return positionChildren
-        .map((c) => c.size[direction.mainAxisVectorIndex])
-        .sum;
-  }
-
-  /// The total space along the main axis taken up by the gaps between
-  /// the [positionChildren].
-  double get _gapSpace {
-    return gap * (positionChildren.length - 1);
   }
 
   /// A helper property to get only the [PositionComponent]s
@@ -374,34 +104,5 @@ abstract class LayoutComponent extends PositionComponent {
   List<PositionComponent> get positionChildren =>
       children.whereType<PositionComponent>().toList();
 
-  /// Any positioning done in [layoutChildren] should not affect the
-  /// [inherentSize]. This is because all [crossAxisAlignment] transformations
-  /// fall within the largestCrossAxisLength, while [mainAxisAlignment] is
-  /// entirely ignored in all cases where [inherentSize] is needed.
-  /// This means that [inherentSize] should be used either *before* or at the
-  /// start of [layoutChildren].
-  Vector2 get inherentSize {
-    // Used at multiple points, cache to avoid recalculating each invocation
-    final positionChildren = this.positionChildren;
-    final crossAxisVectorIndex = direction.crossAxisVectorIndex;
-    final mainAxisVectorIndex = direction.mainAxisVectorIndex;
-    if (positionChildren.isEmpty) {
-      return Vector2.zero();
-    }
-    final largestCrossAxisLength = positionChildren
-        .map((component) => component.size[crossAxisVectorIndex])
-        .max;
-    // This is tricky because it depends on the mainAxisAlignment.
-    // This should only apply when mainAxisAlignment is start, center, or end.
-    // spaceAround, spaceBetween, and spaceEvenly requires the size as a
-    // constraint.
-    final cumulativeMainAxisLength = ((positionChildren.length - 1) * gap) +
-        positionChildren
-            .map((component) => component.size[mainAxisVectorIndex])
-            .sum;
-    final out = Vector2.zero();
-    out[mainAxisVectorIndex] = cumulativeMainAxisLength;
-    out[crossAxisVectorIndex] = largestCrossAxisLength;
-    return out;
-  }
+  Vector2 get inherentSize;
 }
diff --git a/packages/flame/lib/src/experimental/linear_layout_component.dart b/packages/flame/lib/src/experimental/linear_layout_component.dart
new file mode 100644
index 00000000000..c8e1d507f66
--- /dev/null
+++ b/packages/flame/lib/src/experimental/linear_layout_component.dart
@@ -0,0 +1,337 @@
+import 'package:collection/collection.dart';
+import 'package:flame/components.dart';
+import 'package:flame/experimental.dart';
+import 'package:flame/image_composition.dart';
+import 'package:flutter/rendering.dart';
+
+enum Direction {
+  horizontal,
+  vertical;
+
+  /// A helper function for returning the index in the various [Vector2]s like
+  /// position and size to get the corresponding axis.
+  int get mainAxisVectorIndex => this == Direction.horizontal ? 0 : 1;
+
+  /// See documentation for [mainAxisVectorIndex].
+  int get crossAxisVectorIndex => this == Direction.horizontal ? 1 : 0;
+
+  /// A helper for returning the main axis for [vector], given this direction.
+  double mainAxis(Vector2 vector) {
+    return vector[mainAxisVectorIndex];
+  }
+
+  /// A helper for returning the cross axis for [vector], given this direction.
+  double crossAxis(Vector2 vector) {
+    return vector[crossAxisVectorIndex];
+  }
+}
+
+/// Superclass for linear layouts.
+/// A re-layout is performed when
+///  - a change in this component's children takes place
+///  - the [gap] parameter is changed
+///  - the [size] parameter is changed
+///  - the [mainAxisAlignment] parameter is changed
+///  - the [crossAxisAlignment] parameter is changed
+///
+/// Property interactions and gotchas
+///  - [gap] is ignored when the [mainAxisAlignment] is set to one of:
+///    - [MainAxisAlignment.spaceAround]
+///    - [MainAxisAlignment.spaceBetween]
+///    - [MainAxisAlignment.spaceEvenly]
+///  - [size] can be set to null to activate shrink-wrap mode.
+///    Instead of top-down sizing and layout, LayoutComponent derives its size
+///    from its children via [inherentSize]. In this mode, certain layout
+///    options become meaningless. The following are the behaviors you should
+///    note:
+///    - [mainAxisAlignment] acts like [MainAxisAlignment.start], regardless
+///      of its original value.
+///    - [crossAxisAlignment] acts like [CrossAxisAlignment.start], only if
+///      its value is [CrossAxisAlignment.stretch].
+///
+/// Notes:
+///  - Currently, [CrossAxisAlignment.baseline] is unsupported, and behaves
+///    exactly like [CrossAxisAlignment.start].
+///  - Because [CrossAxisAlignment.stretch] alters the size of the children,
+///    and there is no uniform interface for getting the inherent sizes of
+///    [PositionComponent]s, using [CrossAxisAlignment.stretch] "permanently"
+///    changes the sizes of the children. Subsequent changes to
+///    [crossAxisAlignment] will work with the new sizes of the children.
+abstract class LinearLayoutComponent extends LayoutComponent {
+  LinearLayoutComponent({
+    required this.direction,
+    required CrossAxisAlignment crossAxisAlignment,
+    required MainAxisAlignment mainAxisAlignment,
+    required double gap,
+    required super.position,
+    required super.children,
+    required super.size,
+    super.priority,
+  })  : _crossAxisAlignment = crossAxisAlignment,
+        _mainAxisAlignment = mainAxisAlignment,
+        _gap = gap;
+
+  factory LinearLayoutComponent.fromDirection(
+    Direction direction, {
+    CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.start,
+    MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
+    double gap = 0.0,
+    Vector2? position,
+    Vector2? size,
+    Iterable<Component> children = const [],
+  }) {
+    switch (direction) {
+      case Direction.horizontal:
+        return RowComponent(
+          crossAxisAlignment: crossAxisAlignment,
+          mainAxisAlignment: mainAxisAlignment,
+          gap: gap,
+          position: position,
+          size: size,
+          children: children,
+        );
+      case Direction.vertical:
+        return ColumnComponent(
+          crossAxisAlignment: crossAxisAlignment,
+          mainAxisAlignment: mainAxisAlignment,
+          gap: gap,
+          position: position,
+          size: size,
+          children: children,
+        );
+    }
+  }
+
+  final Direction direction;
+
+  CrossAxisAlignment _crossAxisAlignment;
+
+  CrossAxisAlignment get crossAxisAlignment {
+    if (shrinkWrapMode && _crossAxisAlignment == CrossAxisAlignment.stretch) {
+      return CrossAxisAlignment.start;
+    }
+    return _crossAxisAlignment;
+  }
+
+  set crossAxisAlignment(CrossAxisAlignment value) {
+    _crossAxisAlignment = value;
+    layoutChildren();
+  }
+
+  MainAxisAlignment _mainAxisAlignment;
+
+  MainAxisAlignment get mainAxisAlignment {
+    if (shrinkWrapMode) {
+      return MainAxisAlignment.start;
+    }
+    return _mainAxisAlignment;
+  }
+
+  set mainAxisAlignment(MainAxisAlignment value) {
+    _mainAxisAlignment = value;
+    layoutChildren();
+  }
+
+  double _gap;
+
+  /// gap between components
+  double get gap => _gap;
+  set gap(double gap) {
+    _gap = gap;
+    layoutChildren();
+  }
+
+  @override
+  void onChildrenChanged(Component child, ChildrenChangeType type) {
+    if (child is! PositionComponent) {
+      return;
+    }
+    // setupSizeListeners(), but for a single child
+    if (type == ChildrenChangeType.added && shrinkWrapMode) {
+      child.size.addListener(layoutChildren);
+    } else {
+      child.size.removeListener(layoutChildren);
+    }
+    layoutChildren();
+  }
+
+  /// Sets the size of this [LinearLayoutComponent], then lays out the children
+  /// along both main and cross axes.
+  @override
+  void layoutChildren() {
+    if (shrinkWrapMode) {
+      size = null;
+    }
+    _layoutMainAxis();
+    _layoutCrossAxis();
+  }
+
+  void _layoutMainAxis() {
+    final mainAxisVectorIndex = direction.mainAxisVectorIndex;
+    final availableSpace = size[mainAxisVectorIndex];
+    final unoccupiedSpace = availableSpace - _mainAxisOccupiedSpace;
+    final freeSpace = unoccupiedSpace - _gapSpace;
+    // Execute gap override. Internally, only does so if applicable.
+    _gapOverride(unoccupiedSpace);
+
+    // If the accessor `[]` operator is implemented for Offset,
+    // can directly work with Offset rather than Vector2.
+    final initialOffsetVector = Vector2.zero();
+    initialOffsetVector[mainAxisVectorIndex] = switch (mainAxisAlignment) {
+      MainAxisAlignment.spaceEvenly => gap,
+      MainAxisAlignment.spaceAround => gap / 2,
+      MainAxisAlignment.spaceBetween => 0,
+      MainAxisAlignment.start => 0,
+      MainAxisAlignment.end => availableSpace,
+      MainAxisAlignment.center => freeSpace / 2,
+    };
+    _layoutMainAxisImpl(
+      components: positionChildren,
+      initialOffset: initialOffsetVector.toOffset(),
+      reverse: mainAxisAlignment == MainAxisAlignment.end,
+    );
+  }
+
+  /// In cases where the [mainAxisAlignment] is a value where gap overrides
+  /// make sense i.e. spaceEvenly, spaceAround, spaceBetween, override the
+  /// value of gap based on the expected behavior of those alignments.
+  /// Otherwise, no-op.
+  void _gapOverride(double freeSpace) {
+    final gapOverridingAlignments = {
+      MainAxisAlignment.spaceEvenly,
+      MainAxisAlignment.spaceAround,
+      MainAxisAlignment.spaceBetween,
+    };
+    if (!gapOverridingAlignments.contains(mainAxisAlignment)) {
+      // mainAxisAlignment is not an alignment that can override gaps, so no-op.
+      return;
+    }
+
+    final numberOfGaps = switch (mainAxisAlignment) {
+      MainAxisAlignment.spaceEvenly => children.length + 1,
+      MainAxisAlignment.spaceAround => children.length,
+      MainAxisAlignment.spaceBetween => children.length - 1,
+      _ =>
+        // this should never happen because of
+        // the guard at the start of this method.
+        throw Exception('Unexpected call to _gapOverride'),
+    };
+    _gap = freeSpace / numberOfGaps;
+  }
+
+  void _layoutMainAxisImpl({
+    required List<PositionComponent> components,
+
+    /// - Zero when [MainAxisAlignment.start].
+    /// - [LinearLayoutComponent.size] when [MainAxisAlignment.end] and
+    ///   [MainAxisAlignment.spaceBetween]
+    /// - Half of free space when [MainAxisAlignment.center]
+    /// - [LinearLayoutComponent.gap] when [MainAxisAlignment.spaceEvenly]
+    /// - Half of [LinearLayoutComponent.gap] when
+    ///   [MainAxisAlignment.spaceAround]
+    Offset initialOffset = Offset.zero,
+
+    /// true if laying out from the end (bottom/right)
+    bool reverse = false,
+  }) {
+    final mainAxisVectorIndex = direction.mainAxisVectorIndex;
+    final componentList = reverse ? components.reversed : components;
+    for (final (index, component) in componentList.indexed) {
+      final previousChild =
+          index > 0 ? componentList.elementAt(index - 1) : null;
+      final reference = previousChild == null
+          // Essentially the same as start, but gap is set.
+          ? initialOffset.toVector2()
+          // The "end" at any loop other than the first is the previous
+          // child's top left position minus the gap.
+          : previousChild.topLeftPosition +
+              (reverse
+                  ? -Vector2.all(gap)
+                  : previousChild.size + Vector2.all(gap));
+      final positionOffset = reverse ? component.size : Vector2.zero();
+      final newPosition = Vector2.zero();
+      newPosition[mainAxisVectorIndex] =
+          (reference - positionOffset)[mainAxisVectorIndex];
+      component.topLeftPosition.setFrom(newPosition);
+    }
+  }
+
+  void _layoutCrossAxis() {
+    final crossAxisVectorIndex = direction.crossAxisVectorIndex;
+    final positionChildren = this.positionChildren;
+    final newPosition = Vector2.zero();
+    final newSize = Vector2.zero();
+    // There is no need to track index because cross axis positioning is
+    // not influenced by sibling components.
+    for (final component in positionChildren) {
+      newPosition.setFrom(component.topLeftPosition);
+      final crossAxisLength = size[crossAxisVectorIndex];
+      final componentCrossAxisLength = component.size[crossAxisVectorIndex];
+      newPosition[crossAxisVectorIndex] = switch (crossAxisAlignment) {
+        CrossAxisAlignment.start => 0,
+        CrossAxisAlignment.end => crossAxisLength - componentCrossAxisLength,
+        CrossAxisAlignment.center =>
+          (crossAxisLength - componentCrossAxisLength) / 2,
+        CrossAxisAlignment.stretch => 0,
+        CrossAxisAlignment.baseline => 0,
+      };
+      component.topLeftPosition.setFrom(newPosition);
+
+      // Stretch is the only CrossAxisAlignment that involves resizing
+      if (crossAxisAlignment == CrossAxisAlignment.stretch) {
+        newSize.setFrom(component.size);
+        newSize[crossAxisVectorIndex] = size[crossAxisVectorIndex];
+        // Don't use setFrom because children might have their own resizing
+        // logic, such as a nested LayoutComponent.
+        component.size = newSize;
+      }
+    }
+  }
+
+  /// The total space along the main axis occupied by the [positionChildren]
+  /// without the [gap]s. This is so named because we expect to
+  /// implement crossAxisOccupiedSpace for shrink wrapping.
+  double get _mainAxisOccupiedSpace {
+    return positionChildren
+        .map((c) => c.size[direction.mainAxisVectorIndex])
+        .sum;
+  }
+
+  /// The total space along the main axis taken up by the gaps between
+  /// the [positionChildren].
+  double get _gapSpace {
+    return gap * (positionChildren.length - 1);
+  }
+
+  /// Any positioning done in [layoutChildren] should not affect the
+  /// [inherentSize]. This is because all [crossAxisAlignment] transformations
+  /// fall within the largestCrossAxisLength, while [mainAxisAlignment] is
+  /// entirely ignored in all cases where [inherentSize] is needed.
+  /// This means that [inherentSize] should be used either *before* or at the
+  /// start of [layoutChildren].
+  @override
+  Vector2 get inherentSize {
+    // Used at multiple points, cache to avoid recalculating each invocation
+    final positionChildren = this.positionChildren;
+    final crossAxisVectorIndex = direction.crossAxisVectorIndex;
+    final mainAxisVectorIndex = direction.mainAxisVectorIndex;
+    if (positionChildren.isEmpty) {
+      return Vector2.zero();
+    }
+    final largestCrossAxisLength = positionChildren
+        .map((component) => component.size[crossAxisVectorIndex])
+        .max;
+    // This is tricky because it depends on the mainAxisAlignment.
+    // This should only apply when mainAxisAlignment is start, center, or end.
+    // spaceAround, spaceBetween, and spaceEvenly requires the size as a
+    // constraint.
+    final cumulativeMainAxisLength = ((positionChildren.length - 1) * gap) +
+        positionChildren
+            .map((component) => component.size[mainAxisVectorIndex])
+            .sum;
+    final out = Vector2.zero();
+    out[mainAxisVectorIndex] = cumulativeMainAxisLength;
+    out[crossAxisVectorIndex] = largestCrossAxisLength;
+    return out;
+  }
+}
diff --git a/packages/flame/lib/src/experimental/padding_component.dart b/packages/flame/lib/src/experimental/padding_component.dart
new file mode 100644
index 00000000000..8e62b14c278
--- /dev/null
+++ b/packages/flame/lib/src/experimental/padding_component.dart
@@ -0,0 +1,81 @@
+import 'package:flame/components.dart';
+import 'package:flame/experimental.dart';
+import 'package:flame/extensions.dart';
+import 'package:flutter/rendering.dart';
+
+/// A padding component akin to Flutter's Padding widget.
+/// Use [padding] as you would Flutter's counterpart.
+/// In general, do not set the size of this widget explicitly, because it is
+/// only designed to shrink or expand to its child's dimensions, plus padding.
+///
+/// Set the child of this component with [child]. Avoid using [add] directly on
+/// an instance of [PaddingComponent] because its behavior is undefined with
+/// multiple children. It is designed only for one child.
+///
+/// You may set [padding] as well as the [child] after the fact, and it will
+/// cause the layout to refresh.
+class PaddingComponent extends LayoutComponent {
+  PaddingComponent({
+    EdgeInsets? padding,
+    super.anchor,
+    super.position,
+    PositionComponent? child,
+  })  : _padding = padding ?? EdgeInsets.zero,
+        super(size: null) {
+    this.child = child;
+  }
+
+  EdgeInsets _padding;
+
+  EdgeInsets get padding => _padding;
+
+  set padding(EdgeInsets value) {
+    _padding = value;
+    layoutChildren();
+  }
+
+  PositionComponent? _child;
+
+  /// The component that will be positioned by this component. The [child] will
+  /// be automatically mounted to the current component.
+  PositionComponent? get child => _child;
+
+  set child(PositionComponent? value) {
+    final oldChild = _child;
+    if (oldChild?.parent == this) {
+      oldChild?.removeFromParent();
+    }
+    _child = value;
+    if (value != null) {
+      add(value);
+    }
+  }
+
+  @override
+  void layoutChildren() {
+    final child = this.child;
+    if (child == null) {
+      return;
+    }
+    // Regardless of shrinkwrap or size, top left padding is set.
+    child.topLeftPosition.setFrom(padding.topLeft.toVector2());
+
+    if (!shrinkWrapMode) {
+      throw Exception(
+        // ignore: lines_longer_than_80_chars
+        'Unexpected state: PaddingComponent should always be in shrinkWrapMode.',
+      );
+    }
+    size.setFrom(inherentSize);
+  }
+
+  @override
+  Vector2 get inherentSize {
+    final childWidth = child?.size.x ?? 0;
+    final childHeight = child?.size.y ?? 0;
+    return Vector2(
+      childWidth + padding.horizontal,
+      childHeight + padding.vertical,
+    );
+  }
+}
diff --git a/packages/flame/lib/src/experimental/row_component.dart b/packages/flame/lib/src/experimental/row_component.dart
index e97f9fa3397..8db82628173 100644
--- a/packages/flame/lib/src/experimental/row_component.dart
+++ b/packages/flame/lib/src/experimental/row_component.dart
@@ -1,4 +1,4 @@
-import 'package:flame/src/experimental/layout_component.dart';
+import 'package:flame/src/experimental/linear_layout_component.dart';
 import 'package:flutter/rendering.dart';
 
 /// RowComponent is a layout component that arranges its children in a
@@ -29,7 +29,7 @@ import 'package:flutter/rendering.dart';
 ///   ],
 /// );
 /// ```
-class RowComponent extends LayoutComponent {
+class RowComponent extends LinearLayoutComponent {
   RowComponent({
     super.mainAxisAlignment = MainAxisAlignment.start,
     super.crossAxisAlignment = CrossAxisAlignment.start,


Test patch:
diff --git a/packages/flame/test/experimental/layout_component_test.dart b/packages/flame/test/experimental/linear_layout_component_test.dart
similarity index 91%
rename from packages/flame/test/experimental/layout_component_test.dart
rename to packages/flame/test/experimental/linear_layout_component_test.dart
index e600eff2254..18a2de15195 100644
--- a/packages/flame/test/experimental/layout_component_test.dart
+++ b/packages/flame/test/experimental/linear_layout_component_test.dart
@@ -4,19 +4,19 @@ import 'package:flame/experimental.dart';
 import 'package:flame_test/flame_test.dart';
 import 'package:flutter/rendering.dart';
 import 'package:test/test.dart';
-import 'layout_component_test_helpers.dart';
+import 'linear_layout_component_test_helpers.dart';
 
 void main() {
-  group('LayoutComponent', () {
+  group('LinearLayoutComponent', () {
     group('mainAxisAlignment', () {
-      runLayoutComponentTestRegistry(
+      runLinearLayoutComponentTestRegistry(
         {
           'mainAxisAlignment = start': (game, direction) async {
             final circle = CircleComponent(radius: 20);
             final rectangle = RectangleComponent(size: Vector2(100, 50));
             final text = TextComponent(text: 'testing');
             await game.ensureAdd(
-              LayoutComponent.fromDirection(
+              LinearLayoutComponent.fromDirection(
                 direction,
                 children: [circle, rectangle, text],
               ),
@@ -38,7 +38,7 @@ void main() {
             final text = TextComponent(text: 'testing');
             final layoutComponentSize = Vector2.all(500);
             await game.ensureAdd(
-              LayoutComponent.fromDirection(
+              LinearLayoutComponent.fromDirection(
                 direction,
                 children: [circle, rectangle, text],
                 size: layoutComponentSize,
@@ -65,7 +65,7 @@ void main() {
             final text = TextComponent(text: 'testing');
             final layoutComponentSize = Vector2.all(500);
             const gap = 20.0;
-            final layoutComponent = LayoutComponent.fromDirection(
+            final layoutComponent = LinearLayoutComponent.fromDirection(
               direction,
               children: [circle, rectangle, text],
               size: layoutComponentSize,
@@ -94,7 +94,7 @@ void main() {
             final rectangle = RectangleComponent(size: Vector2(100, 50));
             final text = TextComponent(text: 'testing');
             final layoutComponentSize = Vector2.all(500);
-            final layoutComponent = LayoutComponent.fromDirection(
+            final layoutComponent = LinearLayoutComponent.fromDirection(
               direction,
               children: [circle, rectangle, text],
               size: layoutComponentSize,
@@ -124,7 +124,7 @@ void main() {
             final rectangle = RectangleComponent(size: Vector2(100, 50));
             final text = TextComponent(text: 'testing');
             final layoutComponentSize = Vector2.all(500);
-            final layoutComponent = LayoutComponent.fromDirection(
+            final layoutComponent = LinearLayoutComponent.fromDirection(
               direction,
               children: [circle, rectangle, text],
               size: layoutComponentSize,
@@ -160,7 +160,7 @@ void main() {
             final rectangle = RectangleComponent(size: Vector2(100, 50));
             final text = TextComponent(text: 'testing');
             final layoutComponentSize = Vector2.all(500);
-            final layoutComponent = LayoutComponent.fromDirection(
+            final layoutComponent = LinearLayoutComponent.fromDirection(
               direction,
               children: [circle, rectangle, text],
               size: layoutComponentSize,
@@ -190,13 +190,13 @@ void main() {
     });
 
     group('crossAxisAlignment', () {
-      runLayoutComponentTestRegistry({
+      runLinearLayoutComponentTestRegistry({
         'crossAlignment = start': (game, direction) async {
           final circle = CircleComponent(radius: 20);
           final rectangle = RectangleComponent(size: Vector2(100, 50));
           final text = TextComponent(text: 'testing');
           final layoutComponentSize = Vector2.all(500);
-          final layoutComponent = LayoutComponent.fromDirection(
+          final layoutComponent = LinearLayoutComponent.fromDirection(
             direction,
             children: [circle, rectangle, text],
             size: layoutComponentSize,
@@ -212,7 +212,7 @@ void main() {
           final rectangle = RectangleComponent(size: Vector2(100, 50));
           final text = TextComponent(text: 'testing');
           final layoutComponentSize = Vector2.all(500);
-          final layoutComponent = LayoutComponent.fromDirection(
+          final layoutComponent = LinearLayoutComponent.fromDirection(
             direction,
             children: [circle, rectangle, text],
             size: layoutComponentSize,
@@ -238,7 +238,7 @@ void main() {
           final rectangle = RectangleComponent(size: Vector2(100, 50));
           final text = TextComponent(text: 'testing');
           final layoutComponentSize = Vector2.all(500);
-          final layoutComponent = LayoutComponent.fromDirection(
+          final layoutComponent = LinearLayoutComponent.fromDirection(
             direction,
             children: [circle, rectangle, text],
             size: layoutComponentSize,
@@ -263,13 +263,13 @@ void main() {
     });
 
     group('size', () {
-      runLayoutComponentTestRegistry({
+      runLinearLayoutComponentTestRegistry({
         'size=null sets size to inherentSize': (game, direction) async {
           final circle = CircleComponent(radius: 20);
           final rectangle = RectangleComponent(size: Vector2(100, 50));
           final text = TextComponent(text: 'testing');
           final layoutComponentSize = Vector2.all(500);
-          final layoutComponent = LayoutComponent.fromDirection(
+          final layoutComponent = LinearLayoutComponent.fromDirection(
             direction,
             children: [circle, rectangle, text],
             size: layoutComponentSize,
@@ -283,7 +283,7 @@ void main() {
           final circle = CircleComponent(radius: 20);
           final rectangle = RectangleComponent(size: Vector2(100, 50));
           final text = TextComponent(text: 'testing');
-          final layoutComponent = LayoutComponent.fromDirection(
+          final layoutComponent = LinearLayoutComponent.fromDirection(
             direction,
             children: [circle, rectangle, text],
             mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -296,7 +296,7 @@ void main() {
           final circle = CircleComponent(radius: 20);
           final rectangle = RectangleComponent(size: Vector2(100, 50));
           final text = TextComponent(text: 'testing');
-          final layoutComponent = LayoutComponent.fromDirection(
+          final layoutComponent = LinearLayoutComponent.fromDirection(
             direction,
             children: [circle, rectangle, text],
             crossAxisAlignment: CrossAxisAlignment.center,
@@ -313,7 +313,7 @@ void main() {
           final circle = CircleComponent(radius: 20);
           final rectangle = RectangleComponent(size: Vector2(100, 50));
           final text = TextComponent(text: 'testing');
-          final layoutComponent = LayoutComponent.fromDirection(
+          final layoutComponent = LinearLayoutComponent.fromDirection(
             direction,
             children: [circle, rectangle, text],
             crossAxisAlignment: CrossAxisAlignment.stretch,
@@ -324,14 +324,14 @@ void main() {
       });
     });
     group('children', () {
-      runLayoutComponentTestRegistry(
+      runLinearLayoutComponentTestRegistry(
         {
           'size responds when children are added and then resized':
               (game, direction) async {
             final circle = CircleComponent(radius: 20);
             final rectangle2 = RectangleComponent(size: Vector2(100, 50));
             // final rectangle2 = RectangleComponent(size: Vector2(200, 70));
-            final layoutComponent = LayoutComponent.fromDirection(
+            final layoutComponent = LinearLayoutComponent.fromDirection(
               direction,
               crossAxisAlignment: CrossAxisAlignment.stretch,
             );
diff --git a/packages/flame/test/experimental/layout_component_test_helpers.dart b/packages/flame/test/experimental/linear_layout_component_test_helpers.dart
similarity index 79%
rename from packages/flame/test/experimental/layout_component_test_helpers.dart
rename to packages/flame/test/experimental/linear_layout_component_test_helpers.dart
index 39e53c6d319..c04f3f8e7ca 100644
--- a/packages/flame/test/experimental/layout_component_test_helpers.dart
+++ b/packages/flame/test/experimental/linear_layout_component_test_helpers.dart
@@ -1,19 +1,19 @@
 import 'package:flame/components.dart';
+import 'package:flame/experimental.dart';
 import 'package:flame/game.dart';
-import 'package:flame/src/experimental/layout_component.dart';
 import 'package:flame_test/flame_test.dart';
 import 'package:meta/meta.dart';
 
 @isTest
-void testLayoutComponent(
+void testLinearLayoutComponent(
   String testName,
-  LayoutComponent Function() layoutConstructor,
+  LinearLayoutComponent Function() layoutConstructor,
   Future<void> Function(FlameGame<World>) testBody,
 ) {
   testWithFlameGame(testName, testBody);
 }
 
-Future<void> runLayoutComponentTestRegistry(
+Future<void> runLinearLayoutComponentTestRegistry(
   Map<String, Future<void> Function(FlameGame<World>, Direction)> testRegistry,
 ) async {
   for (final entry in testRegistry.entries) {
diff --git a/packages/flame/test/experimental/padding_component_test.dart b/packages/flame/test/experimental/padding_component_test.dart
new file mode 100644
index 00000000000..14b4cacea96
--- /dev/null
+++ b/packages/flame/test/experimental/padding_component_test.dart
@@ -0,0 +1,86 @@
+import 'package:flame/components.dart';
+import 'package:flame/experimental.dart';
+import 'package:flame/src/image_composition.dart';
+import 'package:flame_test/flame_test.dart';
+import 'package:flutter/rendering.dart';
+import 'package:test/test.dart';
+
+void main() {
+  group('PaddingComponent', () {
+    testWithFlameGame('properly sizes self and positions child', (game) async {
+      const padding = EdgeInsets.all(16);
+      final circle = CircleComponent(radius: 20);
+      final paddingComponent = PaddingComponent(
+        padding: padding,
+        child: circle,
+      );
+      await game.ensureAdd(paddingComponent);
+      expect(
+        paddingComponent.size,
+        Vector2(
+          circle.size.x + padding.horizontal,
+          circle.size.y + padding.vertical,
+        ),
+      );
+      expect(
+        paddingComponent.child?.topLeftPosition,
+        padding.topLeft.toVector2(),
+      );
+    });
+    testWithFlameGame('properly sizes self and positions after child is set',
+        (game) async {
+      const padding = EdgeInsets.all(16);
+      final circle = CircleComponent(radius: 20);
+      final paddingComponent = PaddingComponent(
+        padding: padding,
+      );
+      await game.ensureAdd(paddingComponent);
+      expect(
+        paddingComponent.size,
+        Vector2(
+          padding.horizontal,
+          padding.vertical,
+        ),
+      );
+      paddingComponent.child = circle;
+      await game.ready();
+      expect(
+        paddingComponent.size,
+        Vector2(
+          circle.size.x + padding.horizontal,
+          circle.size.y + padding.vertical,
+        ),
+      );
+      expect(
+        paddingComponent.child?.topLeftPosition,
+        padding.topLeft.toVector2(),
+      );
+    });
+    testWithFlameGame('properly sizes self and positions after padding is set',
+        (game) async {
+      final circle = CircleComponent(radius: 20);
+      final paddingComponent = PaddingComponent(child: circle);
+      await game.ensureAdd(paddingComponent);
+      expect(
+        paddingComponent.size,
+        circle.size,
+      );
+      const padding = EdgeInsets.symmetric(
+        vertical: 16,
+        horizontal: 24,
+      );
+      paddingComponent.padding = padding;
+      expect(
+        paddingComponent.size,
+        Vector2(
+          circle.size.x + padding.horizontal,
+          circle.size.y + padding.vertical,
+        ),
+      );
+      expect(
+        paddingComponent.child?.topLeftPosition,
+        padding.topLeft.toVector2(),
+      );
+    });
+  });
+}