/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=2 et sw=2 tw=80: */ /* This Source Code is subject to the terms of the Mozilla Public License * version 2.0 (the "License"). You can obtain a copy of the License at * http://mozilla.org/MPL/2.0/. */ /* rendering object for CSS "display: grid | inline-grid" */ #include "nsGridContainerFrame.h" #include "nsCSSAnonBoxes.h" #include "nsPresContext.h" #include "nsReadableUtils.h" #include "nsStyleContext.h" using namespace mozilla; /** * Search for the aNth occurrence of aName in aNameList (forward), starting at * the zero-based aFromIndex, and return the 1-based index (line number). * Also take into account there is an unconditional match at aImplicitLine * unless it's zero. * Return the last match if aNth occurrences can't be found, or zero if no * occurrence can be found. */ static uint32_t FindLine(const nsString& aName, uint32_t aNth, uint32_t aFromIndex, uint32_t aImplicitLine, const nsTArray>& aNameList) { MOZ_ASSERT(aNth != 0); const uint32_t len = aNameList.Length(); uint32_t lastFound = 0; uint32_t line; uint32_t i = aFromIndex; for (; i < len; i = line) { line = i + 1; if (line == aImplicitLine || aNameList[i].Contains(aName)) { lastFound = line; if (--aNth == 0) { return lastFound; } } } if (aImplicitLine > i) { // aImplicitLine is after the lines we searched above so it's last. // (grid-template-areas has more tracks than grid-template-[rows|columns]) lastFound = aImplicitLine; } return lastFound; } /** * @see FindLine, this function does the same but searches in reverse. */ static uint32_t RFindLine(const nsString& aName, uint32_t aNth, uint32_t aFromIndex, uint32_t aImplicitLine, const nsTArray>& aNameList) { MOZ_ASSERT(aNth != 0); const uint32_t len = aNameList.Length(); uint32_t lastFound = 0; // The implicit line may be beyond the length of aNameList so we match this // line first if it's within the 0..aFromIndex range. if (aImplicitLine > len && aImplicitLine < aFromIndex) { lastFound = aImplicitLine; if (--aNth == 0) { return lastFound; } } uint32_t i = aFromIndex == 0 ? len : std::min(aFromIndex, len); for (; i; --i) { if (i == aImplicitLine || aNameList[i - 1].Contains(aName)) { lastFound = i; if (--aNth == 0) { break; } } } return lastFound; } static uint32_t FindNamedLine(const nsString& aName, int32_t aNth, uint32_t aFromIndex, uint32_t aImplicitLine, const nsTArray>& aNameList) { MOZ_ASSERT(aNth != 0); if (aNth > 0) { return ::FindLine(aName, aNth, aFromIndex, aImplicitLine, aNameList); } return ::RFindLine(aName, -aNth, aFromIndex, aImplicitLine, aNameList); } /** * A convenience method to lookup a name in 'grid-template-areas'. * @param aStyle the StylePosition() for the grid container * @return null if not found */ static const css::GridNamedArea* FindNamedArea(const nsSubstring& aName, const nsStylePosition* aStyle) { if (!aStyle->mGridTemplateAreas) { return nullptr; } const nsTArray& areas = aStyle->mGridTemplateAreas->mNamedAreas; size_t len = areas.Length(); for (size_t i = 0; i < len; ++i) { const css::GridNamedArea& area = areas[i]; if (area.mName == aName) { return &area; } } return nullptr; } // Return true if aString ends in aSuffix and has at least one character before // the suffix. Assign aIndex to where the suffix starts. static bool IsNameWithSuffix(const nsString& aString, const nsString& aSuffix, uint32_t* aIndex) { if (StringEndsWith(aString, aSuffix)) { *aIndex = aString.Length() - aSuffix.Length(); return *aIndex != 0; } return false; } static bool IsNameWithEndSuffix(const nsString& aString, uint32_t* aIndex) { return IsNameWithSuffix(aString, NS_LITERAL_STRING("-end"), aIndex); } static bool IsNameWithStartSuffix(const nsString& aString, uint32_t* aIndex) { return IsNameWithSuffix(aString, NS_LITERAL_STRING("-start"), aIndex); } //---------------------------------------------------------------------- // Frame class boilerplate // ======================= NS_QUERYFRAME_HEAD(nsGridContainerFrame) NS_QUERYFRAME_ENTRY(nsGridContainerFrame) NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame) NS_IMPL_FRAMEARENA_HELPERS(nsGridContainerFrame) nsContainerFrame* NS_NewGridContainerFrame(nsIPresShell* aPresShell, nsStyleContext* aContext) { return new (aPresShell) nsGridContainerFrame(aContext); } //---------------------------------------------------------------------- // nsGridContainerFrame Method Implementations // =========================================== void nsGridContainerFrame::AddImplicitNamedAreas( const nsTArray>& aLineNameLists) { // http://dev.w3.org/csswg/css-grid/#implicit-named-areas // XXX this just checks x-start .. x-end in one dimension and there's // no other error checking. A few wrong cases (maybe): // (x-start x-end) // (x-start) 0 (x-start) 0 (x-end) // (x-end) 0 (x-start) 0 (x-end) // (x-start) 0 (x-end) 0 (x-start) 0 (x-end) const uint32_t len = aLineNameLists.Length(); nsTHashtable currentStarts; ImplicitNamedAreas* areas = GetImplicitNamedAreas(); for (uint32_t i = 0; i < len; ++i) { const nsTArray& names(aLineNameLists[i]); const uint32_t jLen = names.Length(); for (uint32_t j = 0; j < jLen; ++j) { const nsString& name = names[j]; uint32_t index; if (::IsNameWithStartSuffix(name, &index)) { currentStarts.PutEntry(nsDependentSubstring(name, 0, index)); } else if (::IsNameWithEndSuffix(name, &index)) { nsDependentSubstring area(name, 0, index); if (currentStarts.Contains(area)) { if (!areas) { areas = new ImplicitNamedAreas; Properties().Set(ImplicitNamedAreasProperty(), areas); } areas->PutEntry(area); } } } } } void nsGridContainerFrame::InitImplicitNamedAreas(const nsStylePosition* aStyle) { ImplicitNamedAreas* areas = GetImplicitNamedAreas(); if (areas) { // Clear it, but reuse the hashtable itself for now. We'll remove it // below if it isn't needed anymore. areas->Clear(); } AddImplicitNamedAreas(aStyle->mGridTemplateColumns.mLineNameLists); AddImplicitNamedAreas(aStyle->mGridTemplateRows.mLineNameLists); if (areas && areas->Count() == 0) { Properties().Delete(ImplicitNamedAreasProperty()); } } uint32_t nsGridContainerFrame::ResolveLine( const nsStyleGridLine& aLine, int32_t aNth, uint32_t aFromIndex, const nsTArray>& aLineNameList, uint32_t GridNamedArea::* aAreaStart, uint32_t GridNamedArea::* aAreaEnd, uint32_t aExplicitGridEnd, LineRangeSide aSide, const nsStylePosition* aStyle) { MOZ_ASSERT(!aLine.IsAuto()); uint32_t line = 0; if (aLine.mLineName.IsEmpty()) { MOZ_ASSERT(aNth != 0, "css-grid 9.2: must not be zero."); line = std::max(int32_t(aFromIndex) + aNth, 1); } else { if (aNth == 0) { // was omitted; treat it as 1. aNth = 1; } bool isNameOnly = !aLine.mHasSpan && aLine.mInteger == 0; if (isNameOnly) { const GridNamedArea* area = ::FindNamedArea(aLine.mLineName, aStyle); if (area || HasImplicitNamedArea(aLine.mLineName)) { // The given name is a named area - look for explicit lines named // -start/-end depending on which side we're resolving. // http://dev.w3.org/csswg/css-grid/#grid-placement-slot uint32_t implicitLine = 0; nsAutoString lineName(aLine.mLineName); if (aSide == eLineRangeSideStart) { lineName.AppendLiteral("-start"); implicitLine = area ? area->*aAreaStart : 0; } else { lineName.AppendLiteral("-end"); implicitLine = area ? area->*aAreaEnd : 0; } // XXX must Implicit Named Areas have all four lines? // http://dev.w3.org/csswg/css-grid/#implicit-named-areas line = ::FindNamedLine(lineName, aNth, aFromIndex, implicitLine, aLineNameList); } } if (line == 0) { // If mLineName ends in -start/-end, try the prefix as a named area. uint32_t implicitLine = 0; uint32_t index; auto GridNamedArea::* areaEdge = aAreaStart; bool found = ::IsNameWithStartSuffix(aLine.mLineName, &index); if (!found) { found = ::IsNameWithEndSuffix(aLine.mLineName, &index); areaEdge = aAreaEnd; } if (found) { const GridNamedArea* area = ::FindNamedArea(nsDependentSubstring(aLine.mLineName, 0, index), aStyle); if (area) { implicitLine = area->*areaEdge; } } line = ::FindNamedLine(aLine.mLineName, aNth, aFromIndex, implicitLine, aLineNameList); } if (line == 0) { // No line matching exists. if (aLine.mHasSpan) { // http://dev.w3.org/csswg/css-grid/#grid-placement-span-int // Treat 'span N' as 'span N'. line = std::max(int32_t(aFromIndex) + aNth, 1); } else { // http://dev.w3.org/csswg/css-grid/#grid-placement-int // Treat ' N' as first/last line depending on N's sign. // XXX this is wrong due to a spec change, see bug 1009776 comment 17. // XXX we want to possibly expand the implicit grid here. line = aNth >= 0 ? 1 : aExplicitGridEnd; } } } // The only case which can result in "auto" (line == 0) is a plain // (without or 'span') which wasn't found. MOZ_ASSERT(line != 0 || (!aLine.mHasSpan && aLine.mInteger == 0), "Given a or 'span' the result should never be auto"); return line; } nsGridContainerFrame::LinePair nsGridContainerFrame::ResolveLineRangeHelper( const nsStyleGridLine& aStart, const nsStyleGridLine& aEnd, const nsTArray>& aLineNameList, uint32_t GridNamedArea::* aAreaStart, uint32_t GridNamedArea::* aAreaEnd, uint32_t aExplicitGridEnd, const nsStylePosition* aStyle) { if (aStart.mHasSpan) { if (aEnd.mHasSpan || aEnd.IsAuto()) { // http://dev.w3.org/csswg/css-grid/#grid-placement-errors if (aStart.mLineName.IsEmpty()) { // span / span * // span / auto return LinePair(0, aStart.mInteger); } // span / span * // span / auto return LinePair(0, 1); // XXX subgrid explicit size instead of 1? } auto end = ResolveLine(aEnd, aEnd.mInteger, 0, aLineNameList, aAreaStart, aAreaEnd, aExplicitGridEnd, eLineRangeSideEnd, aStyle); if (end == 0) { // span * / that can't be found return LinePair(0, aStart.mInteger); } int32_t span = aStart.mInteger == 0 ? 1 : aStart.mInteger; auto start = ResolveLine(aStart, -span, end, aLineNameList, aAreaStart, aAreaEnd, aExplicitGridEnd, eLineRangeSideStart, aStyle); MOZ_ASSERT(start > 0, "A start span can never resolve to 'auto'"); return LinePair(start, end); } uint32_t start = 0; if (!aStart.IsAuto()) { start = ResolveLine(aStart, aStart.mInteger, 0, aLineNameList, aAreaStart, aAreaEnd, aExplicitGridEnd, eLineRangeSideStart, aStyle); } if (aEnd.IsAuto()) { // * (except span) / auto return LinePair(start, 1); // XXX subgrid explicit size instead of 1? } if (start == 0 && aEnd.mHasSpan) { if (aEnd.mLineName.IsEmpty()) { // auto (or not found ) / span MOZ_ASSERT(aEnd.mInteger != 0); return LinePair(0, aEnd.mInteger); } // http://dev.w3.org/csswg/css-grid/#grid-placement-errors // auto (or not found ) / span return LinePair(0, 1); // XXX subgrid explicit size instead of 1? } uint32_t from = aEnd.mHasSpan ? start : 0; auto end = ResolveLine(aEnd, aEnd.mInteger, from, aLineNameList, aAreaStart, aAreaEnd, aExplicitGridEnd, eLineRangeSideEnd, aStyle); if (end == 0) { // * (except span) / not found end = 1; // XXX subgrid explicit size instead of 1? } else if (start == 0) { // auto (or not found ) / definite line start = std::max(1U, end - 1); } return LinePair(start, end); } nsGridContainerFrame::LineRange nsGridContainerFrame::ResolveLineRange( const nsStyleGridLine& aStart, const nsStyleGridLine& aEnd, const nsTArray>& aLineNameList, uint32_t GridNamedArea::* aAreaStart, uint32_t GridNamedArea::* aAreaEnd, uint32_t aExplicitGridEnd, const nsStylePosition* aStyle) { LinePair r = ResolveLineRangeHelper(aStart, aEnd, aLineNameList, aAreaStart, aAreaEnd, aExplicitGridEnd, aStyle); MOZ_ASSERT(r.second != 0); // http://dev.w3.org/csswg/css-grid/#grid-placement-errors if (r.second <= r.first) { r.second = r.first + 1; } return LineRange(r.first, r.second); } nsGridContainerFrame::GridArea nsGridContainerFrame::PlaceDefinite(nsIFrame* aChild, const nsStylePosition* aStyle) { const nsStylePosition* itemStyle = aChild->StylePosition(); return GridArea( ResolveLineRange(itemStyle->mGridColumnStart, itemStyle->mGridColumnEnd, aStyle->mGridTemplateColumns.mLineNameLists, &GridNamedArea::mColumnStart, &GridNamedArea::mColumnEnd, mExplicitGridColEnd, aStyle), ResolveLineRange(itemStyle->mGridRowStart, itemStyle->mGridRowEnd, aStyle->mGridTemplateRows.mLineNameLists, &GridNamedArea::mRowStart, &GridNamedArea::mRowEnd, mExplicitGridRowEnd, aStyle)); } void nsGridContainerFrame::InitializeGridBounds(const nsStylePosition* aStyle) { // http://dev.w3.org/csswg/css-grid/#grid-definition uint32_t colEnd = aStyle->mGridTemplateColumns.mLineNameLists.Length(); uint32_t rowEnd = aStyle->mGridTemplateRows.mLineNameLists.Length(); auto areas = aStyle->mGridTemplateAreas.get(); mExplicitGridColEnd = std::max(colEnd, areas ? areas->mNColumns + 1 : 1); mExplicitGridRowEnd = std::max(rowEnd, areas ? areas->NRows() + 1 : 1); mGridColEnd = mExplicitGridColEnd; mGridRowEnd = mExplicitGridRowEnd; } void nsGridContainerFrame::PlaceGridItems(const nsStylePosition* aStyle) { mCellMap.ClearOccupied(); InitializeGridBounds(aStyle); // http://dev.w3.org/csswg/css-grid/#auto-placement-algo // Step 1, place definite positions. for (nsFrameList::Enumerator e(PrincipalChildList()); !e.AtEnd(); e.Next()) { nsIFrame* child = e.get(); const GridArea& area = PlaceDefinite(child, aStyle); GridArea* prop = GetGridAreaForChild(child); if (prop) { *prop = area; } else { child->Properties().Set(GridAreaProperty(), new GridArea(area)); } if (area.IsDefinite()) { mCellMap.Fill(area); InflateGridFor(area); } } } void nsGridContainerFrame::Reflow(nsPresContext* aPresContext, nsHTMLReflowMetrics& aDesiredSize, const nsHTMLReflowState& aReflowState, nsReflowStatus& aStatus) { DO_GLOBAL_REFLOW_COUNT("nsGridContainerFrame"); DISPLAY_REFLOW(aPresContext, this, aReflowState, aDesiredSize, aStatus); if (IsFrameTreeTooDeep(aReflowState, aDesiredSize, aStatus)) { return; } #ifdef DEBUG SanityCheckAnonymousGridItems(); #endif // DEBUG LogicalMargin bp = aReflowState.ComputedLogicalBorderPadding(); bp.ApplySkipSides(GetLogicalSkipSides()); nscoord contentBSize = GetEffectiveComputedBSize(aReflowState); if (contentBSize == NS_AUTOHEIGHT) { contentBSize = 0; } WritingMode wm = aReflowState.GetWritingMode(); LogicalSize finalSize(wm, aReflowState.ComputedISize() + bp.IStartEnd(wm), contentBSize + bp.BStartEnd(wm)); aDesiredSize.SetSize(wm, finalSize); aDesiredSize.SetOverflowAreasToDesiredBounds(); const nsStylePosition* stylePos = aReflowState.mStylePosition; InitImplicitNamedAreas(stylePos); PlaceGridItems(stylePos); aStatus = NS_FRAME_COMPLETE; NS_FRAME_SET_TRUNCATION(aStatus, aReflowState, aDesiredSize); } nsIAtom* nsGridContainerFrame::GetType() const { return nsGkAtoms::gridContainerFrame; } #ifdef DEBUG_FRAME_DUMP nsresult nsGridContainerFrame::GetFrameName(nsAString& aResult) const { return MakeFrameName(NS_LITERAL_STRING("GridContainer"), aResult); } #endif void nsGridContainerFrame::CellMap::Fill(const GridArea& aGridArea) { MOZ_ASSERT(aGridArea.IsDefinite()); MOZ_ASSERT(aGridArea.mRows.mStart < aGridArea.mRows.mEnd); MOZ_ASSERT(aGridArea.mRows.mStart > 0); MOZ_ASSERT(aGridArea.mCols.mStart < aGridArea.mCols.mEnd); MOZ_ASSERT(aGridArea.mCols.mStart > 0); // Line numbers are 1-based so convert them to a zero-based index. const auto numRows = aGridArea.mRows.mEnd - 1; const auto numCols = aGridArea.mCols.mEnd - 1; mCells.EnsureLengthAtLeast(numRows); for (auto i = aGridArea.mRows.mStart - 1; i < numRows; ++i) { nsTArray& cellsInRow = mCells[i]; cellsInRow.EnsureLengthAtLeast(numCols); for (auto j = aGridArea.mCols.mStart - 1; j < numCols; ++j) { cellsInRow[j].mIsOccupied = true; } } } void nsGridContainerFrame::CellMap::ClearOccupied() { const size_t numRows = mCells.Length(); for (size_t i = 0; i < numRows; ++i) { nsTArray& cellsInRow = mCells[i]; const size_t numCols = cellsInRow.Length(); for (size_t j = 0; j < numCols; ++j) { cellsInRow[j].mIsOccupied = false; } } } #ifdef DEBUG void nsGridContainerFrame::CellMap::Dump() const { const size_t numRows = mCells.Length(); for (size_t i = 0; i < numRows; ++i) { const nsTArray& cellsInRow = mCells[i]; const size_t numCols = cellsInRow.Length(); printf("%lu:\t", (unsigned long)i + 1); for (size_t j = 0; j < numCols; ++j) { printf(cellsInRow[j].mIsOccupied ? "X " : ". "); } printf("\n"); } } static bool FrameWantsToBeInAnonymousGridItem(nsIFrame* aFrame) { // Note: This needs to match the logic in // nsCSSFrameConstructor::FrameConstructionItem::NeedsAnonFlexOrGridItem() return (aFrame->IsFrameOfType(nsIFrame::eLineParticipant) || nsGkAtoms::placeholderFrame == aFrame->GetType()); } // Debugging method, to let us assert that our anonymous grid items are // set up correctly -- in particular, we assert: // (1) we don't have any inline non-replaced children // (2) we don't have any consecutive anonymous grid items // (3) we don't have any empty anonymous grid items // (4) all children are on the expected child lists void nsGridContainerFrame::SanityCheckAnonymousGridItems() const { // XXX handle kOverflowContainersList / kExcessOverflowContainersList // when we implement fragmentation? ChildListIDs noCheckLists = kAbsoluteList | kFixedList; ChildListIDs checkLists = kPrincipalList | kOverflowList; for (nsIFrame::ChildListIterator childLists(this); !childLists.IsDone(); childLists.Next()) { if (!checkLists.Contains(childLists.CurrentID())) { MOZ_ASSERT(noCheckLists.Contains(childLists.CurrentID()), "unexpected non-empty child list"); continue; } bool prevChildWasAnonGridItem = false; nsFrameList children = childLists.CurrentList(); for (nsFrameList::Enumerator e(children); !e.AtEnd(); e.Next()) { nsIFrame* child = e.get(); MOZ_ASSERT(!FrameWantsToBeInAnonymousGridItem(child), "frame wants to be inside an anonymous grid item, " "but it isn't"); if (child->StyleContext()->GetPseudo() == nsCSSAnonBoxes::anonymousGridItem) { /* XXX haven't decided yet whether to reorder children or not. XXX If we do, we want this assertion instead of the one below. MOZ_ASSERT(!prevChildWasAnonGridItem || HasAnyStateBits(NS_STATE_GRID_CHILDREN_REORDERED), "two anon grid items in a row (shouldn't happen, unless our " "children have been reordered with the 'order' property)"); */ MOZ_ASSERT(!prevChildWasAnonGridItem, "two anon grid items in a row"); nsIFrame* firstWrappedChild = child->GetFirstPrincipalChild(); MOZ_ASSERT(firstWrappedChild, "anonymous grid item is empty (shouldn't happen)"); prevChildWasAnonGridItem = true; } else { prevChildWasAnonGridItem = false; } } } } #endif // DEBUG