[Story of Your Layout] Visualize main axis spaces (#1378)

* Visualize main axis spaces

* Refactor class name and remove commented code

* Remove before/after space and mix it into leadingSpace

* turn off experimental flag

* update goldens
diff --git a/packages/devtools_app/lib/src/inspector/flutter/inspector_data_models.dart b/packages/devtools_app/lib/src/inspector/flutter/inspector_data_models.dart
index 65de1b5..b51db20 100644
--- a/packages/devtools_app/lib/src/inspector/flutter/inspector_data_models.dart
+++ b/packages/devtools_app/lib/src/inspector/flutter/inspector_data_models.dart
@@ -257,16 +257,55 @@
 
   double get crossAxisDimension => dimension(crossAxisDirection);
 
-  List<Size> childrenRenderSizes({
+  /// render properties for laying out rendered Flex & Flex children widgets
+  /// the computation is similar to [RenderFlex].performLayout() method
+  List<RenderProperties> childrenRenderProperties({
     @required double smallestRenderWidth,
     @required double largestRenderWidth,
     @required double smallestRenderHeight,
     @required double largestRenderHeight,
-    @required double maxWidthAvailable,
-    @required double maxHeightAvailable,
+    @required double Function(Axis) maxSizeAvailable,
   }) {
-    double maxSizeAvailable(Axis axis) {
-      return axis == Axis.horizontal ? maxWidthAvailable : maxHeightAvailable;
+    /// calculate the render empty spaces
+    final freeSpace = dimension(direction) - sum(childrenDimensions(direction));
+
+    double leadingSpace(double freeSpace) {
+      if (children.isEmpty) return 0.0;
+      switch (mainAxisAlignment) {
+        case MainAxisAlignment.start:
+        case MainAxisAlignment.end:
+          return freeSpace;
+        case MainAxisAlignment.center:
+          return freeSpace * 0.5;
+        case MainAxisAlignment.spaceBetween:
+          return 0.0;
+        case MainAxisAlignment.spaceAround:
+          final spaceBetweenChildren = freeSpace / children.length;
+          return spaceBetweenChildren * 0.5;
+        case MainAxisAlignment.spaceEvenly:
+          return freeSpace / (children.length + 1);
+        default:
+          return 0.0;
+      }
+    }
+
+    double betweenSpace(double freeSpace) {
+      if (children.isEmpty) return 0.0;
+      switch (mainAxisAlignment) {
+        case MainAxisAlignment.start:
+        case MainAxisAlignment.end:
+        case MainAxisAlignment.center:
+          return 0.0;
+        case MainAxisAlignment.spaceBetween:
+          if (children.length == 1) return freeSpace;
+          return freeSpace / (children.length - 1);
+        case MainAxisAlignment.spaceAround:
+          return freeSpace / children.length;
+        case MainAxisAlignment.spaceEvenly:
+          return freeSpace / (children.length + 1);
+        default:
+          return 0.0;
+      }
     }
 
     double smallestRenderSize(Axis axis) {
@@ -284,6 +323,10 @@
 
     List<double> renderSizes(Axis axis) {
       final sizes = childrenDimensions(axis);
+      if (freeSpace > 0.0 && axis == direction) {
+        /// include free space in the computation
+        sizes.add(freeSpace);
+      }
       final smallestSize = min(sizes);
       final largestSize = max(sizes);
       if (axis == direction ||
@@ -308,93 +351,117 @@
 
     final widths = renderSizes(Axis.horizontal);
     final heights = renderSizes(Axis.vertical);
-    assert(widths.length == heights.length);
-    return [
-      for (var i = 0; i < widths.length; ++i) Size(widths[i], heights[i])
-    ];
-  }
 
-  double _calculateCrossAxisOffset(double maxDimension, double usedDimension) {
-    if (crossAxisAlignment == CrossAxisAlignment.start ||
-        crossAxisAlignment == CrossAxisAlignment.stretch ||
-        maxDimension == usedDimension) return 0.0;
-    final emptySpace = math.max(0.0, maxDimension - usedDimension);
-    if (crossAxisAlignment == CrossAxisAlignment.end) return emptySpace;
-    return emptySpace * 0.5;
-  }
+    final renderFreeSpace = freeSpace > 0.0
+        ? (isMainAxisHorizontal ? widths.last : heights.last)
+        : 0.0;
 
-  List<Offset> childrenRenderOffsets({
-    @required List<Size> childrenRenderSizes,
-    @required double maxWidthAvailable,
-    @required double maxHeightAvailable,
-  }) {
-    final offsets = <Offset>[];
-    for (var i = 0; i < children.length; ++i) {
-      double dx, dy;
-      if (direction == Axis.horizontal) {
-        dx = i == 0 ? 0.0 : offsets.last.dx + childrenRenderSizes[i - 1].width;
-        dy = _calculateCrossAxisOffset(
-            maxHeightAvailable, childrenRenderSizes[i].height);
-      } else {
-        dy = i == 0 ? 0.0 : offsets.last.dy + childrenRenderSizes[i - 1].height;
-        dx = _calculateCrossAxisOffset(
-            maxWidthAvailable, childrenRenderSizes[i].width);
-      }
-      offsets.add(Offset(dx, dy));
+    final renderLeadingSpace = leadingSpace(renderFreeSpace);
+    final renderBetweenSpace = betweenSpace(renderFreeSpace);
+
+    final childrenRenderProps = <RenderProperties>[];
+
+    double lastMainAxisOffset() {
+      if (childrenRenderProps.isEmpty) return 0.0;
+      return childrenRenderProps.last.mainAxisOffset;
     }
-    return offsets;
+
+    double lastMainAxisDimension() {
+      if (childrenRenderProps.isEmpty) return 0.0;
+      return childrenRenderProps.last.mainAxisDimension;
+    }
+
+    double space(int index) {
+      if (index == 0) {
+        if (mainAxisAlignment == MainAxisAlignment.start) return 0.0;
+        return renderLeadingSpace;
+      }
+      return renderBetweenSpace;
+    }
+
+    double calculateMainAxisOffset(int i) {
+      return lastMainAxisOffset() + lastMainAxisDimension() + space(i);
+    }
+
+    double calculateCrossAxisOffset(int i) {
+      final maxDimension = maxSizeAvailable(crossAxisDirection);
+      final usedDimension =
+          crossAxisDirection == Axis.horizontal ? widths[i] : heights[i];
+
+      if (crossAxisAlignment == CrossAxisAlignment.start ||
+          crossAxisAlignment == CrossAxisAlignment.stretch ||
+          maxDimension == usedDimension) return 0.0;
+      final emptySpace = math.max(0.0, maxDimension - usedDimension);
+      if (crossAxisAlignment == CrossAxisAlignment.end) return emptySpace;
+      return emptySpace * 0.5;
+    }
+
+    for (var i = 0; i < children.length; ++i) {
+      childrenRenderProps.add(
+        RenderProperties(
+          axis: direction,
+          size: Size(widths[i], heights[i]),
+          offset: Offset.zero,
+          realSize: children[i].size,
+        )
+          ..mainAxisOffset = calculateMainAxisOffset(i)
+          ..crossAxisOffset = calculateCrossAxisOffset(i),
+      );
+    }
+
+    final spaces = <RenderProperties>[];
+    final actualLeadingSpace = leadingSpace(freeSpace);
+    final actualBetweenSpace = betweenSpace(freeSpace);
+    final renderPropsWithFullCrossAxisDimension =
+        RenderProperties(axis: direction, isFreeSpace: true)
+          ..crossAxisDimension = maxSizeAvailable(crossAxisDirection)
+          ..crossAxisRealDimension = dimension(crossAxisDirection)
+          ..crossAxisOffset = 0.0;
+    if (actualLeadingSpace > 0.0 &&
+        mainAxisAlignment != MainAxisAlignment.start) {
+      spaces.add(renderPropsWithFullCrossAxisDimension.clone()
+        ..mainAxisOffset = 0.0
+        ..mainAxisDimension = renderLeadingSpace
+        ..mainAxisRealDimension = actualLeadingSpace);
+    }
+    if (actualBetweenSpace > 0.0)
+      for (var i = 0; i < childrenRenderProps.length - 1; ++i) {
+        final child = childrenRenderProps[i];
+        spaces.add(renderPropsWithFullCrossAxisDimension.clone()
+          ..mainAxisDimension = renderBetweenSpace
+          ..mainAxisRealDimension = actualBetweenSpace
+          ..mainAxisOffset = child.mainAxisOffset + child.mainAxisDimension);
+      }
+    if (actualLeadingSpace > 0.0 &&
+        mainAxisAlignment != MainAxisAlignment.end) {
+      spaces.add(renderPropsWithFullCrossAxisDimension.clone()
+        ..mainAxisOffset = childrenRenderProps.last.mainAxisDimension +
+            childrenRenderProps.last.mainAxisOffset
+        ..mainAxisDimension = renderLeadingSpace
+        ..mainAxisRealDimension = actualLeadingSpace);
+    }
+    return [...childrenRenderProps, ...spaces];
   }
 
-  List<RenderInfo> childrenRenderInformation({
-    @required double smallestRenderWidth,
-    @required double largestRenderWidth,
-    @required double smallestRenderHeight,
-    @required double largestRenderHeight,
-    @required double maxWidthAvailable,
-    @required double maxHeightAvailable,
-  }) {
-    final renderSizes = childrenRenderSizes(
-      smallestRenderWidth: smallestRenderWidth,
-      largestRenderWidth: largestRenderWidth,
-      smallestRenderHeight: smallestRenderHeight,
-      largestRenderHeight: largestRenderHeight,
-      maxWidthAvailable: maxWidthAvailable,
-      maxHeightAvailable: maxHeightAvailable,
-    );
-    final renderOffsets = childrenRenderOffsets(
-      childrenRenderSizes: renderSizes,
-      maxWidthAvailable: maxWidthAvailable,
-      maxHeightAvailable: maxHeightAvailable,
-    );
-    return [
-      for (var i = 0; i < children.length; ++i)
-        RenderInfo(
-            direction, renderSizes[i], renderOffsets[i], children[i].size)
-    ];
-  }
-
-  List<RenderInfo> crossAxisSpaces({
-    List<RenderInfo> childrenRenderInfo,
-    double maxWidthAvailable,
-    double maxHeightAvailable,
+  List<RenderProperties> crossAxisSpaces({
+    @required List<RenderProperties> childrenRenderProps,
+    @required double Function(Axis) maxSizeAvailable,
   }) {
     if (crossAxisAlignment == CrossAxisAlignment.stretch) return [];
-    final spaces = <RenderInfo>[];
-    final maxSizeAvailable = crossAxisDirection == Axis.horizontal
-        ? maxWidthAvailable
-        : maxHeightAvailable;
+    final spaces = <RenderProperties>[];
     for (var i = 0; i < children.length; ++i) {
       if (dimension(crossAxisDirection) ==
               children[i].dimension(crossAxisDirection) ||
-          childrenRenderInfo[i].crossAxisDimension == maxSizeAvailable)
-        continue;
+          childrenRenderProps[i].crossAxisDimension ==
+              maxSizeAvailable(crossAxisDirection)) continue;
 
-      final renderInfo = childrenRenderInfo[i];
+      final renderInfo = childrenRenderProps[i];
       final space = renderInfo.clone();
 
       space.crossAxisRealDimension =
           crossAxisDimension - space.crossAxisRealDimension;
-      space.crossAxisDimension = maxSizeAvailable - space.crossAxisDimension;
+      space.crossAxisDimension =
+          maxSizeAvailable(crossAxisDirection) - space.crossAxisDimension;
 
       if (crossAxisAlignment == CrossAxisAlignment.center) {
         space.crossAxisDimension *= 0.5;
@@ -428,17 +495,23 @@
       EnumUtils<TextBaseline>(TextBaseline.values);
 }
 
-class RenderInfo {
-  RenderInfo(this.axis, Size size, Offset offset, Size realSize)
-      : width = size.width,
-        height = size.height,
-        realWidth = realSize.width,
-        realHeight = realSize.height,
-        dx = offset.dx,
-        dy = offset.dy;
+class RenderProperties {
+  RenderProperties({
+    @required this.axis,
+    Size size,
+    Offset offset,
+    Size realSize,
+    this.isFreeSpace = false,
+  })  : width = size?.width,
+        height = size?.height,
+        realWidth = realSize?.width,
+        realHeight = realSize?.height,
+        dx = offset?.dx,
+        dy = offset?.dy;
 
   final Axis axis;
 
+  bool isFreeSpace;
   double dx, dy;
   double width, height;
   double realWidth, realHeight;
@@ -505,7 +578,13 @@
       realWidth = newVal;
   }
 
-  RenderInfo clone() {
-    return RenderInfo(axis, size, offset, realSize);
+  RenderProperties clone() {
+    return RenderProperties(
+      axis: axis,
+      size: size,
+      offset: offset,
+      realSize: realSize,
+      isFreeSpace: isFreeSpace,
+    );
   }
 }
diff --git a/packages/devtools_app/lib/src/inspector/flutter/story_of_your_layout/flex.dart b/packages/devtools_app/lib/src/inspector/flutter/story_of_your_layout/flex.dart
index 46fb672..2a2a11b 100644
--- a/packages/devtools_app/lib/src/inspector/flutter/story_of_your_layout/flex.dart
+++ b/packages/devtools_app/lib/src/inspector/flutter/story_of_your_layout/flex.dart
@@ -25,7 +25,7 @@
 const arrowStrokeWidth = 1.5;
 
 /// Hardcoded sizes for scaling the flex children widget properly.
-const minRenderWidth = 225.0;
+const minRenderWidth = 215.0;
 const minRenderHeight = 275.0;
 const defaultMaxRenderWidth = 300.0;
 const defaultMaxRenderHeight = 300.0;
@@ -74,6 +74,8 @@
 const crossAxisTextColor =
     ThemedColor(crossAxisLightTextColor, crossAxisDarkTextColor);
 
+const freeSpaceAssetName = 'assets/img/story_of_layout/empty_space.png';
+
 class StoryOfYourFlexWidget extends StatefulWidget {
   const StoryOfYourFlexWidget(
     this.properties, {
@@ -280,16 +282,31 @@
           final maxWidth = constraints.maxWidth;
           final maxHeight = constraints.maxHeight;
 
-          final renderInfo = properties.childrenRenderInformation(
+          double maxSizeAvailable(Axis axis) {
+            return axis == Axis.horizontal ? maxWidth : maxHeight;
+          }
+
+          final childrenAndMainAxisSpacesRenderProps =
+              properties.childrenRenderProperties(
             smallestRenderWidth: minRenderWidth,
             largestRenderWidth: defaultMaxRenderWidth,
             smallestRenderHeight: minRenderHeight,
             largestRenderHeight: defaultMaxRenderHeight,
-            maxWidthAvailable: maxWidth,
-            maxHeightAvailable: maxHeight,
+            maxSizeAvailable: maxSizeAvailable,
           );
 
-          final widgetChildren = <Widget>[
+          final renderProps = childrenAndMainAxisSpacesRenderProps
+              .where((renderProps) => !renderProps.isFreeSpace)
+              .toList();
+          final mainAxisSpaces = childrenAndMainAxisSpacesRenderProps
+              .where((renderProps) => renderProps.isFreeSpace)
+              .toList();
+          final crossAxisSpaces = properties.crossAxisSpaces(
+            childrenRenderProps: renderProps,
+            maxSizeAvailable: maxSizeAvailable,
+          );
+
+          final childrenRenderWidgets = <Widget>[
             for (var i = 0; i < children.length; i++)
               _visualizeChild(
                 backgroundColor:
@@ -299,25 +316,24 @@
                 childProperties: children[i],
                 borderColor: i.isOdd ? mainAxisColor : crossAxisColor,
                 textColor: i.isOdd ? null : const Color(0xFF303030),
-                renderSize: renderInfo[i].size,
-                renderOffset: renderInfo[i].offset,
+                renderSize: renderProps[i].size,
+                renderOffset: renderProps[i].offset,
               )
           ];
 
-          final crossAxisSpaces = <Widget>[
-            for (var spaceRenderInfo in properties.crossAxisSpaces(
-              childrenRenderInfo: renderInfo,
-              maxWidthAvailable: maxWidth,
-              maxHeightAvailable: maxHeight,
-            ))
+          final freeSpacesWidgets = <Widget>[
+            for (var renderProperties in [
+              ...mainAxisSpaces,
+              ...crossAxisSpaces
+            ])
               Positioned(
-                top: spaceRenderInfo.dy,
-                left: spaceRenderInfo.dx,
+                top: renderProperties.dy,
+                left: renderProperties.dx,
                 child: EmptySpaceVisualizerWidget(
-                  width: spaceRenderInfo.realWidth,
-                  height: spaceRenderInfo.realHeight,
-                  renderWidth: spaceRenderInfo.width,
-                  renderHeight: spaceRenderInfo.height,
+                  width: renderProperties.realWidth,
+                  height: renderProperties.realHeight,
+                  renderWidth: renderProperties.width,
+                  renderHeight: renderProperties.height,
                 ),
               )
           ];
@@ -329,14 +345,27 @@
                 minWidth: maxWidth,
                 minHeight: maxHeight,
                 maxWidth: direction == Axis.horizontal
-                    ? sum(renderInfo.map((renderSize) => renderSize.width))
+                    ? sum(childrenAndMainAxisSpacesRenderProps
+                        .map((renderSize) => renderSize.width))
                     : maxWidth,
                 maxHeight: direction == Axis.vertical
-                    ? sum(renderInfo.map((renderSize) => renderSize.height))
+                    ? sum(childrenAndMainAxisSpacesRenderProps
+                        .map((renderSize) => renderSize.height))
                     : maxHeight,
               ),
               child: Stack(
-                children: [...widgetChildren, ...crossAxisSpaces],
+                children: [
+                  Positioned.fill(
+                    child: Image.asset(
+                      freeSpaceAssetName,
+                      width: maxWidth,
+                      height: maxHeight,
+                      fit: BoxFit.fill,
+                    ),
+                  ),
+                  ...childrenRenderWidgets,
+                  ...freeSpacesWidgets
+                ],
               ),
             ),
           );
@@ -546,6 +575,7 @@
                                     quarterTurns: 3,
                                     child: Text(
                                       properties.verticalDirectionDescription,
+                                      overflow: TextOverflow.ellipsis,
                                       textAlign: TextAlign.center,
                                       textScaleFactor: largeTextScaleFactor,
                                       style:
@@ -574,6 +604,7 @@
                                   child: FittedBox(
                                     child: Text(
                                       properties.horizontalDirectionDescription,
+                                      overflow: TextOverflow.ellipsis,
                                       textAlign: TextAlign.center,
                                       textScaleFactor: largeTextScaleFactor,
                                       style:
@@ -677,12 +708,14 @@
         ),
         color: backgroundColor,
       ),
-      margin: const EdgeInsets.all(1.0),
     );
   }
 }
 
 class EmptySpaceVisualizerWidget extends StatelessWidget {
+  // width and height to be displayed on Text
+  // width and height for rendering/sizing the widget
+
   const EmptySpaceVisualizerWidget({
     Key key,
     @required this.width,
@@ -691,16 +724,11 @@
     @required this.renderHeight,
   }) : super(key: key);
 
-  // width and height to be displayed on Text
   final double width;
   final double height;
-
-  // width and height for rendering/sizing the widget
   final double renderWidth;
   final double renderHeight;
 
-  static const assetName = 'assets/img/story_of_layout/empty_space.png';
-
   @override
   Widget build(BuildContext context) {
     return Container(
@@ -708,14 +736,6 @@
       height: renderHeight,
       child: Stack(
         children: <Widget>[
-          Positioned.fill(
-            child: Image.asset(
-              assetName,
-              width: renderWidth,
-              height: renderHeight,
-              fit: BoxFit.fill,
-            ),
-          ),
           Align(
             alignment: Alignment.bottomCenter,
             child: Container(
@@ -744,6 +764,7 @@
                   quarterTurns: 1,
                   child: Text(
                     'h=${toStringAsFixed(height)}',
+                    overflow: TextOverflow.ellipsis,
                   ),
                 ),
                 arrowColor: heightIndicatorColor,
diff --git a/packages/devtools_app/pubspec.yaml b/packages/devtools_app/pubspec.yaml
index 1a68ccc..dc3bc41 100644
--- a/packages/devtools_app/pubspec.yaml
+++ b/packages/devtools_app/pubspec.yaml
@@ -98,18 +98,9 @@
     - web/icons/inspector/
     - web/icons/memory/
     - web/icons/perf/
-    - assets/img/story_of_layout/main_axis_alignment/center.png
-    - assets/img/story_of_layout/main_axis_alignment/end.png
-    - assets/img/story_of_layout/main_axis_alignment/spaceAround.png
-    - assets/img/story_of_layout/main_axis_alignment/spaceBetween.png
-    - assets/img/story_of_layout/main_axis_alignment/spaceEvenly.png
-    - assets/img/story_of_layout/main_axis_alignment/start.png
-    - assets/img/story_of_layout/cross_axis_alignment/start.png
-    - assets/img/story_of_layout/cross_axis_alignment/end.png
-    - assets/img/story_of_layout/cross_axis_alignment/center.png
-    - assets/img/story_of_layout/cross_axis_alignment/stretch.png
-    - assets/img/story_of_layout/cross_axis_alignment/baseline.png
-    - assets/img/story_of_layout/empty_space.png
+    - assets/img/story_of_layout/
+    - assets/img/story_of_layout/main_axis_alignment/
+    - assets/img/story_of_layout/cross_axis_alignment/
 
   # See https://github.com/flutter/flutter/wiki/Desktop-shells#fonts
   # TODO(https://github.com/flutter/devtools/issues/1339): Include a monospaced font.
diff --git a/packages/devtools_app/test/flutter/story_of_layout/goldens/story_of_column_layout.png b/packages/devtools_app/test/flutter/story_of_layout/goldens/story_of_column_layout.png
index a7295c0..eb62a60 100644
--- a/packages/devtools_app/test/flutter/story_of_layout/goldens/story_of_column_layout.png
+++ b/packages/devtools_app/test/flutter/story_of_layout/goldens/story_of_column_layout.png
Binary files differ
diff --git a/packages/devtools_app/test/flutter/story_of_layout/goldens/story_of_row_layout.png b/packages/devtools_app/test/flutter/story_of_layout/goldens/story_of_row_layout.png
index fcb9763..1332cd9 100644
--- a/packages/devtools_app/test/flutter/story_of_layout/goldens/story_of_row_layout.png
+++ b/packages/devtools_app/test/flutter/story_of_layout/goldens/story_of_row_layout.png
Binary files differ