A Touch of Class

fantasai
2023-06-22

Implementing align-content for blocks

This documents the principal patch for Gecko bug 1684236, “Implement align-content for block containers”.

Trigger an independent formatting context

The first, and simplest, thing that align-content does is enforce an independent formatting context by turning the block, if it is not already, into a block formatting context root so that it fully contains its descendant margins and floats.

Make nsCSSFrameConstructor Construct a Flow Root

This segment adds a test for whether align-content is normal to the part of nsCSSFrameConstructor that flags an nsBlockFrame as being a BFC root.

nsCSSFrameConstructor::ConstructNonScrollableBlock()
diff --git a/layout/base/nsCSSFrameConstructor.cpp b/layout/base/nsCSSFrameConstructor.cpp
--- a/layout/base/nsCSSFrameConstructor.cpp
+++ b/layout/base/nsCSSFrameConstructor.cpp
@@ -4582,19 +4582,21 @@ nsIFrame* nsCSSFrameConstructor::Constru
   ComputedStyle* const computedStyle = aItem.mComputedStyle;

   // We want a block formatting context root in paginated contexts for
   // every block that would be scrollable in a non-paginated context.
   // We mark our blocks with a bit here if this condition is true, so
   // we can check it later in nsIFrame::ApplyPaginatedOverflowClipping.
   bool clipPaginatedOverflow =
       (aItem.mFCData->mBits & FCDATA_FORCED_NON_SCROLLABLE_BLOCK) != 0;
+  bool isAligned = computedStyle->StylePosition()->mAlignContent.primary !=
+                   mozilla::StyleAlignFlags::NORMAL;
   nsFrameState flags = nsFrameState(0);
   if ((aDisplay->IsAbsolutelyPositionedStyle() || aDisplay->IsFloatingStyle() ||
-       aDisplay->DisplayInside() == StyleDisplayInside::FlowRoot ||
+       aDisplay->DisplayInside() == StyleDisplayInside::FlowRoot || isAligned ||
        clipPaginatedOverflow) &&
       !aParentFrame->IsInSVGTextSubtree()) {
     flags = NS_BLOCK_FORMATTING_CONTEXT_STATE_BITS;
     if (clipPaginatedOverflow) {
       flags |= NS_BLOCK_CLIP_PAGINATED_OVERFLOW;
     }
   }

Make Style Changes Trigger Frame Reconstruction

This segment in nsStyleStruct.cpp updates CalcDifference() to trigger frame reconstruction whenever a block frame switches between normal and non-normal align-content values, to support switching between a BFC root or not. To do this, it needs to also pass in nsStyleDisplay, to detect whether the frame is a block. It also tries to skip reconstruction in cases where the block frame is already forced to be a BFC root by a different property.

nsStyleStruct::CalcDifference()
diff --git a/layout/style/nsStyleStruct.cpp b/layout/style/nsStyleStruct.cpp
--- a/layout/style/nsStyleStruct.cpp
+++ b/layout/style/nsStyleStruct.cpp
@@ -1142,25 +1142,47 @@ static bool IsAutonessEqual(const StyleR
       return false;
     }
   }
   return true;
 }

 nsChangeHint nsStylePosition::CalcDifference(
     const nsStylePosition& aNewData,
-    const nsStyleVisibility& aOldStyleVisibility) const {
+    const nsStyleVisibility& aOldStyleVisibility,
+    const nsStyleDisplay& aOldStyleDisplay) const {
   if (mGridTemplateColumns.IsMasonry() !=
           aNewData.mGridTemplateColumns.IsMasonry() ||
       mGridTemplateRows.IsMasonry() != aNewData.mGridTemplateRows.IsMasonry()) {
     // XXXmats this could be optimized to AllReflowHints with a bit of work,
     // but I'll assume this is a very rare use case in practice. (bug 1623886)
     return nsChangeHint_ReconstructFrame;
   }

+  if (  // alignment switched between normal / non-normal
+      ((mAlignContent.primary == mozilla::StyleAlignFlags::NORMAL) !=
+       (aNewData.mAlignContent.primary == mozilla::StyleAlignFlags::NORMAL)) &&
+      // is a block box
+      aOldStyleDisplay.IsBlockOutsideStyle() &&
+      aOldStyleDisplay.DisplayInside() == mozilla::StyleDisplayInside::Flow &&
+      // not a scroll container (which is always a flow-root anyway)
+      (aOldStyleDisplay.mOverflowX == mozilla::StyleOverflow::Visible ||
+       aOldStyleDisplay.mOverflowX == mozilla::StyleOverflow::Clip) &&
+      (aOldStyleDisplay.mOverflowY == mozilla::StyleOverflow::Visible ||
+       aOldStyleDisplay.mOverflowY == mozilla::StyleOverflow::Clip) &&
+      // not out of flow (which is always a flow-root anyway)
+      aOldStyleDisplay.mFloat == mozilla::StyleFloat::None &&
+      aOldStyleDisplay.mPosition != mozilla::StylePositionProperty::Absolute &&
+      aOldStyleDisplay.mPosition != mozilla::StylePositionProperty::Fixed) {
+    // Change in align-content causes switch between block flow / flow-root
+    return nsChangeHint_ReconstructFrame;
+    // XXX It would be best if we could not do this if we're a flex / grid item,
+    // but we can't tell because that depends on the parent display. :(
+  }
+
   nsChangeHint hint = nsChangeHint(0);

   // Changes to "z-index" require a repaint.
   if (mZIndex != aNewData.mZIndex) {
     hint |= nsChangeHint_RepaintFrame;
   }

   // Changes to "object-fit" & "object-position" require a repaint.  They

To support this, the nsStyleStruct.h is also altered to pass nsStyleDisplay:

nsStyleStruct.h/CalcDifference()
diff --git a/layout/style/nsStyleStruct.h b/layout/style/nsStyleStruct.h
--- a/layout/style/nsStyleStruct.h
+++ b/layout/style/nsStyleStruct.h
@@ -713,19 +713,19 @@ struct MOZ_NEEDS_MEMMOVABLE_MEMBERS nsSt
   using StyleAlignSelf = mozilla::StyleAlignSelf;
   using StyleJustifySelf = mozilla::StyleJustifySelf;

   explicit nsStylePosition(const mozilla::dom::Document&);
   nsStylePosition(const nsStylePosition& aOther);
   ~nsStylePosition();
   static constexpr bool kHasTriggerImageLoads = false;

-  nsChangeHint CalcDifference(
-      const nsStylePosition& aNewData,
-      const nsStyleVisibility& aOldStyleVisibility) const;
+  nsChangeHint CalcDifference(const nsStylePosition& aNewData,
+                              const nsStyleVisibility& aOldStyleVisibility,
+                              const nsStyleDisplay& aStyleDisplay) const;

   // Returns whether we need to compute an hypothetical position if we were
   // absolutely positioned.
   bool NeedsHypotheticalPositionIfAbsPos() const {
     return (mOffset.Get(mozilla::eSideRight).IsAuto() &&
             mOffset.Get(mozilla::eSideLeft).IsAuto()) ||
            (mOffset.Get(mozilla::eSideTop).IsAuto() &&
             mOffset.Get(mozilla::eSideBottom).IsAuto());

And so is the template in ComputedStyle.cpp:

ComputedStyle.cpp
diff --git a/layout/style/ComputedStyle.cpp b/layout/style/ComputedStyle.cpp
--- a/layout/style/ComputedStyle.cpp
+++ b/layout/style/ComputedStyle.cpp
@@ -162,17 +162,18 @@ nsChangeHint ComputedStyle::CalcStyleDif
   DO_STRUCT_DIFFERENCE(Outline);
   DO_STRUCT_DIFFERENCE(TableBorder);
   DO_STRUCT_DIFFERENCE(Table);
   DO_STRUCT_DIFFERENCE(UIReset);
   DO_STRUCT_DIFFERENCE(Text);
   DO_STRUCT_DIFFERENCE_WITH_ARGS(List, (, *StyleDisplay()));
   DO_STRUCT_DIFFERENCE(SVGReset);
   DO_STRUCT_DIFFERENCE(SVG);
-  DO_STRUCT_DIFFERENCE_WITH_ARGS(Position, (, *StyleVisibility()));
+  DO_STRUCT_DIFFERENCE_WITH_ARGS(Position,
+                                 (, *StyleVisibility(), *StyleDisplay()));
   DO_STRUCT_DIFFERENCE(Font);
   DO_STRUCT_DIFFERENCE(Margin);
   DO_STRUCT_DIFFERENCE(Padding);
   DO_STRUCT_DIFFERENCE(Border);
   DO_STRUCT_DIFFERENCE(TextReset);
   DO_STRUCT_DIFFERENCE(Effects);
   DO_STRUCT_DIFFERENCE(Background);
   DO_STRUCT_DIFFERENCE(Page);

Align the Contents of nsBlockFrame

The core of the patch is the AlignContent() function, which is called at the end of nsBlockFame::Reflow, after calculating the final size of the block. It shifts the contents of the block, including floats and list marker boxes, but not absolutely-positioned elements.

nsBlockFrame::Reflow() calling AlignContent()

This segment adds the call to AlignContent() right after ComputeFinalSize, and before we reflow any absolutely-positioned boxes for which this block is a containing block (so that their static positions will be correct).

nsBlockFrame::Reflow()
diff --git a/layout/generic/nsBlockFrame.cpp b/layout/generic/nsBlockFrame.cpp
--- a/layout/generic/nsBlockFrame.cpp
+++ b/layout/generic/nsBlockFrame.cpp
@@ -1608,16 +1608,19 @@ void nsBlockFrame::Reflow(nsPresContext*
   }

   CheckFloats(state);

   // Compute our final size
   nscoord blockEndEdgeOfChildren;
   ComputeFinalSize(aReflowInput, state, aMetrics, &blockEndEdgeOfChildren);

+  // Align content
+  AlignContent(state, aMetrics, &blockEndEdgeOfChildren);
+
   // If the block direction is right-to-left, we need to update the bounds of
   // lines that were placed relative to mContainerSize during reflow, as
   // we typically do not know the true container size until we've reflowed all
   // its children. So we use a dummy mContainerSize during reflow (see
   // BlockReflowState's constructor) and then fix up the positions of the
   // lines here, once the final block size is known.
   //
   // Note that writing-mode:vertical-rl is the only case where the block

nsBlockFrame::AlignContent()

This segment inserts the definition of nsBlockFrame::AlignContent() right after the definition of ComputeFinalSize() (upon whose calculations it depends).

The premise of this function is very simple:

  • If the block is start-aligned, it short-circuits and returns.
  • If the block is end- or center-aligned, it calculates a shift value from the distance between the end of the block’s contents and the bottom edge of the block, and then loops through the block’s contents to apply the shift.

There's some fancy caching happening through aState.mAlignContentShift; ignore this value (assume it’s zero) on your first read-through. We’ll get back to it later.

nsBlockFrame::AlignContent()
@@ -2159,16 +2167,101 @@ void nsBlockFrame::ComputeFinalSize(cons
   if ((ABSURD_SIZE(aMetrics.Width()) || ABSURD_SIZE(aMetrics.Height())) &&
       !GetParent()->IsAbsurdSizeAssertSuppressed()) {
     ListTag(stdout);
     printf(": WARNING: desired:%d,%d\n", aMetrics.Width(), aMetrics.Height());
   }
 #endif
 }

+void nsBlockFrame::AlignContent(BlockReflowState& aState,
+                                ReflowOutput& aMetrics,
+                                nscoord* aBEndEdgeOfChildren) {
+  if (!HasAllStateBits(NS_BLOCK_FORMATTING_CONTEXT_STATE_BITS))
+    return;  // non-BFC can't have been aligned
+
+  StyleAlignFlags alignment = StylePosition()->mAlignContent.primary;
+  alignment &= ~StyleAlignFlags::FLAG_BITS;
+
+  // Short circuit
+  bool isCentered = alignment == mozilla::StyleAlignFlags::CENTER ||
+                    alignment == mozilla::StyleAlignFlags::SPACE_AROUND ||
+                    alignment == mozilla::StyleAlignFlags::SPACE_EVENLY;
+  bool isEndAlign = alignment == mozilla::StyleAlignFlags::END ||
+                    alignment == mozilla::StyleAlignFlags::FLEX_END ||
+                    alignment == mozilla::StyleAlignFlags::LAST_BASELINE;
+  if (!(isEndAlign) && !isCentered    // desired shift = 0
+      && !aState.mAlignContentShift)  // no cached shift to undo
+    return;
+
+  // NOTE: ComputeFinalSize already called aState.UndoAlignContentShift(),
+  //       so metrics no longer include cached shift.
+  // NOTE: Content is currently positioned at cached shift
+  // NOTE: Content has been fragmented against 0-shift assumption.
+
+  // Calculate shift
+  nscoord shift = 0;
+  WritingMode wm = aState.mReflowInput.GetWritingMode();
+  if ((isCentered || isEndAlign) && !mLines.empty() &&
+      aState.mReflowStatus.IsFullyComplete() && !GetPrevInFlow()) {
+    nscoord availB = aState.mReflowInput.AvailableBSize();
+    nscoord endB = aMetrics.BSize(wm) - aState.BorderPadding().BEnd(wm);
+    shift = std::min(availB, endB) - (*aBEndEdgeOfChildren);
+
+    // note: these measures all include start BP, so it subtracts out
+    if (!(StylePosition()->mAlignContent.primary &
+          mozilla::StyleAlignFlags::UNSAFE)) {
+      shift = std::max(0, shift);
+    }
+    if (isCentered) {
+      shift = shift / 2;
+    }
+
+    // Handle limitations due to fragmentation
+    if (availB != NS_UNCONSTRAINEDSIZE) {
+      nscoord maxShift = availB - aState.mOverflowBCoord;
+      if (shift > maxShift) {
+        // avoid needing overflow fragmentation
+        shift = maxShift;
+        // trigger reflow if availB increases
+        aState.mFlags.mHasAlignContentOverflow = true;
+      }
+    }
+    // XXX Should also floor negative shift by distance to top of page
+    //     so it doesn't get clipped when printing.
+  }
+  // else: zero shift if start-aligned or if fragmented
+
+  nscoord delta = shift - aState.mAlignContentShift;
+  if (delta) {
+    // Shift children
+    LogicalPoint translation(wm, 0, delta);
+    for (LineIterator line = LinesBegin(), line_end = LinesEnd();
+         line != line_end; ++line) {
+      SlideLine(aState, line, delta);
+    }
+    for (nsIFrame* kid = GetChildList(FrameChildListID::Float).FirstChild();
+         kid; kid = kid->GetNextSibling()) {
+      kid->MovePositionBy(wm, translation);
+      nsContainerFrame::PlaceFrameView(kid);
+    }
+    if (HasOutsideMarker() && !mLines.empty()) {
+      nsIFrame* marker = GetOutsideMarker();
+      marker->MovePositionBy(wm, translation);
+    }
+
+    // Cache shift
+    SetProperty(AlignContentShift(), shift);
+  }
+
+  if (!shift) {
+    RemoveProperty(AlignContentShift());
+  }
+}
+
 void nsBlockFrame::ConsiderBlockEndEdgeOfChildren(
     OverflowAreas& aOverflowAreas, nscoord aBEndEdgeOfChildren,
     const nsStyleDisplay* aDisplay) const {
   const auto wm = GetWritingMode();

   // Factor in the block-end edge of the children.  Child frames will be added
   // to the overflow area as we iterate through the lines, but their margins
   // won't, so we need to account for block-end margins here.

And of course, we also need to declare the new function in nsBlockFrame.h. (Ignore the property declarations for now, we’ll get back to them later.)

nsBlockFrame.h/AlignContent()
diff --git a/layout/generic/nsBlockFrame.h b/layout/generic/nsBlockFrame.h
--- a/layout/generic/nsBlockFrame.h
+++ b/layout/generic/nsBlockFrame.h
@@ -481,16 +481,26 @@ class nsBlockFrame : public nsContainerF
   // helper for SlideLine and UpdateLineContainerSize
   void MoveChildFramesOfLine(nsLineBox* aLine, nscoord aDeltaBCoord);

   void ComputeFinalSize(const ReflowInput& aReflowInput,
                         BlockReflowState& aState, ReflowOutput& aMetrics,
                         nscoord* aBEndEdgeOfChildren);

   /**
+   * Calculates the necessary shift to honor 'align-content' and applies it.
+   */
+  void AlignContent(BlockReflowState& aState, ReflowOutput& aMetrics,
+                    nscoord* aBEndEdgeOfChildren);
+  // Stash the effective align-content shift value between reflows
+  NS_DECLARE_FRAME_PROPERTY_SMALL_VALUE(AlignContentShift, nscoord)
+  // Trigger reflow when alignment was limited due to limited AvailableBSize
+  NS_DECLARE_FRAME_PROPERTY_SMALL_VALUE(AlignContentOverflow, bool)
+
+  /**
    * Helper method for Reflow(). Computes the overflow areas created by our
    * children, and includes them into aOverflowAreas.
    */
   void ComputeOverflowAreas(mozilla::OverflowAreas& aOverflowAreas,
                             nscoord aBEndEdgeOfChildren,
                             const nsStyleDisplay* aDisplay) const;

   /**

Avoid Complications Due to Fragmentation

This patch is fragmentation-aware, in that it can cope with fragmentation even in dynamic contexts like resizes of or within a multi-column element. However, it’s approach to fragmentation is very dumb:

  • It bails out of alignment if the block or its contents are fragmented (aState.mReflowStatus.IsFullyComplete() && !GetPrevInFlow()).
  • It limits any shift values that would push its contents to overflow the fragmentainer boundary (and thus require them to be fragmented).
nsBlockFrame::AlignContent() Review
+  // Calculate shift
+  nscoord shift = 0;
+  WritingMode wm = aState.mReflowInput.GetWritingMode();
+  if ((isCentered || isEndAlign) && !mLines.empty() &&
+      aState.mReflowStatus.IsFullyComplete() && !GetPrevInFlow()) {
...
+    // Handle limitations due to fragmentation
+    if (availB != NS_UNCONSTRAINEDSIZE) {
+      nscoord maxShift = availB - aState.mOverflowBCoord;
+      if (shift > maxShift) {
+        // avoid needing overflow fragmentation
+        shift = maxShift;
+        // trigger reflow if availB increases
+        aState.mFlags.mHasAlignContentOverflow = true;
+      }
+    }
+    // XXX Should also floor negative shift by distance to top of page
+    //     so it doesn't get clipped when printing.
+  }
+  // else: zero shift if start-aligned or if fragmented

Tracking overflow bounds with BlockReflowState.mOverflowBCoord

In order to reference the limits of the block’s potentially overflowing content, we introduce BlockReflowState.mOverflowBCoord to track the bottommost limit of its scrollable overflow.

This segment declares mOverflowBCoord as a member of BlockReflowState:

nsBlockReflowState.h/mOverflowBCoord
@@ -338,16 +357,19 @@ class BlockReflowState {
   // which we know is adjacent to the top of the block (in other words,
   // all lines before it are empty and do not have clearance. This line is
   // always before the current line.
   nsLineList::iterator mLineAdjacentToTop;

   // The current block-direction coordinate in the block
   nscoord mBCoord;

+  // The farthest child overflow coordinate in the block so far
+  nscoord mOverflowBCoord;
+
   // mBlock's computed logical border+padding with pre-reflow skip sides applied
   // (See the constructor and nsIFrame::PreReflowBlockLevelLogicalSkipSides).
   LogicalMargin mBorderPadding;

   // The overflow areas of all floats placed so far
   OverflowAreas mFloatOverflowAreas;

   // Previous child. This is used when pulling up a frame to update

This segment increases mOverflowBCoord to include child scrollable overflow bounds on a line whose contents are not being reflowed this time:

nsBlockFrame::ReflowDirtyLines()
@@ -3022,16 +3107,22 @@ void nsBlockFrame::ReflowDirtyLines(Bloc
       lastLineMovedUp = deltaBCoord < 0;

       if (deltaBCoord != 0) {
         SlideLine(aState, line, deltaBCoord);
       } else {
         repositionViews = true;
       }

+      mozilla::LogicalRect overflowArea =
+          line->GetOverflowArea(mozilla::OverflowType::Scrollable,
+                                line->mWritingMode, aState.mContainerSize);
+      aState.mOverflowBCoord = std::max(aState.mOverflowBCoord,
+                                        overflowArea.BEnd(line->mWritingMode));
+
       NS_ASSERTION(!line->IsDirty() || !line->HasFloats(),
                    "Possibly stale float cache here!");
       if (willReflowAgain && line->IsBlock()) {
         // If we're going to reflow everything again, and this line is a block,
         // then there is no need to recover float state. The line may contain
         // other lines with floats, but in that case RecoverStateFrom would only
         // add floats to the float manager. We don't need to do that because
         // everything's going to get reflowed again "for real". Calling

This segment increases mOverflowBCoord to include child scrollable overflow bounds on a line whose contents are being reflowed this time:

nsBlockFrame::ReflowLine()
@@ -3444,16 +3525,22 @@ void nsBlockFrame::ReflowLine(BlockReflo
           // This line is overlapping a float - store the edges marking the area
           // between the floats for text-overflow analysis.
           aLine->SetFloatEdges(s, e);
         }
       }
     }
   }

+  mozilla::LogicalRect overflowArea =
+      aLine->GetOverflowArea(mozilla::OverflowType::Scrollable,
+                             aLine->mWritingMode, aState.mContainerSize);
+  aState.mOverflowBCoord =
+      std::max(aState.mOverflowBCoord, overflowArea.BEnd(aLine->mWritingMode));
+
   aLine->ClearMovedFragments();
 }

 nsIFrame* nsBlockFrame::PullFrame(BlockReflowState& aState,
                                   LineIterator aLine) {
   // First check our remaining lines.
   if (LinesEnd() != aLine.next()) {
     return PullFrameFrom(aLine, this, aLine.next());

Tracking reflow needs with AlignContentOverflow

Remember that in AlignContent() we are limiting the amount of shift in order to prevent overflowing content from overflowing the edge of the fragmentainer:

nsBlockFrame::AlignContent() Review
+    // Handle limitations due to fragmentation
+    if (availB != NS_UNCONSTRAINEDSIZE) {
+      nscoord maxShift = availB - aState.mOverflowBCoord;
+      if (shift > maxShift) {
+        // avoid needing overflow fragmentation
+        shift = maxShift;
+        // trigger reflow if availB increases
+        aState.mFlags.mHasAlignContentOverflow = true;
+      }
+    }

For this to work correctly, we need to trigger reflow not only when ReflowInput.AvailableBSize decreases such that the box’s scrollable overflow no longer fits, but also when ReflowInput.AvailableBSize increases such that we need to adjust how much we limit the alignment shift!

To do this, we track boxes that might need this special reflow by flagging them similar to how we flag boxes that need special reflow due to containing descendants with clear. In fact, we re-use the exact same mechanism for this.

This segment renames the relevant frame state bit:

nsFrameStatebits.h
diff --git a/layout/generic/nsFrameStateBits.h b/layout/generic/nsFrameStateBits.h
--- a/layout/generic/nsFrameStateBits.h
+++ b/layout/generic/nsFrameStateBits.h
@@ -536,20 +536,18 @@ FRAME_STATE_BIT(Block, 23, NS_BLOCK_FLOA
 (NS_BLOCK_FLOAT_MGR | NS_BLOCK_MARGIN_ROOT)

 FRAME_STATE_BIT(Block, 24, NS_BLOCK_HAS_LINE_CURSOR)

 FRAME_STATE_BIT(Block, 25, NS_BLOCK_HAS_OVERFLOW_LINES)

 FRAME_STATE_BIT(Block, 26, NS_BLOCK_HAS_OVERFLOW_OUT_OF_FLOWS)

-// Set on any block that has descendant frames in the normal
-// flow with 'clear' set to something other than 'none'
-// (including <BR CLEAR="..."> frames)
-FRAME_STATE_BIT(Block, 27, NS_BLOCK_HAS_CLEAR_CHILDREN)
+// Set on any block that needs unoptimized reflow for itself or a descendant
+FRAME_STATE_BIT(Block, 27, NS_BLOCK_HAS_SPECIAL_CHILDREN)

 // NS_BLOCK_CLIP_PAGINATED_OVERFLOW is only set in paginated prescontexts, on
 // blocks which were forced to not have scrollframes but still need to clip
 // the display of their kids.
 FRAME_STATE_BIT(Block, 28, NS_BLOCK_CLIP_PAGINATED_OVERFLOW)

 // NS_BLOCK_HAS_FIRST_LETTER_STYLE means that the block has first-letter style,
 // even if it has no actual first-letter frame among its descendants.

Because it's now serving two purposes, we need more information to distinguish them, so we declare two distinct frame properties for this purpose:

This segment declares the frame property HasClearChildren:

nsBlockFrame.h/HasClearChildren
@@ -682,16 +692,21 @@ class nsBlockFrame : public nsContainerF
   /** set up the conditions necessary for an resize reflow
    * the primary task is to mark the minimumly sufficient lines dirty.
    */
   void PrepareResizeReflow(BlockReflowState& aState);

   /** reflow all lines that have been marked dirty */
   void ReflowDirtyLines(BlockReflowState& aState);

+  // Set on any block that has descendant frames in the normal
+  // flow with 'clear' set to something other than 'none'
+  // (including <BR CLEAR="..."> frames)
+  NS_DECLARE_FRAME_PROPERTY_SMALL_VALUE(HasClearChildren, bool)
+
   /** Mark a given line dirty due to reflow being interrupted on or before it */
   void MarkLineDirtyForInterrupt(nsLineBox* aLine);

   //----------------------------------------
   // Methods for line reflow
   /**
    * Reflow a line.
    *

This segment declares the frame property AlignContentOverflow:

nsBlockFrame.h Review AlignContentOverflow
   /**
+   * Calculates the necessary shift to honor 'align-content' and applies it.
+   */
+  void AlignContent(BlockReflowState& aState, ReflowOutput& aMetrics,
+                    nscoord* aBEndEdgeOfChildren);
+  // Stash the effective align-content shift value between reflows
+  NS_DECLARE_FRAME_PROPERTY_SMALL_VALUE(AlignContentShift, nscoord)
+  // Trigger reflow when alignment was limited due to limited AvailableBSize
+  NS_DECLARE_FRAME_PROPERTY_SMALL_VALUE(AlignContentOverflow, bool)

To track these statuses more easily, we set up some infrastructure in BlockReflowState.

This segment declares and initializes the BockReflowState::Flags mHasClearChildren and mHasAlignContentOverflow:

BlockReflowState.h/mHasClearChildren, mHasAlignContentOveflow
diff --git a/layout/generic/BlockReflowState.h b/layout/generic/BlockReflowState.h
--- a/layout/generic/BlockReflowState.h
+++ b/layout/generic/BlockReflowState.h
@@ -33,16 +33,18 @@ class BlockReflowState {
     Flags()
         : mIsBStartMarginRoot(false),
           mIsBEndMarginRoot(false),
           mShouldApplyBStartMargin(false),
           mHasLineAdjacentToTop(false),
           mBlockNeedsFloatManager(false),
           mIsLineLayoutEmpty(false),
           mIsFloatListInBlockPropertyTable(false),
+          mHasClearChildren(false),
+          mHasAlignContentOverflow(false),
           mCanHaveOverflowMarkers(false) {}

     // Set in the BlockReflowState constructor when reflowing a "block margin
     // root" frame (i.e. a frame with the NS_BLOCK_MARGIN_ROOT flag set, for
     // which margins apply by default).
     //
     // The flag is also set when reflowing a frame whose computed BStart border
     // padding is non-zero.
@@ -84,28 +86,43 @@ class BlockReflowState {

     // Set when nsLineLayout::LineIsEmpty was true at the end of reflowing
     // the current line.
     bool mIsLineLayoutEmpty : 1;

     // Set when our mPushedFloats list is stored on the block's property table.
     bool mIsFloatListInBlockPropertyTable : 1;

+    // Set when we have descendants with 'clear'
+    bool mHasClearChildren : 1;
+
+    // Set when we truncated alignment to avoid overflowing the fragmentainer
+    bool mHasAlignContentOverflow : 1;
+
     // Set when we need text-overflow or -webkit-line-clamp processing.
     bool mCanHaveOverflowMarkers : 1;
   };

This segment defines the BlockReflowState destructor to manage setting and removing the NS_BLOCK_HAS_SPECIAL_CHILDREN frame bit and the HasClearChildren and AlignContentOverflow frame properties based on the BlockReflowState’s mHasClearChildren and mHasAlignContentOverflowflags.

BlockReflowState::~BlockReflowState()
+BlockReflowState::~BlockReflowState() {
+  // annotate conditions that require Reflow to inspect deeper
+  if (mFlags.mHasClearChildren || mFlags.mHasAlignContentOverflow) {
+    // this lookup is frequent, so we note it in frame bits
+    mBlock->AddStateBits(NS_BLOCK_HAS_SPECIAL_CHILDREN);
+    // positive cases are rare, so distinguish in frame properties
+    if (mFlags.mHasClearChildren)
+      mBlock->SetProperty(nsBlockFrame::HasClearChildren(), true);
+    if (mFlags.mHasAlignContentOverflow)
+      mBlock->SetProperty(nsBlockFrame::AlignContentOverflow(), true);
+  } else if (mBlock->HasAnyStateBits(NS_BLOCK_HAS_SPECIAL_CHILDREN)) {
+    mBlock->RemoveStateBits(NS_BLOCK_HAS_SPECIAL_CHILDREN);
+    mBlock->RemoveProperty(nsBlockFrame::HasClearChildren());
+    mBlock->RemoveProperty(nsBlockFrame::AlignContentOverflow());
+  }
+}
+

And now we update the nsBlockFrame call sites to use the new flag system, refactoring the LineHasClear() pattern to use a new BlockReflowState::CaptureSpecialReflowNeeds() function.

This segment updates the nsBlockFrame::ReflowDirtyLines() to use the new special reflow system:

nsBlockFrame::ReflowDirtyLines()
@@ -2614,17 +2698,18 @@ static bool LinesAreEmpty(const nsLineLi
     }
   }
   return true;
 }

 void nsBlockFrame::ReflowDirtyLines(BlockReflowState& aState) {
   bool keepGoing = true;
   bool repositionViews = false;  // should we really need this?
-  bool foundAnyClears = aState.mTrailingClearFromPIF != StyleClear::None;
+  aState.mFlags.mHasClearChildren =
+      aState.mTrailingClearFromPIF != StyleClear::None;
   bool willReflowAgain = false;

 #ifdef DEBUG
   if (gNoisyReflow) {
     IndentBy(stdout, gNoiseIndent);
     ListTag(stdout);
     printf(": reflowing dirty lines");
     printf(" computedISize=%d\n", aState.mReflowInput.ComputedISize());
@@ -2708,17 +2793,17 @@ void nsBlockFrame::ReflowDirtyLines(Bloc
     if (selfDirty) {
       line->MarkDirty();
     }

     // This really sucks, but we have to look inside any blocks that have clear
     // elements inside them.
     // XXX what can we do smarter here?
     if (!line->IsDirty() && line->IsBlock() &&
-        line->mFirstChild->HasAnyStateBits(NS_BLOCK_HAS_CLEAR_CHILDREN)) {
+        line->mFirstChild->HasAnyStateBits(NS_BLOCK_HAS_SPECIAL_CHILDREN)) {
       line->MarkDirty();
     }

     nsIFrame* floatAvoidingBlock = nullptr;
     if (line->IsBlock() &&
         !nsBlockFrame::BlockCanIntersectFloats(line->mFirstChild)) {
       floatAvoidingBlock = line->mFirstChild;
     }
@@ -3069,19 +3160,17 @@ void nsBlockFrame::ReflowDirtyLines(Bloc
     // Record if we need to clear floats before reflowing the next
     // line. Note that inlineFloatClearType will be handled and
     // cleared before the next line is processed, so there is no
     // need to combine break types here.
     if (line->HasFloatClearTypeAfter()) {
       inlineFloatClearType = line->FloatClearTypeAfter();
     }

-    if (LineHasClear(line.get())) {
-      foundAnyClears = true;
-    }
+    aState.CaptureSpecialReflowNeeds(line.get());

     DumpLine(aState, line, deltaBCoord, -1);

     if (aState.mPresContext->HasPendingInterrupt()) {
       willReflowAgain = true;
       // Another option here might be to leave |line| clean if
       // !HasPendingInterrupt() before the CheckForInterrupt() call, since in
       // that case the line really did reflow as it should have.  Not sure
@@ -3250,19 +3339,17 @@ void nsBlockFrame::ReflowDirtyLines(Bloc
           DumpLine(aState, line, deltaBCoord, -1);
           if (!keepGoing) {
             if (0 == line->GetChildCount()) {
               DeleteLine(aState, line, line_end);
             }
             break;
           }

-          if (LineHasClear(line.get())) {
-            foundAnyClears = true;
-          }
+          aState.CaptureSpecialReflowNeeds(line.get());

           if (aState.mPresContext->CheckForInterrupt(this)) {
             MarkLineDirtyForInterrupt(line);
             break;
           }

           // If this is an inline frame then its time to stop
           ++line;
@@ -3321,22 +3408,16 @@ void nsBlockFrame::ReflowDirtyLines(Bloc
       }
     }
   }

   if (LinesAreEmpty(mLines) && ShouldHaveLineIfEmpty()) {
     aState.mBCoord += aState.mMinLineHeight;
   }

-  if (foundAnyClears) {
-    AddStateBits(NS_BLOCK_HAS_CLEAR_CHILDREN);
-  } else {
-    RemoveStateBits(NS_BLOCK_HAS_CLEAR_CHILDREN);
-  }
-
 #ifdef DEBUG
   VerifyLines(true);
   VerifyOverflowSituation();
   if (gNoisyReflow) {
     IndentBy(stdout, gNoiseIndent - 1);
     ListTag(stdout);
     printf(": done reflowing dirty lines (status=%s)\n",
            ToString(aState.mReflowStatus).c_str());

This segment removes the old LineHasClear() helper function:

nsBlockFrame.cpp/LineHasClear()
@@ -2553,25 +2646,16 @@ void nsBlockFrame::PropagateFloatDamage(
       // changes, but that's not straightforward to check
       if (wasImpactedByFloat || floatAvailableSpace.HasFloats()) {
         aLine->MarkDirty();
       }
     }
   }
 }

-static bool LineHasClear(nsLineBox* aLine) {
-  return aLine->IsBlock()
-             ? (aLine->HasForcedLineBreakBefore() ||
-                aLine->mFirstChild->HasAnyStateBits(
-                    NS_BLOCK_HAS_CLEAR_CHILDREN) ||
-                !nsBlockFrame::BlockCanIntersectFloats(aLine->mFirstChild))
-             : aLine->HasFloatClearTypeAfter();
-}
-
 /**
  * Reparent a whole list of floats from aOldParent to this block.  The
  * floats might be taken from aOldParent's overflow list. They will be
  * removed from the list. They end up appended to our mFloats list.
  */
 void nsBlockFrame::ReparentFloats(nsIFrame* aFirstFrame,
                                   nsBlockFrame* aOldParent,
                                   bool aReparentSiblings) {

This segment defines the new CaptureSpecialReflowNeeds() helper function:

BlockReflowState::CaptureSpecialReflowNeeds()
+
+void BlockReflowState::CaptureSpecialReflowNeeds(nsLineBox* aLine) {
+  if (aLine->IsBlock()) {
+    nsIFrame* child = aLine->mFirstChild;
+    bool hasClear = false;
+    if (child->HasAnyStateBits(NS_BLOCK_HAS_SPECIAL_CHILDREN)) {
+      mFlags.mHasAlignContentOverflow =
+          child->GetProperty(nsBlockFrame::AlignContentOverflow());
+      hasClear = child->GetProperty(nsBlockFrame::HasClearChildren());
+    }
+    mFlags.mHasClearChildren = hasClear || aLine->HasForcedLineBreakBefore() ||
+                               !nsBlockFrame::BlockCanIntersectFloats(child);
+  } else {
+    mFlags.mHasClearChildren = aLine->HasFloatClearTypeAfter();
+  }
+}

This segment declares the new CaptureSpecialReflowNeeds() helper function in the BlockReflowState class definition:

BlockReflowState.h/CaptureSpecialReflowNeeds()
@@ -232,16 +249,18 @@ class BlockReflowState {
   void AdvanceToNextLine() {
     if (mFlags.mIsLineLayoutEmpty) {
       mFlags.mIsLineLayoutEmpty = false;
     } else {
       mLineNumber++;
     }
   }

+  void CaptureSpecialReflowNeeds(nsLineBox* aLine);
+
   //----------------------------------------

   // This state is the "global" state computed once for the reflow of
   // the block.

   // The block frame that is using this object
   nsBlockFrame* mBlock;

Cache the Alignment Shift

Since we don't want to re-position all the frames twice (which could affect thousands of children) every time we run nsBlockFrame::Reflow(), we start reflow by assuming that the alignment shift this time is going to be the same alignment shift as last time. This way, if nothing else changed in the block axis, then none of the children move at all. So we need to start layout at the shifted start position, rather than at the content edge of the block.

However if something changed, we need to recalculate the shift and move the boxes accordingly. Remember that the AlignContent() function handles this by calculating the delta and shifting things accordingly.

Also if the fragmentation limit (AvailableBSize) changed, we need to make sure we fragment against the new limit from an unshifted position, even though we're positioned at a shifted position!

To manage all this, we introduce a new frame property, AlignContentShift, to cache the shift amount; and some infrastructure in BlockReflowState to manage it. First, BlockReflowState applies the shift to all of our reflow coordinate tracking in its constructor; then it provides a rewind function which is called by ComputeFinalSize() so that it can actually compute an accurate final size (which is then inputted into AlignContent() to calculate the shift value).

To do this, we need a mutable ReflowInput (because we need to modify its AvailableBSize); this is why the earlier supporting patch in the series does some refactoring in nsBlockFrame::Reflow() to make that more straightforward to manage.

This segment initializes BlockReflowState::mAlignContentShift and defines the BlockReflowState::UndoAlignContentShift() helper function:

BlockReflowState::BlockReflowState() and BlockReflowState::UndoAlignContentShift()
diff --git a/layout/generic/BlockReflowState.cpp b/layout/generic/BlockReflowState.cpp
--- a/layout/generic/BlockReflowState.cpp
+++ b/layout/generic/BlockReflowState.cpp
@@ -43,17 +43,18 @@ BlockReflowState::BlockReflowState(const
       mBorderPadding(
           mReflowInput
               .ComputedLogicalBorderPadding(mReflowInput.GetWritingMode())
               .ApplySkipSides(aFrame->PreReflowBlockLevelLogicalSkipSides())),
       mPrevBEndMargin(),
       mMinLineHeight(aReflowInput.GetLineHeight()),
       mLineNumber(0),
       mTrailingClearFromPIF(StyleClear::None),
-      mConsumedBSize(aConsumedBSize) {
+      mConsumedBSize(aConsumedBSize),
+      mAlignContentShift(0) {
   NS_ASSERTION(mConsumedBSize != NS_UNCONSTRAINEDSIZE,
                "The consumed block-size should be constrained!");

   WritingMode wm = aReflowInput.GetWritingMode();

   // Note that mContainerSize is the physical size, needed to
   // convert logical block-coordinates in vertical-rl writing mode
   // (measured from a RHS origin) to physical coordinates within the
@@ -130,20 +131,88 @@ BlockReflowState::BlockReflowState(const
   } else {
     // When we are not in a paginated situation, then we always use a
     // unconstrained block-size.
     mContentArea.BSize(wm) = NS_UNCONSTRAINEDSIZE;
   }
   mContentArea.IStart(wm) = mBorderPadding.IStart(wm);
   mBCoord = mContentArea.BStart(wm) = mBorderPadding.BStart(wm);

+  if (mBlock->HasAllStateBits(NS_BLOCK_FORMATTING_CONTEXT_STATE_BITS)) {
+    // We can skip this lookup on non-BFC frames; they can't be aligned.
+
+    // Cache the lookup
+    mAlignContentShift = aFrame->GetProperty(nsBlockFrame::AlignContentShift());
+
+    // Account for cached shift ; re-position in AlignContent() if needed
+    if (mAlignContentShift) {
+      mBCoord += mAlignContentShift;
+      mOverflowBCoord += mAlignContentShift;
+      mContentArea.BStart(wm) += mAlignContentShift;
+
+      if (mReflowInput.AvailableBSize() != NS_UNCONSTRAINEDSIZE) {
+        const_cast<ReflowInput&>(mReflowInput)
+            .SetAvailableBSize(mReflowInput.AvailableBSize() +
+                               mAlignContentShift);
+        mContentArea.BSize(wm) += mAlignContentShift;
+      }
+    }
+  }
+
   mPrevChild = nullptr;
   mCurrentLine = aFrame->LinesEnd();
 }

+void BlockReflowState::UndoAlignContentShift() {
+  if (!mAlignContentShift) return;
+
+  mBCoord -= mAlignContentShift;
+  mOverflowBCoord -= mAlignContentShift;
+  mContentArea.BStart(mReflowInput.GetWritingMode()) -= mAlignContentShift;
+
+  if (mReflowInput.AvailableBSize() != NS_UNCONSTRAINEDSIZE) {
+    const_cast<ReflowInput&>(mReflowInput)
+        .SetAvailableBSize(mReflowInput.AvailableBSize() - mAlignContentShift);
+    mContentArea.BSize(mReflowInput.GetWritingMode()) -= mAlignContentShift;
+  }
+}

This segment declares the BlockReflowState::UndoAlignContentShift() helper function:

BlockReflowState.h/UndoAlignContentShift()
  public:
   BlockReflowState(const ReflowInput& aReflowInput, nsPresContext* aPresContext,
                    nsBlockFrame* aFrame, bool aBStartMarginRoot,
                    bool aBEndMarginRoot, bool aBlockNeedsFloatManager,
                    const nscoord aConsumedBSize,
                    const nscoord aEffectiveContentBoxBSize);

   /**
+   * Unshifts coords, restores availableBSize to reality.
+   * (Constructor applies any cached shift before reflow
+   *  so that frames are reflowed with cached shift)
+   */
+  void UndoAlignContentShift();
+
+  ~BlockReflowState();
+
+  /**
    * Get the available reflow space (the area not occupied by floats)
    * for the current y coordinate. The available space is relative to
    * our coordinate system, which is the content box, with (0, 0) in the
    * upper left.
    *
    * Returns whether there are floats present at the given block-direction
    * coordinate and within the inline size of the content rect.
    *

This segment declares BlockReflowState::mAlignContentShift:

BlockReflowState.h/mAlignContentShift
@@ -392,16 +414,21 @@ class BlockReflowState {
   // Cache the result of nsBlockFrame::FindTrailingClear() from mBlock's
   // prev-in-flows. See nsBlockFrame::ReflowPushedFloats().
   StyleClear mTrailingClearFromPIF;

   // The amount of computed content block-size "consumed" by our previous
   // continuations.
   const nscoord mConsumedBSize;

+  // The amount of block-axis alignment shift to assume during reflow.
+  // Cached between reflows in the AlignContentShift property.
+  // (This system optimizes reflow for not changing the shift.)
+  nscoord mAlignContentShift;
+
   // Cache the current line's BSize if nsBlockFrame::PlaceLine() fails to
   // place the line. When redoing the line, it will be used to query the
   // accurate float available space in AddFloat() and
   // nsBlockFrame::PlaceLine().
   Maybe<nscoord> mLineBSize;

  private:
   bool CanPlaceFloat(nscoord aFloatISize,

This segment calss BlockReflowState::UndoAlignContentShift() from halfway through ComputeFinalSize(), after we compute the blockEndEdgeOfChildren and before we compute the final metrics:

nsBlockFrame::ComputeFinalSize()
@@ -1970,16 +1973,21 @@ void nsBlockFrame::ComputeFinalSize(cons
     // block-end margin of any floated elements; e.g., inside a table cell.
     //
     // Note: The block coordinate returned by ClearFloats is always greater than
     // or equal to blockEndEdgeOfChildren.
     std::tie(blockEndEdgeOfChildren, std::ignore) =
         aState.ClearFloats(blockEndEdgeOfChildren, StyleClear::Both);
   }

+  // undo cached alignment shift for sizing purposes
+  // (we used shifted positions because the float manager uses them)
+  blockEndEdgeOfChildren -= aState.mAlignContentShift;
+  aState.UndoAlignContentShift();
+
   if (NS_UNCONSTRAINEDSIZE != aReflowInput.ComputedBSize()) {
     // Note: We don't use blockEndEdgeOfChildren because it includes the
     // previous margin.
     const nscoord contentBSizeWithBStartBP =
         aState.mBCoord + nonCarriedOutBDirMargin;

     // We don't care about ApplyLineClamp's return value (the line-clamped
     // content BSize) in this explicit-BSize codepath, but we do still need to

The rest of the handling for the cached value is in nsBlockFrame::AlignContent(), see above, this time paying attention to mAlignContentShift. :)

This concludes our tour of the patch. There are some additional notes for follow-up improvements in this file and Bug 1499443.