/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/*
* rendering object for CSS display:block, inline-block, and list-item
* boxes, also used for various anonymous boxes
*/
#include "nsBlockFrame.h"
#include "gfxContext.h"
#include "mozilla/AppUnits.h"
#include "mozilla/Baseline.h"
#include "mozilla/ComputedStyle.h"
#include "mozilla/DebugOnly.h"
#include "mozilla/Likely.h"
#include "mozilla/Maybe.h"
#include "mozilla/PresShell.h"
#include "mozilla/ScrollContainerFrame.h"
#include "mozilla/StaticPrefs_browser.h"
#include "mozilla/StaticPrefs_layout.h"
#include "mozilla/SVGUtils.h"
#include "mozilla/ToString.h"
#include "mozilla/UniquePtr.h"
#include "nsCRT.h"
#include "nsCOMPtr.h"
#include "nsCSSRendering.h"
#include "nsAbsoluteContainingBlock.h"
#include "nsBlockReflowContext.h"
#include "BlockReflowState.h"
#include "nsFontMetrics.h"
#include "nsGenericHTMLElement.h"
#include "nsLineBox.h"
#include "nsLineLayout.h"
#include "nsPlaceholderFrame.h"
#include "nsStyleConsts.h"
#include "nsFrameManager.h"
#include "nsPresContext.h"
#include "nsPresContextInlines.h"
#include "nsHTMLParts.h"
#include "nsGkAtoms.h"
#include "mozilla/Sprintf.h"
#include "nsFloatManager.h"
#include "prenv.h"
#include "nsError.h"
#include <algorithm>
#include "nsLayoutUtils.h"
#include "nsDisplayList.h"
#include "nsCSSFrameConstructor.h"
#include "TextOverflow.h"
#include "nsIFrameInlines.h"
#include "CounterStyleManager.h"
#include "mozilla/dom/Selection.h"
#include "mozilla/PresShell.h"
#include "mozilla/RestyleManager.h"
#include "mozilla/ServoStyleSet.h"
#include "nsFlexContainerFrame.h"
#include "nsTextControlFrame.h"
#include "nsBidiPresUtils.h"
#include <inttypes.h>
static const int MIN_LINES_NEEDING_CURSOR = 20;
using namespace mozilla;
using namespace mozilla::css;
using namespace mozilla::dom;
using namespace mozilla::layout;
using AbsPosReflowFlags = nsAbsoluteContainingBlock::AbsPosReflowFlags;
using ClearFloatsResult = BlockReflowState::ClearFloatsResult;
using ShapeType = nsFloatManager::ShapeType;
static void MarkAllInlineLinesDirty(nsBlockFrame* aBlock) {
for (
auto& line : aBlock->Lines()) {
if (line.IsInline()) {
line.MarkDirty();
}
}
}
static void MarkAllDescendantLinesDirty(nsBlockFrame* aBlock) {
for (
auto& line : aBlock->Lines()) {
if (line.IsBlock()) {
nsBlockFrame* bf = do_QueryFrame(line.mFirstChild);
if (bf) {
MarkAllDescendantLinesDirty(bf);
}
}
line.MarkDirty();
}
}
static void MarkSameFloatManagerLinesDirty(nsBlockFrame* aBlock) {
nsBlockFrame* blockWithFloatMgr = aBlock;
while (!blockWithFloatMgr->HasAnyStateBits(NS_BLOCK_BFC)) {
nsBlockFrame* bf = do_QueryFrame(blockWithFloatMgr->GetParent());
if (!bf) {
break;
}
blockWithFloatMgr = bf;
}
// Mark every line at and below the line where the float was
// dirty, and mark their lines dirty too. We could probably do
// something more efficient --- e.g., just dirty the lines that intersect
// the float vertically.
MarkAllDescendantLinesDirty(blockWithFloatMgr);
}
/**
* Returns true if aFrame is a block that has one or more float children.
*/
static bool BlockHasAnyFloats(nsIFrame* aFrame) {
nsBlockFrame* block = do_QueryFrame(aFrame);
if (!block) {
return false;
}
if (block->GetChildList(FrameChildListID::
Float).FirstChild()) {
return true;
}
for (
const auto& line : block->Lines()) {
if (line.IsBlock() && BlockHasAnyFloats(line.mFirstChild)) {
return true;
}
}
return false;
}
// Determines whether the given frame is visible text or has visible text that
// participate in the same line. Frames that are not line participants do not
// have their children checked.
static bool FrameHasVisibleInlineText(nsIFrame* aFrame) {
MOZ_ASSERT(aFrame,
"Frame argument cannot be null");
if (!aFrame->IsLineParticipant()) {
return false;
}
if (aFrame->IsTextFrame()) {
return aFrame->StyleVisibility()->IsVisible() &&
NS_GET_A(aFrame->StyleText()->mWebkitTextFillColor.CalcColor(
aFrame)) != 0;
}
for (nsIFrame* kid : aFrame->PrincipalChildList()) {
if (FrameHasVisibleInlineText(kid)) {
return true;
}
}
return false;
}
// Determines whether any of the frames from the given line have visible text.
static bool LineHasVisibleInlineText(nsLineBox* aLine) {
nsIFrame* kid = aLine->mFirstChild;
int32_t n = aLine->GetChildCount();
while (n-- > 0) {
if (FrameHasVisibleInlineText(kid)) {
return true;
}
kid = kid->GetNextSibling();
}
return false;
}
/**
* Iterates through the frame's in-flow children and
* unions the ink overflow of all text frames which
* participate in the line aFrame belongs to.
* If a child of aFrame is not a text frame,
* we recurse with the child as the aFrame argument.
* If aFrame isn't a line participant, we skip it entirely
* and return an empty rect.
* The resulting nsRect is offset relative to the parent of aFrame.
*/
static nsRect GetFrameTextArea(nsIFrame* aFrame,
nsDisplayListBuilder* aBuilder) {
nsRect textArea;
if (
const nsTextFrame* textFrame = do_QueryFrame(aFrame)) {
if (!textFrame->IsEntirelyWhitespace()) {
textArea = aFrame->InkOverflowRect();
}
}
else if (aFrame->IsLineParticipant()) {
for (nsIFrame* kid : aFrame->PrincipalChildList()) {
nsRect kidTextArea = GetFrameTextArea(kid, aBuilder);
textArea.OrWith(kidTextArea);
}
}
// add aFrame's position to keep textArea relative to aFrame's parent
return textArea + aFrame->GetPosition();
}
/**
* Iterates through the line's children and
* unions the ink overflow of all text frames.
* GetFrameTextArea unions and returns the ink overflow
* from all line-participating text frames within the given child.
* The nsRect returned from GetLineTextArea is offset
* relative to the given line.
*/
static nsRect GetLineTextArea(nsLineBox* aLine,
nsDisplayListBuilder* aBuilder) {
nsRect textArea;
nsIFrame* kid = aLine->mFirstChild;
int32_t n = aLine->GetChildCount();
while (n-- > 0) {
nsRect kidTextArea = GetFrameTextArea(kid, aBuilder);
textArea.OrWith(kidTextArea);
kid = kid->GetNextSibling();
}
return textArea;
}
/**
* Starting with aFrame, iterates upward through parent frames and checks for
* non-transparent background colors. If one is found, we use that as our
* backplate color. Otheriwse, we use the default background color from
* our high contrast theme.
*/
static nscolor GetBackplateColor(nsIFrame* aFrame) {
nsPresContext* pc = aFrame->PresContext();
nscolor currentBackgroundColor = NS_TRANSPARENT;
for (nsIFrame* frame = aFrame; frame; frame = frame->GetParent()) {
// NOTE(emilio): We assume themed frames (frame->IsThemed()) have correct
// background-color information so as to compute the right backplate color.
//
// This holds because HTML widgets with author-specified backgrounds or
// borders disable theming. So as long as the UA-specified background colors
// match the actual theme (which they should because we always use system
// colors with the non-native theme, and native system colors should also
// match the native theme), then we're alright and we should compute an
// appropriate backplate color.
const auto* style = frame->Style();
if (style->StyleBackground()->IsTransparent(style)) {
continue;
}
bool drawImage =
false, drawColor =
false;
nscolor backgroundColor = nsCSSRendering::DetermineBackgroundColor(
pc, style, frame, drawImage, drawColor);
if (!drawColor && !drawImage) {
continue;
}
if (NS_GET_A(backgroundColor) == 0) {
// Even if there's a background image, if there's no background color we
// keep going up the frame tree, see bug 1723938.
continue;
}
if (NS_GET_A(currentBackgroundColor) == 0) {
// Try to avoid somewhat expensive math in the common case.
currentBackgroundColor = backgroundColor;
}
else {
currentBackgroundColor =
NS_ComposeColors(backgroundColor, currentBackgroundColor);
}
if (NS_GET_A(currentBackgroundColor) == 0xff) {
// If fully opaque, we're done, otherwise keep going up blending with our
// background.
return currentBackgroundColor;
}
}
nscolor backgroundColor = aFrame->PresContext()->DefaultBackgroundColor();
if (NS_GET_A(currentBackgroundColor) == 0) {
return backgroundColor;
}
return NS_ComposeColors(backgroundColor, currentBackgroundColor);
}
static nsRect GetNormalMarginRect(
const nsIFrame& aFrame,
bool aIncludePositiveMargins =
true) {
nsMargin m = aFrame.GetUsedMargin().ApplySkipSides(aFrame.GetSkipSides());
if (!aIncludePositiveMargins) {
m.EnsureAtMost(nsMargin());
}
auto rect = aFrame.GetRectRelativeToSelf();
rect.Inflate(m);
return rect + aFrame.GetNormalPosition();
}
#ifdef DEBUG
# include
"nsBlockDebugFlags.h"
bool nsBlockFrame::gLamePaintMetrics;
bool nsBlockFrame::gLameReflowMetrics;
bool nsBlockFrame::gNoisy;
bool nsBlockFrame::gNoisyDamageRepair;
bool nsBlockFrame::gNoisyIntrinsic;
bool nsBlockFrame::gNoisyReflow;
bool nsBlockFrame::gReallyNoisyReflow;
bool nsBlockFrame::gNoisyFloatManager;
bool nsBlockFrame::gVerifyLines;
bool nsBlockFrame::gDisableResizeOpt;
int32_t nsBlockFrame::gNoiseIndent;
struct BlockDebugFlags {
const char* name;
bool* on;
};
static const BlockDebugFlags gFlags[] = {
{
"reflow", &nsBlockFrame::gNoisyReflow},
{
"really-noisy-reflow", &nsBlockFrame::gReallyNoisyReflow},
{
"intrinsic", &nsBlockFrame::gNoisyIntrinsic},
{
"float-manager", &nsBlockFrame::gNoisyFloatManager},
{
"verify-lines", &nsBlockFrame::gVerifyLines},
{
"damage-repair", &nsBlockFrame::gNoisyDamageRepair},
{
"lame-paint-metrics", &nsBlockFrame::gLamePaintMetrics},
{
"lame-reflow-metrics", &nsBlockFrame::gLameReflowMetrics},
{
"disable-resize-opt", &nsBlockFrame::gDisableResizeOpt},
};
# define NUM_DEBUG_FLAGS (
sizeof(gFlags) /
sizeof(gFlags[0]))
static void ShowDebugFlags() {
printf(
"Here are the available GECKO_BLOCK_DEBUG_FLAGS:\n");
const BlockDebugFlags* bdf = gFlags;
const BlockDebugFlags* end = gFlags + NUM_DEBUG_FLAGS;
for (; bdf < end; bdf++) {
printf(
" %s\n", bdf->name);
}
printf(
"Note: GECKO_BLOCK_DEBUG_FLAGS is a comma separated list of flag\n");
printf(
"names (no whitespace)\n");
}
void nsBlockFrame::InitDebugFlags() {
static bool firstTime =
true;
if (firstTime) {
firstTime =
false;
char* flags = PR_GetEnv(
"GECKO_BLOCK_DEBUG_FLAGS");
if (flags) {
bool error =
false;
for (;;) {
char* cm = strchr(flags,
',');
if (cm) {
*cm =
'\0';
}
bool found =
false;
const BlockDebugFlags* bdf = gFlags;
const BlockDebugFlags* end = gFlags + NUM_DEBUG_FLAGS;
for (; bdf < end; bdf++) {
if (nsCRT::strcasecmp(bdf->name, flags) == 0) {
*(bdf->on) =
true;
printf(
"nsBlockFrame: setting %s debug flag on\n", bdf->name);
gNoisy =
true;
found =
true;
break;
}
}
if (!found) {
error =
true;
}
if (!cm) {
break;
}
*cm =
',';
flags = cm + 1;
}
if (error) {
ShowDebugFlags();
}
}
}
}
#endif
//----------------------------------------------------------------------
// Debugging support code
#ifdef DEBUG
const char* nsBlockFrame::kReflowCommandType[] = {
"ContentChanged",
"StyleChanged",
"ReflowDirty",
"Timeout",
"UserDefined",
};
const char* nsBlockFrame::LineReflowStatusToString(
LineReflowStatus aLineReflowStatus)
const {
switch (aLineReflowStatus) {
case LineReflowStatus::OK:
return "LINE_REFLOW_OK";
case LineReflowStatus::Stop:
return "LINE_REFLOW_STOP";
case LineReflowStatus::RedoNoPull:
return "LINE_REFLOW_REDO_NO_PULL";
case LineReflowStatus::RedoMoreFloats:
return "LINE_REFLOW_REDO_MORE_FLOATS";
case LineReflowStatus::RedoNextBand:
return "LINE_REFLOW_REDO_NEXT_BAND";
case LineReflowStatus::Truncated:
return "LINE_REFLOW_TRUNCATED";
}
return "unknown";
}
#endif
#ifdef REFLOW_STATUS_COVERAGE
static void RecordReflowStatus(
bool aChildIsBlock,
const nsReflowStatus& aFrameReflowStatus) {
static uint32_t record[2];
// 0: child-is-block
// 1: child-is-inline
int index = 0;
if (!aChildIsBlock) {
index |= 1;
}
// Compute new status
uint32_t newS = record[index];
if (aFrameReflowStatus.IsInlineBreak()) {
if (aFrameReflowStatus.IsInlineBreakBefore()) {
newS |= 1;
}
else if (aFrameReflowStatus.IsIncomplete()) {
newS |= 2;
}
else {
newS |= 4;
}
}
else if (aFrameReflowStatus.IsIncomplete()) {
newS |= 8;
}
else {
newS |= 16;
}
// Log updates to the status that yield different values
if (record[index] != newS) {
record[index] = newS;
printf(
"record(%d): %02x %02x\n", index, record[0], record[1]);
}
}
#endif
NS_DECLARE_FRAME_PROPERTY_WITH_DTOR_NEVER_CALLED(OverflowLinesProperty,
nsBlockFrame::FrameLines)
NS_DECLARE_FRAME_PROPERTY_FRAMELIST(OverflowOutOfFlowsProperty)
NS_DECLARE_FRAME_PROPERTY_FRAMELIST(FloatsProperty)
NS_DECLARE_FRAME_PROPERTY_FRAMELIST(PushedFloatsProperty)
NS_DECLARE_FRAME_PROPERTY_FRAMELIST(OutsideMarkerProperty)
NS_DECLARE_FRAME_PROPERTY_WITHOUT_DTOR(InsideMarkerProperty, nsIFrame)
//----------------------------------------------------------------------
nsBlockFrame* NS_NewBlockFrame(PresShell* aPresShell, ComputedStyle* aStyle) {
return new (aPresShell) nsBlockFrame(aStyle, aPresShell->GetPresContext());
}
NS_IMPL_FRAMEARENA_HELPERS(nsBlockFrame)
nsBlockFrame::~nsBlockFrame() =
default;
void nsBlockFrame::AddSizeOfExcludingThisForTree(
nsWindowSizes& aWindowSizes)
const {
nsContainerFrame::AddSizeOfExcludingThisForTree(aWindowSizes);
// Add the size of any nsLineBox::mFrames hashtables we might have:
for (
const auto& line : Lines()) {
line.AddSizeOfExcludingThis(aWindowSizes);
}
const FrameLines* overflowLines = GetOverflowLines();
if (overflowLines) {
ConstLineIterator line = overflowLines->mLines.begin(),
line_end = overflowLines->mLines.end();
for (; line != line_end; ++line) {
line->AddSizeOfExcludingThis(aWindowSizes);
}
}
}
void nsBlockFrame::Destroy(DestroyContext& aContext) {
ClearLineCursors();
DestroyAbsoluteFrames(aContext);
nsPresContext* presContext = PresContext();
mozilla::PresShell* presShell = presContext->PresShell();
if (HasFloats()) {
SafelyDestroyFrameListProp(aContext, presShell, FloatsProperty());
RemoveStateBits(NS_BLOCK_HAS_FLOATS);
}
nsLineBox::DeleteLineList(presContext, mLines, &mFrames, aContext);
if (HasPushedFloats()) {
SafelyDestroyFrameListProp(aContext, presShell, PushedFloatsProperty());
RemoveStateBits(NS_BLOCK_HAS_PUSHED_FLOATS);
}
// destroy overflow lines now
FrameLines* overflowLines = RemoveOverflowLines();
if (overflowLines) {
nsLineBox::DeleteLineList(presContext, overflowLines->mLines,
&overflowLines->mFrames, aContext);
delete overflowLines;
}
if (HasAnyStateBits(NS_BLOCK_HAS_OVERFLOW_OUT_OF_FLOWS)) {
SafelyDestroyFrameListProp(aContext, presShell,
OverflowOutOfFlowsProperty());
RemoveStateBits(NS_BLOCK_HAS_OVERFLOW_OUT_OF_FLOWS);
}
if (HasMarker()) {
SafelyDestroyFrameListProp(aContext, presShell, OutsideMarkerProperty());
RemoveStateBits(NS_BLOCK_HAS_MARKER);
}
nsContainerFrame::Destroy(aContext);
}
/* virtual */
nsILineIterator* nsBlockFrame::GetLineIterator() {
nsLineIterator* iter = GetProperty(LineIteratorProperty());
if (!iter) {
const nsStyleVisibility* visibility = StyleVisibility();
iter =
new nsLineIterator(mLines,
visibility->mDirection == StyleDirection::Rtl);
SetProperty(LineIteratorProperty(), iter);
}
return iter;
}
NS_QUERYFRAME_HEAD(nsBlockFrame)
NS_QUERYFRAME_ENTRY(nsBlockFrame)
NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame)
#ifdef DEBUG_FRAME_DUMP
void nsBlockFrame::List(FILE* out,
const char* aPrefix,
ListFlags aFlags)
const {
nsCString str;
ListGeneric(str, aPrefix, aFlags);
fprintf_stderr(out,
"%s <\n", str.get());
nsCString pfx(aPrefix);
pfx +=
" ";
// Output the lines
if (!mLines.empty()) {
ConstLineIterator line = LinesBegin(), line_end = LinesEnd();
for (; line != line_end; ++line) {
line->List(out, pfx.get(), aFlags);
}
}
// Output the overflow lines.
const FrameLines* overflowLines = GetOverflowLines();
if (overflowLines && !overflowLines->mLines.empty()) {
fprintf_stderr(out,
"%sOverflow-lines %p/%p <\n", pfx.get(), overflowLines,
&overflowLines->mFrames);
nsCString nestedPfx(pfx);
nestedPfx +=
" ";
ConstLineIterator line = overflowLines->mLines.begin(),
line_end = overflowLines->mLines.end();
for (; line != line_end; ++line) {
line->List(out, nestedPfx.get(), aFlags);
}
fprintf_stderr(out,
"%s>\n", pfx.get());
}
// skip the principal list - we printed the lines above
// skip the overflow list - we printed the overflow lines above
ChildListIDs skip = {FrameChildListID::Principal, FrameChildListID::Overflow};
ListChildLists(out, pfx.get(), aFlags, skip);
fprintf_stderr(out,
"%s>\n", aPrefix);
}
nsresult nsBlockFrame::GetFrameName(nsAString& aResult)
const {
return MakeFrameName(u
"Block"_ns, aResult);
}
#endif
void nsBlockFrame::InvalidateFrame(uint32_t aDisplayItemKey,
bool aRebuildDisplayItems) {
if (IsInSVGTextSubtree()) {
NS_ASSERTION(GetParent()->IsSVGTextFrame(),
"unexpected block frame in SVG text");
GetParent()->InvalidateFrame();
return;
}
nsContainerFrame::InvalidateFrame(aDisplayItemKey, aRebuildDisplayItems);
}
void nsBlockFrame::InvalidateFrameWithRect(
const nsRect& aRect,
uint32_t aDisplayItemKey,
bool aRebuildDisplayItems) {
if (IsInSVGTextSubtree()) {
NS_ASSERTION(GetParent()->IsSVGTextFrame(),
"unexpected block frame in SVG text");
GetParent()->InvalidateFrame();
return;
}
nsContainerFrame::InvalidateFrameWithRect(aRect, aDisplayItemKey,
aRebuildDisplayItems);
}
nscoord nsBlockFrame::SynthesizeFallbackBaseline(
WritingMode aWM, BaselineSharingGroup aBaselineGroup)
const {
return Baseline::SynthesizeBOffsetFromMarginBox(
this, aWM, aBaselineGroup);
}
template <
typename LineIteratorType>
Maybe<nscoord> nsBlockFrame::GetBaselineBOffset(
LineIteratorType aStart, LineIteratorType aEnd, WritingMode aWM,
BaselineSharingGroup aBaselineGroup,
BaselineExportContext aExportContext)
const {
MOZ_ASSERT((std::is_same_v<LineIteratorType, ConstLineIterator> &&
aBaselineGroup == BaselineSharingGroup::First) ||
(std::is_same_v<LineIteratorType, ConstReverseLineIterator> &&
aBaselineGroup == BaselineSharingGroup::Last),
"Iterator direction must match baseline sharing group.");
for (
auto line = aStart; line != aEnd; ++line) {
if (!line->IsBlock()) {
// XXX Is this the right test? We have some bogus empty lines
// floating around, but IsEmpty is perhaps too weak.
if (line->BSize() != 0 || !line->IsEmpty()) {
const auto ascent = line->BStart() + line->GetLogicalAscent();
if (aBaselineGroup == BaselineSharingGroup::Last) {
return Some(BSize(aWM) - ascent);
}
return Some(ascent);
}
continue;
}
nsIFrame* kid = line->mFirstChild;
if (aWM.IsOrthogonalTo(kid->GetWritingMode())) {
continue;
}
if (aExportContext == BaselineExportContext::LineLayout &&
kid->IsTableWrapperFrame()) {
// `<table>` in inline-block context does not export any baseline.
continue;
}
const auto kidBaselineGroup =
aExportContext == BaselineExportContext::LineLayout
? kid->GetDefaultBaselineSharingGroup()
: aBaselineGroup;
const auto kidBaseline =
kid->GetNaturalBaselineBOffset(aWM, kidBaselineGroup, aExportContext);
if (!kidBaseline) {
continue;
}
auto result = *kidBaseline;
if (kidBaselineGroup == BaselineSharingGroup::Last) {
result = kid->BSize(aWM) - result;
}
// Ignore relative positioning for baseline calculations.
const nsSize& sz = line->mContainerSize;
result += kid->GetLogicalNormalPosition(aWM, sz).B(aWM);
if (aBaselineGroup == BaselineSharingGroup::Last) {
return Some(BSize(aWM) - result);
}
return Some(result);
}
return Nothing{};
}
Maybe<nscoord> nsBlockFrame::GetNaturalBaselineBOffset(
WritingMode aWM, BaselineSharingGroup aBaselineGroup,
BaselineExportContext aExportContext)
const {
if (StyleDisplay()->IsContainLayout()) {
return Nothing{};
}
if (aBaselineGroup == BaselineSharingGroup::First) {
return GetBaselineBOffset(LinesBegin(), LinesEnd(), aWM, aBaselineGroup,
aExportContext);
}
return GetBaselineBOffset(LinesRBegin(), LinesREnd(), aWM, aBaselineGroup,
aExportContext);
}
nscoord nsBlockFrame::GetCaretBaseline()
const {
const auto wm = GetWritingMode();
if (!mLines.empty()) {
ConstLineIterator line = LinesBegin();
if (!line->IsEmpty()) {
if (line->IsBlock()) {
return GetLogicalUsedBorderAndPadding(wm).BStart(wm) +
line->mFirstChild->GetCaretBaseline();
}
return line->BStart() + line->GetLogicalAscent();
}
}
return GetFontMetricsDerivedCaretBaseline(ContentBSize(wm));
}
/////////////////////////////////////////////////////////////////////////////
// Child frame enumeration
const nsFrameList& nsBlockFrame::GetChildList(ChildListID aListID)
const {
switch (aListID) {
case FrameChildListID::Principal:
return mFrames;
case FrameChildListID::Overflow: {
FrameLines* overflowLines = GetOverflowLines();
return overflowLines ? overflowLines->mFrames : nsFrameList::EmptyList();
}
case FrameChildListID::OverflowOutOfFlow: {
const nsFrameList* list = GetOverflowOutOfFlows();
return list ? *list : nsFrameList::EmptyList();
}
case FrameChildListID::
Float: {
const nsFrameList* list = GetFloats();
return list ? *list : nsFrameList::EmptyList();
}
case FrameChildListID::PushedFloats: {
const nsFrameList* list = GetPushedFloats();
return list ? *list : nsFrameList::EmptyList();
}
case FrameChildListID::Bullet: {
const nsFrameList* list = GetOutsideMarkerList();
return list ? *list : nsFrameList::EmptyList();
}
default:
return nsContainerFrame::GetChildList(aListID);
}
}
void nsBlockFrame::GetChildLists(nsTArray<ChildList>* aLists)
const {
nsContainerFrame::GetChildLists(aLists);
FrameLines* overflowLines = GetOverflowLines();
if (overflowLines) {
overflowLines->mFrames.AppendIfNonempty(aLists, FrameChildListID::Overflow);
}
if (
const nsFrameList* list = GetOverflowOutOfFlows()) {
list->AppendIfNonempty(aLists, FrameChildListID::OverflowOutOfFlow);
}
if (
const nsFrameList* list = GetOutsideMarkerList()) {
list->AppendIfNonempty(aLists, FrameChildListID::Bullet);
}
if (
const nsFrameList* list = GetFloats()) {
list->AppendIfNonempty(aLists, FrameChildListID::
Float);
}
if (
const nsFrameList* list = GetPushedFloats()) {
list->AppendIfNonempty(aLists, FrameChildListID::PushedFloats);
}
}
/* virtual */
bool nsBlockFrame::IsFloatContainingBlock()
const {
return true; }
/**
* Remove the first line from aFromLines and adjust the associated frame list
* aFromFrames accordingly. The removed line is assigned to *aOutLine and
* a frame list with its frames is assigned to *aOutFrames, i.e. the frames
* that were extracted from the head of aFromFrames.
* aFromLines must contain at least one line, the line may be empty.
* @return true if aFromLines becomes empty
*/
static bool RemoveFirstLine(nsLineList& aFromLines, nsFrameList& aFromFrames,
nsLineBox** aOutLine, nsFrameList* aOutFrames) {
LineListIterator removedLine = aFromLines.begin();
*aOutLine = removedLine;
LineListIterator next = aFromLines.erase(removedLine);
bool isLastLine = next == aFromLines.end();
nsIFrame* firstFrameInNextLine = isLastLine ? nullptr : next->mFirstChild;
*aOutFrames = aFromFrames.TakeFramesBefore(firstFrameInNextLine);
return isLastLine;
}
//////////////////////////////////////////////////////////////////////
// Reflow methods
/* virtual */
void nsBlockFrame::MarkIntrinsicISizesDirty() {
nsBlockFrame* dirtyBlock =
static_cast<nsBlockFrame*>(FirstContinuation());
dirtyBlock->mCachedIntrinsics.Clear();
if (!HasAnyStateBits(NS_BLOCK_NEEDS_BIDI_RESOLUTION)) {
for (nsIFrame* frame = dirtyBlock; frame;
frame = frame->GetNextContinuation()) {
frame->AddStateBits(NS_BLOCK_NEEDS_BIDI_RESOLUTION);
}
}
nsContainerFrame::MarkIntrinsicISizesDirty();
}
void nsBlockFrame::CheckIntrinsicCacheAgainstShrinkWrapState() {
nsPresContext* presContext = PresContext();
if (!nsLayoutUtils::FontSizeInflationEnabled(presContext)) {
return;
}
bool inflationEnabled = !presContext->mInflationDisabledForShrinkWrap;
if (inflationEnabled != HasAnyStateBits(NS_BLOCK_INTRINSICS_INFLATED)) {
mCachedIntrinsics.Clear();
AddOrRemoveStateBits(NS_BLOCK_INTRINSICS_INFLATED, inflationEnabled);
}
}
// Whether this line is indented by the text-indent amount.
bool nsBlockFrame::TextIndentAppliesTo(
const LineIterator& aLine)
const {
const auto& textIndent = StyleText()->mTextIndent;
bool isFirstLineOrAfterHardBreak = [&] {
if (aLine != LinesBegin()) {
// If not the first line of the block, but 'each-line' is in effect,
// check if the previous line was not wrapped.
return textIndent.each_line && !aLine.prev()->IsLineWrapped();
}
if (nsBlockFrame* prevBlock = do_QueryFrame(GetPrevInFlow())) {
// There's a prev-in-flow, so this only counts as a first-line if
// 'each-line' and the prev-in-flow's last line was not wrapped.
return textIndent.each_line &&
(prevBlock->Lines().empty() ||
!prevBlock->LinesEnd().prev()->IsLineWrapped());
}
return true;
}();
// The 'hanging' option inverts which lines are/aren't indented.
return isFirstLineOrAfterHardBreak != textIndent.hanging;
}
nscoord nsBlockFrame::IntrinsicISize(
const IntrinsicSizeInput& aInput,
IntrinsicISizeType aType) {
nsIFrame* firstCont = FirstContinuation();
if (firstCont !=
this) {
return firstCont->IntrinsicISize(aInput, aType);
}
CheckIntrinsicCacheAgainstShrinkWrapState();
return mCachedIntrinsics.GetOrSet(*
this, aType, aInput, [&] {
return aType == IntrinsicISizeType::MinISize ? MinISize(aInput)
: PrefISize(aInput);
});
}
/* virtual */
nscoord nsBlockFrame::MinISize(
const IntrinsicSizeInput& aInput) {
if (Maybe<nscoord> containISize = ContainIntrinsicISize()) {
return *containISize;
}
#ifdef DEBUG
if (gNoisyIntrinsic) {
IndentBy(stdout, gNoiseIndent);
ListTag(stdout);
printf(
": MinISize\n");
}
AutoNoisyIndenter indenter(gNoisyIntrinsic);
#endif
for (nsBlockFrame* curFrame =
this; curFrame;
curFrame =
static_cast<nsBlockFrame*>(curFrame->GetNextContinuation())) {
curFrame->LazyMarkLinesDirty();
}
if (HasAnyStateBits(NS_BLOCK_NEEDS_BIDI_RESOLUTION) &&
PresContext()->BidiEnabled()) {
ResolveBidi();
}
const bool whiteSpaceCanWrap = StyleText()->WhiteSpaceCanWrapStyle();
InlineMinISizeData data;
for (nsBlockFrame* curFrame =
this; curFrame;
curFrame =
static_cast<nsBlockFrame*>(curFrame->GetNextContinuation())) {
for (LineIterator line = curFrame->LinesBegin(),
line_end = curFrame->LinesEnd();
line != line_end; ++line) {
#ifdef DEBUG
if (gNoisyIntrinsic) {
IndentBy(stdout, gNoiseIndent);
printf(
"line (%s%s)\n", line->IsBlock() ?
"block" :
"inline",
line->IsEmpty() ?
", empty" :
"");
}
AutoNoisyIndenter lineindent(gNoisyIntrinsic);
#endif
if (line->IsBlock()) {
data.ForceBreak();
nsIFrame* kid = line->mFirstChild;
const IntrinsicSizeInput kidInput(aInput, kid->GetWritingMode(),
GetWritingMode());
data.mCurrentLine = nsLayoutUtils::IntrinsicForContainer(
kidInput.mContext, kid, IntrinsicISizeType::MinISize,
kidInput.mPercentageBasisForChildren);
data.ForceBreak();
}
else {
if (!curFrame->GetPrevContinuation() && TextIndentAppliesTo(line)) {
data.mCurrentLine += StyleText()->mTextIndent.length.Resolve(0);
}
data.mLine = &line;
data.SetLineContainer(curFrame);
nsIFrame* kid = line->mFirstChild;
for (int32_t i = 0, i_end = line->GetChildCount(); i != i_end;
++i, kid = kid->GetNextSibling()) {
const IntrinsicSizeInput kidInput(aInput, kid->GetWritingMode(),
GetWritingMode());
kid->AddInlineMinISize(kidInput, &data);
if (whiteSpaceCanWrap && data.mTrailingWhitespace) {
data.OptionallyBreak();
}
}
}
#ifdef DEBUG
if (gNoisyIntrinsic) {
IndentBy(stdout, gNoiseIndent);
printf(
"min: [prevLines=%d currentLine=%d]\n", data.mPrevLines,
data.mCurrentLine);
}
#endif
}
}
data.ForceBreak();
return data.mPrevLines;
}
/* virtual */
nscoord nsBlockFrame::PrefISize(
const IntrinsicSizeInput& aInput) {
if (Maybe<nscoord> containISize = ContainIntrinsicISize()) {
return *containISize;
}
#ifdef DEBUG
if (gNoisyIntrinsic) {
IndentBy(stdout, gNoiseIndent);
ListTag(stdout);
printf(
": PrefISize\n");
}
AutoNoisyIndenter indenter(gNoisyIntrinsic);
#endif
for (nsBlockFrame* curFrame =
this; curFrame;
curFrame =
static_cast<nsBlockFrame*>(curFrame->GetNextContinuation())) {
curFrame->LazyMarkLinesDirty();
}
if (HasAnyStateBits(NS_BLOCK_NEEDS_BIDI_RESOLUTION) &&
PresContext()->BidiEnabled()) {
ResolveBidi();
}
InlinePrefISizeData data;
for (nsBlockFrame* curFrame =
this; curFrame;
curFrame =
static_cast<nsBlockFrame*>(curFrame->GetNextContinuation())) {
for (LineIterator line = curFrame->LinesBegin(),
line_end = curFrame->LinesEnd();
line != line_end; ++line) {
#ifdef DEBUG
if (gNoisyIntrinsic) {
IndentBy(stdout, gNoiseIndent);
printf(
"line (%s%s)\n", line->IsBlock() ?
"block" :
"inline",
line->IsEmpty() ?
", empty" :
"");
}
AutoNoisyIndenter lineindent(gNoisyIntrinsic);
#endif
if (line->IsBlock()) {
nsIFrame* kid = line->mFirstChild;
UsedClear clearType;
if (!data.mLineIsEmpty || BlockCanIntersectFloats(kid)) {
clearType = UsedClear::Both;
}
else {
clearType = kid->StyleDisplay()->UsedClear(GetWritingMode());
}
data.ForceBreak(clearType);
const IntrinsicSizeInput kidInput(aInput, kid->GetWritingMode(),
GetWritingMode());
data.mCurrentLine = nsLayoutUtils::IntrinsicForContainer(
kidInput.mContext, kid, IntrinsicISizeType::PrefISize,
kidInput.mPercentageBasisForChildren);
data.ForceBreak();
}
else {
if (!curFrame->GetPrevContinuation() && TextIndentAppliesTo(line)) {
nscoord indent = StyleText()->mTextIndent.length.Resolve(0);
data.mCurrentLine += indent;
// XXXmats should the test below be indent > 0?
if (indent != nscoord(0)) {
data.mLineIsEmpty =
false;
}
}
data.mLine = &line;
data.SetLineContainer(curFrame);
nsIFrame* kid = line->mFirstChild;
for (int32_t i = 0, i_end = line->GetChildCount(); i != i_end;
++i, kid = kid->GetNextSibling()) {
const IntrinsicSizeInput kidInput(aInput, kid->GetWritingMode(),
GetWritingMode());
kid->AddInlinePrefISize(kidInput, &data);
}
}
#ifdef DEBUG
if (gNoisyIntrinsic) {
IndentBy(stdout, gNoiseIndent);
printf(
"pref: [prevLines=%d currentLine=%d]\n", data.mPrevLines,
data.mCurrentLine);
}
#endif
}
}
data.ForceBreak();
return data.mPrevLines;
}
nsRect nsBlockFrame::ComputeTightBounds(DrawTarget* aDrawTarget)
const {
// be conservative
if (Style()->HasTextDecorationLines()) {
return InkOverflowRect();
}
return ComputeSimpleTightBounds(aDrawTarget);
}
/* virtual */
nsresult nsBlockFrame::GetPrefWidthTightBounds(gfxContext* aRenderingContext,
nscoord* aX, nscoord* aXMost) {
nsIFrame* firstInFlow = FirstContinuation();
if (firstInFlow !=
this) {
return firstInFlow->GetPrefWidthTightBounds(aRenderingContext, aX, aXMost);
}
*aX = 0;
*aXMost = 0;
nsresult rv;
InlinePrefISizeData data;
for (nsBlockFrame* curFrame =
this; curFrame;
curFrame =
static_cast<nsBlockFrame*>(curFrame->GetNextContinuation())) {
for (LineIterator line = curFrame->LinesBegin(),
line_end = curFrame->LinesEnd();
line != line_end; ++line) {
nscoord childX, childXMost;
if (line->IsBlock()) {
data.ForceBreak();
rv = line->mFirstChild->GetPrefWidthTightBounds(aRenderingContext,
&childX, &childXMost);
NS_ENSURE_SUCCESS(rv, rv);
*aX = std::min(*aX, childX);
*aXMost = std::max(*aXMost, childXMost);
}
else {
if (!curFrame->GetPrevContinuation() && TextIndentAppliesTo(line)) {
data.mCurrentLine += StyleText()->mTextIndent.length.Resolve(0);
}
data.mLine = &line;
data.SetLineContainer(curFrame);
nsIFrame* kid = line->mFirstChild;
// Per comment in nsIFrame::GetPrefWidthTightBounds(), the function is
// only implemented for nsBlockFrame and nsTextFrame and is used to
// determine the intrinsic inline sizes of MathML token elements. These
// elements shouldn't have percentage block sizes that require a
// percentage basis for resolution.
const IntrinsicSizeInput kidInput(aRenderingContext, Nothing(),
Nothing());
for (int32_t i = 0, i_end = line->GetChildCount(); i != i_end;
++i, kid = kid->GetNextSibling()) {
rv = kid->GetPrefWidthTightBounds(aRenderingContext, &childX,
&childXMost);
NS_ENSURE_SUCCESS(rv, rv);
*aX = std::min(*aX, data.mCurrentLine + childX);
*aXMost = std::max(*aXMost, data.mCurrentLine + childXMost);
kid->AddInlinePrefISize(kidInput, &data);
}
}
}
}
data.ForceBreak();
return NS_OK;
}
/**
* Return whether aNewAvailableSpace is smaller *on either side*
* (inline-start or inline-end) than aOldAvailableSpace, so that we know
* if we need to redo layout on an line, replaced block, or block
* formatting context, because its height (which we used to compute
* aNewAvailableSpace) caused it to intersect additional floats.
*/
static bool AvailableSpaceShrunk(WritingMode aWM,
const LogicalRect& aOldAvailableSpace,
const LogicalRect& aNewAvailableSpace,
bool aCanGrow
/* debug-only */) {
if (aNewAvailableSpace.ISize(aWM) == 0) {
// Positions are not significant if the inline size is zero.
return aOldAvailableSpace.ISize(aWM) != 0;
}
if (aCanGrow) {
NS_ASSERTION(
aNewAvailableSpace.IStart(aWM) <= aOldAvailableSpace.IStart(aWM) ||
aNewAvailableSpace.IEnd(aWM) <= aOldAvailableSpace.IEnd(aWM),
"available space should not shrink on the start side and "
"grow on the end side");
NS_ASSERTION(
aNewAvailableSpace.IStart(aWM) >= aOldAvailableSpace.IStart(aWM) ||
aNewAvailableSpace.IEnd(aWM) >= aOldAvailableSpace.IEnd(aWM),
"available space should not grow on the start side and "
"shrink on the end side");
}
else {
NS_ASSERTION(
aOldAvailableSpace.IStart(aWM) <= aNewAvailableSpace.IStart(aWM) &&
aOldAvailableSpace.IEnd(aWM) >= aNewAvailableSpace.IEnd(aWM),
"available space should never grow");
}
// Have we shrunk on either side?
return aNewAvailableSpace.IStart(aWM) > aOldAvailableSpace.IStart(aWM) ||
aNewAvailableSpace.IEnd(aWM) < aOldAvailableSpace.IEnd(aWM);
}
static LogicalSize CalculateContainingBlockSizeForAbsolutes(
WritingMode aWM,
const ReflowInput& aReflowInput, LogicalSize aFrameSize) {
// The issue here is that for a 'height' of 'auto' the reflow input
// code won't know how to calculate the containing block height
// because it's calculated bottom up. So we use our own computed
// size as the dimensions.
nsIFrame* frame = aReflowInput.mFrame;
LogicalSize cbSize(aFrameSize);
// Containing block is relative to the padding edge
const LogicalMargin border = aReflowInput.ComputedLogicalBorder(aWM);
cbSize.ISize(aWM) -= border.IStartEnd(aWM);
cbSize.BSize(aWM) -= border.BStartEnd(aWM);
if (frame->GetParent()->GetContent() != frame->GetContent() ||
frame->GetParent()->IsCanvasFrame()) {
return cbSize;
}
// We are a wrapped frame for the content (and the wrapper is not the
// canvas frame, whose size is not meaningful here).
// Use the container's dimensions, if they have been precomputed.
// XXX This is a hack! We really should be waiting until the outermost
// frame is fully reflowed and using the resulting dimensions, even
// if they're intrinsic.
// In fact we should be attaching absolute children to the outermost
// frame and not always sticking them in block frames.
// First, find the reflow input for the outermost frame for this content.
const ReflowInput* lastRI = &aReflowInput;
DebugOnly<
const ReflowInput*> lastButOneRI = &aReflowInput;
while (lastRI->mParentReflowInput &&
lastRI->mParentReflowInput->mFrame->GetContent() ==
frame->GetContent()) {
lastButOneRI = lastRI;
lastRI = lastRI->mParentReflowInput;
}
if (lastRI == &aReflowInput) {
return cbSize;
}
// For scroll containers, we can just use cbSize (which is the padding-box
// size of the scrolled-content frame).
if (lastRI->mFrame->IsScrollContainerOrSubclass()) {
// Assert that we're not missing any frames between the abspos containing
// block and the scroll container.
// the parent.
MOZ_ASSERT(lastButOneRI == &aReflowInput);
return cbSize;
}
// Same for fieldsets, where the inner anonymous frame has the correct padding
// area with the legend taken into account.
if (lastRI->mFrame->IsFieldSetFrame()) {
return cbSize;
}
// We found a reflow input for the outermost wrapping frame, so use
// its computed metrics if available, converted to our writing mode
const LogicalSize lastRISize = lastRI->ComputedSize(aWM);
const LogicalMargin lastRIPadding = lastRI->ComputedLogicalPadding(aWM);
if (lastRISize.ISize(aWM) != NS_UNCONSTRAINEDSIZE) {
cbSize.ISize(aWM) =
std::max(0, lastRISize.ISize(aWM) + lastRIPadding.IStartEnd(aWM));
}
if (lastRISize.BSize(aWM) != NS_UNCONSTRAINEDSIZE) {
cbSize.BSize(aWM) =
std::max(0, lastRISize.BSize(aWM) + lastRIPadding.BStartEnd(aWM));
}
return cbSize;
}
/**
* Returns aFrame if it is an in-flow, non-BFC block frame, and null otherwise.
*
* This is used to determine whether to recurse into aFrame when applying
* -webkit-line-clamp.
*/
static const nsBlockFrame* GetAsLineClampDescendant(
const nsIFrame* aFrame) {
const nsBlockFrame* block = do_QueryFrame(aFrame);
if (!block || block->HasAnyStateBits(NS_FRAME_OUT_OF_FLOW | NS_BLOCK_BFC)) {
return nullptr;
}
return block;
}
static nsBlockFrame* GetAsLineClampDescendant(nsIFrame* aFrame) {
return const_cast<nsBlockFrame*>(
GetAsLineClampDescendant(
const_cast<
const nsIFrame*>(aFrame)));
}
static bool IsLineClampRoot(
const nsBlockFrame* aFrame) {
if (!aFrame->StyleDisplay()->mWebkitLineClamp) {
return false;
}
if (!aFrame->HasAnyStateBits(NS_BLOCK_BFC)) {
return false;
}
if (StaticPrefs::layout_css_webkit_line_clamp_block_enabled() ||
aFrame->PresContext()->Document()->ChromeRulesEnabled()) {
return true;
}
// For now, -webkit-box is the only thing allowed to be a line-clamp root.
// Ideally we'd just make this work everywhere, but for now we're carrying
// this forward as a limitation on the legacy -webkit-line-clamp feature,
// since relaxing this limitation might create webcompat trouble.
auto origDisplay = [&] {
if (aFrame->Style()->GetPseudoType() == PseudoStyleType::scrolledContent) {
// If we're the anonymous block inside the scroll frame, we need to look
// at the original display of our parent frame.
MOZ_ASSERT(aFrame->GetParent());
const auto& parentDisp = *aFrame->GetParent()->StyleDisplay();
MOZ_ASSERT(parentDisp.mWebkitLineClamp ==
aFrame->StyleDisplay()->mWebkitLineClamp,
":-moz-scrolled-content should inherit -webkit-line-clamp, "
"via rule in UA stylesheet");
return parentDisp.mOriginalDisplay;
}
return aFrame->StyleDisplay()->mOriginalDisplay;
}();
return origDisplay.Inside() == StyleDisplayInside::WebkitBox;
}
nsBlockFrame* nsBlockFrame::GetLineClampRoot()
const {
if (IsLineClampRoot(
this)) {
return const_cast<nsBlockFrame*>(
this);
}
const nsBlockFrame* cur =
this;
while (GetAsLineClampDescendant(cur)) {
cur = do_QueryFrame(cur->GetParent());
if (!cur) {
break;
}
if (IsLineClampRoot(cur)) {
return const_cast<nsBlockFrame*>(cur);
}
}
return nullptr;
}
bool nsBlockFrame::MaybeHasFloats()
const {
if (HasFloats()) {
return true;
}
if (HasPushedFloats()) {
return true;
}
// For the OverflowOutOfFlowsProperty I think we do enforce that, but it's
// a mix of out-of-flow frames, so that's why the method name has "Maybe".
return HasAnyStateBits(NS_BLOCK_HAS_OVERFLOW_OUT_OF_FLOWS);
}
/**
* Iterator over all descendant inline line boxes, except for those that are
* under an independent formatting context.
*/
class MOZ_RAII LineClampLineIterator {
public:
LineClampLineIterator(nsBlockFrame* aFrame,
const nsBlockFrame* aStopAtFrame)
: mCur(aFrame->LinesBegin()),
mEnd(aFrame->LinesEnd()),
mCurrentFrame(mCur == mEnd ? nullptr : aFrame),
mStopAtFrame(aStopAtFrame) {
if (mCur != mEnd && !mCur->IsInline()) {
Advance();
}
}
nsLineBox* GetCurrentLine() {
return mCurrentFrame ? mCur.get() : nullptr; }
nsBlockFrame* GetCurrentFrame() {
return mCurrentFrame; }
// Advances the iterator to the next line line.
//
// Next() shouldn't be called once the iterator is at the end, which can be
// checked for by GetCurrentLine() or GetCurrentFrame() returning null.
void Next() {
MOZ_ASSERT(mCur != mEnd && mCurrentFrame,
"Don't call Next() when the iterator is at the end");
++mCur;
Advance();
}
private:
void Advance() {
for (;;) {
if (mCur == mEnd) {
// Reached the end of the current block. Pop the parent off the
// stack; if there isn't one, then we've reached the end.
if (mStack.IsEmpty()) {
mCurrentFrame = nullptr;
break;
}
if (mCurrentFrame == mStopAtFrame) {
mStack.Clear();
mCurrentFrame = nullptr;
break;
}
auto entry = mStack.PopLastElement();
mCurrentFrame = entry.first;
mCur = entry.second;
mEnd = mCurrentFrame->LinesEnd();
}
else if (mCur->IsBlock()) {
if (nsBlockFrame* child = GetAsLineClampDescendant(mCur->mFirstChild)) {
nsBlockFrame::LineIterator next = mCur;
++next;
mStack.AppendElement(std::make_pair(mCurrentFrame, next));
mCur = child->LinesBegin();
mEnd = child->LinesEnd();
mCurrentFrame = child;
}
else {
// Some kind of frame we shouldn't descend into.
++mCur;
}
}
else {
MOZ_ASSERT(mCur->IsInline());
break;
}
}
}
// The current line within the current block.
//
// When this is equal to mEnd, the iterator is at its end, and mCurrentFrame
// is set to null.
nsBlockFrame::LineIterator mCur;
// The iterator end for the current block.
nsBlockFrame::LineIterator mEnd;
// The current block.
nsBlockFrame* mCurrentFrame;
// The block past which we can't look at line-clamp.
const nsBlockFrame* mStopAtFrame;
// Stack of mCurrentFrame and mEnd values that we push and pop as we enter and
// exist blocks.
AutoTArray<std::pair<nsBlockFrame*, nsBlockFrame::LineIterator>, 8> mStack;
};
static bool ClearLineClampEllipsis(nsBlockFrame* aFrame) {
if (aFrame->HasLineClampEllipsis()) {
MOZ_ASSERT(!aFrame->HasLineClampEllipsisDescendant());
aFrame->SetHasLineClampEllipsis(
false);
for (
auto& line : aFrame->Lines()) {
if (line.HasLineClampEllipsis()) {
line.ClearHasLineClampEllipsis();
break;
}
}
return true;
}
if (aFrame->HasLineClampEllipsisDescendant()) {
aFrame->SetHasLineClampEllipsisDescendant(
false);
for (nsIFrame* f : aFrame->PrincipalChildList()) {
if (nsBlockFrame* child = GetAsLineClampDescendant(f)) {
if (ClearLineClampEllipsis(child)) {
return true;
}
}
}
}
return false;
}
void nsBlockFrame::ClearLineClampEllipsis() { ::ClearLineClampEllipsis(
this); }
void nsBlockFrame::Reflow(nsPresContext* aPresContext, ReflowOutput& aMetrics,
const ReflowInput& aReflowInput,
nsReflowStatus& aStatus) {
if (IsHiddenByContentVisibilityOfInFlowParentForLayout()) {
FinishAndStoreOverflow(&aMetrics, aReflowInput.mStyleDisplay);
return;
}
MarkInReflow();
DO_GLOBAL_REFLOW_COUNT(
"nsBlockFrame");
MOZ_ASSERT(aStatus.IsEmpty(),
"Caller should pass a fresh reflow status!");
#ifdef DEBUG
if (gNoisyReflow) {
IndentBy(stdout, gNoiseIndent);
ListTag(stdout);
printf(
": begin reflow availSize=%d,%d computedSize=%d,%d\n",
aReflowInput.AvailableISize(), aReflowInput.AvailableBSize(),
aReflowInput.ComputedISize(), aReflowInput.ComputedBSize());
}
AutoNoisyIndenter indent(gNoisy);
PRTime start = 0;
// Initialize these variablies to silence the compiler.
int32_t ctc = 0;
// We only use these if they are set (gLameReflowMetrics).
if (gLameReflowMetrics) {
start = PR_Now();
ctc = nsLineBox::GetCtorCount();
}
#endif
// ColumnSetWrapper's children depend on ColumnSetWrapper's block-size or
// max-block-size because both affect the children's available block-size.
if (IsColumnSetWrapperFrame()) {
AddStateBits(NS_FRAME_CONTAINS_RELATIVE_BSIZE);
}
Maybe<nscoord> restoreReflowInputAvailBSize;
auto MaybeRestore = MakeScopeExit([&] {
if (MOZ_UNLIKELY(restoreReflowInputAvailBSize)) {
const_cast<ReflowInput&>(aReflowInput)
.SetAvailableBSize(*restoreReflowInputAvailBSize);
}
});
WritingMode wm = aReflowInput.GetWritingMode();
const nscoord consumedBSize = CalcAndCacheConsumedBSize();
const nscoord effectiveContentBoxBSize =
GetEffectiveComputedBSize(aReflowInput, consumedBSize);
// If we have non-auto block size, we're clipping our kids and we fit,
// make sure our kids fit too.
if (aReflowInput.AvailableBSize() != NS_UNCONSTRAINEDSIZE &&
aReflowInput.ComputedBSize() != NS_UNCONSTRAINEDSIZE &&
ShouldApplyOverflowClipping(aReflowInput.mStyleDisplay)
.contains(wm.PhysicalAxis(LogicalAxis::Block))) {
LogicalMargin blockDirExtras =
aReflowInput.ComputedLogicalBorderPadding(wm);
if (GetLogicalSkipSides().BStart()) {
blockDirExtras.BStart(wm) = 0;
}
else {
// Block-end margin never causes us to create continuations, so we
// don't need to worry about whether it fits in its entirety.
blockDirExtras.BStart(wm) +=
aReflowInput.ComputedLogicalMargin(wm).BStart(wm);
}
if (effectiveContentBoxBSize + blockDirExtras.BStartEnd(wm) <=
aReflowInput.AvailableBSize()) {
restoreReflowInputAvailBSize.emplace(aReflowInput.AvailableBSize());
const_cast<ReflowInput&>(aReflowInput)
.SetAvailableBSize(NS_UNCONSTRAINEDSIZE);
}
}
if (IsFrameTreeTooDeep(aReflowInput, aMetrics, aStatus)) {
return;
}
// OK, some lines may be reflowed. Blow away any saved line cursor
// because we may invalidate the nondecreasing
// overflowArea.InkOverflow().y/yMost invariant, and we may even
// delete the line with the line cursor.
ClearLineCursors();
// See comment below about oldSize. Use *only* for the
// abs-pos-containing-block-size-change optimization!
nsSize oldSize = GetSize();
// Should we create a float manager?
nsAutoFloatManager autoFloatManager(
const_cast<ReflowInput&>(aReflowInput));
// XXXldb If we start storing the float manager in the frame rather
// than keeping it around only during reflow then we should create it
// only when there are actually floats to manage. Otherwise things
// like tables will gain significant bloat.
//
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1931286:
// if we're a reflow root and no float manager is provided by the caller
// in aReflowInput, we'd normally expect the block to be a BFC and so
// BlockNeedsFloatManager will return true. But sometimes the block may
// have lost its BFC-ness since it was recorded as a dirty reflow root
// but before the reflow actually happens. Creating a float manager here
// avoids crashing, but may not be entirely correct in such a case.
bool needFloatManager =
!aReflowInput.mFloatManager || nsBlockFrame::BlockNeedsFloatManager(
this);
if (needFloatManager) {
autoFloatManager.CreateFloatManager(aPresContext);
}
if (HasAnyStateBits(NS_BLOCK_NEEDS_BIDI_RESOLUTION) &&
PresContext()->BidiEnabled()) {
static_cast<nsBlockFrame*>(FirstContinuation())->ResolveBidi();
}
// Whether to apply text-wrap: balance behavior.
bool tryBalance =
StyleText()->mTextWrapStyle == StyleTextWrapStyle::Balance &&
!GetPrevContinuation();
// Struct used to hold the "target" number of lines or clamp position to
// maintain when doing text-wrap: balance.
struct BalanceTarget {
// If line-clamp is in effect, mContent and mOffset indicate the starting
// position of the first line after the clamp limit, and mBlockCoord is the
// block-axis offset of its position.
// If line-clamp is not in use, mContent is null, mOffset is the total
// number of lines that the block must contain, and mBlockCoord is its end
// edge in the block direction.
nsIContent* mContent = nullptr;
int32_t mOffset = -1;
nscoord mBlockCoord = 0;
bool operator==(
const BalanceTarget& aOther)
const {
return mContent == aOther.mContent && mOffset == aOther.mOffset &&
mBlockCoord == aOther.mBlockCoord;
}
bool operator!=(
const BalanceTarget& aOther)
const {
return !(*
this == aOther);
}
};
BalanceTarget balanceTarget;
// Helpers for text-wrap: balance implementation:
// Count the number of inline lines in the mLines list, but return -1 (to
// suppress balancing) instead if the count is going to exceed aLimit.
auto countLinesUpTo = [&](int32_t aLimit) -> int32_t {
int32_t n = 0;
for (
auto iter = mLines.begin(); iter != mLines.end(); ++iter) {
// Block lines are ignored as they do not participate in balancing.
if (iter->IsInline() && ++n > aLimit) {
return -1;
}
}
return n;
};
// Return a BalanceTarget record representing the position at which line-clamp
// will take effect for the current line list. Only to be used when there are
// enough lines that the clamp will apply.
auto getClampPosition = [&](uint32_t aClampCount) -> BalanceTarget {
MOZ_ASSERT(aClampCount < mLines.size());
auto iter = mLines.begin();
for (uint32_t i = 0; i < aClampCount; i++) {
++iter;
}
nsIFrame* firstChild = iter->mFirstChild;
if (!firstChild) {
return BalanceTarget{};
}
nsIContent* content = firstChild->GetContent();
if (!content) {
return BalanceTarget{};
}
int32_t offset = 0;
if (firstChild->IsTextFrame()) {
auto* textFrame =
static_cast<nsTextFrame*>(firstChild);
offset = textFrame->GetContentOffset();
}
return BalanceTarget{content, offset, iter.get()->BStart()};
};
// "balancing" is implemented by shortening the effective inline-size of the
// lines, so that content will tend to be pushed down to fill later lines of
// the block. `balanceInset` is the current amount of "inset" to apply, and
// `balanceStep` is the increment to adjust it by for the next iteration.
nscoord balanceStep = 0;
// text-wrap: balance loop, executed only once if balancing is not required.
nsReflowStatus reflowStatus;
TrialReflowState trialState(consumedBSize, effectiveContentBoxBSize,
needFloatManager);
while (
true) {
// Save the initial floatManager state for repeated trial reflows.
// We'll restore (and re-save) the initial state each time we repeat the
// reflow.
nsFloatManager::SavedState floatManagerState;
aReflowInput.mFloatManager->PushState(&floatManagerState);
aMetrics = ReflowOutput(aMetrics.GetWritingMode());
reflowStatus =
TrialReflow(aPresContext, aMetrics, aReflowInput, trialState);
// Do we need to start a `text-wrap: balance` iteration?
if (tryBalance) {
tryBalance =
false;
// Don't try to balance an incomplete block, or if we had to use an
// overflow-wrap break position in the initial reflow.
if (!reflowStatus.IsFullyComplete() || trialState.mUsedOverflowWrap) {
break;
}
balanceTarget.mOffset =
countLinesUpTo(StaticPrefs::layout_css_text_wrap_balance_limit());
if (balanceTarget.mOffset < 2) {
// If there are less than 2 lines, or the number exceeds the limit,
// no balancing is needed; just break from the balance loop.
break;
}
balanceTarget.mBlockCoord = mLines.back()->BEnd();
// Initialize the amount of inset to try, and the iteration step size.
balanceStep = aReflowInput.ComputedISize() / balanceTarget.mOffset;
trialState.ResetForBalance(balanceStep);
balanceStep /= 2;
// If -webkit-line-clamp is in effect, then we need to maintain the
// content location at which clamping occurs, rather than the total
// number of lines in the block.
if (StaticPrefs::layout_css_text_wrap_balance_after_clamp_enabled() &&
IsLineClampRoot(
this)) {
uint32_t lineClampCount = aReflowInput.mStyleDisplay->mWebkitLineClamp;
if (uint32_t(balanceTarget.mOffset) > lineClampCount) {
auto t = getClampPosition(lineClampCount);
if (t.mContent) {
balanceTarget = t;
}
}
}
// Restore initial floatManager state for a new trial with updated inset.
aReflowInput.mFloatManager->PopState(&floatManagerState);
continue;
}
// Helper to determine whether the current trial succeeded (i.e. was able
// to fit the content into the expected number of lines).
auto trialSucceeded = [&]() ->
bool {
if (!reflowStatus.IsFullyComplete() || trialState.mUsedOverflowWrap) {
return false;
}
if (balanceTarget.mContent) {
auto t = getClampPosition(aReflowInput.mStyleDisplay->mWebkitLineClamp);
return t == balanceTarget;
}
int32_t numLines =
countLinesUpTo(StaticPrefs::layout_css_text_wrap_balance_limit());
return numLines == balanceTarget.mOffset &&
mLines.back()->BEnd() == balanceTarget.mBlockCoord;
};
// If we're in the process of a balance operation, check whether we've
// inset by too much and either increase or reduce the inset for the next
// iteration.
if (balanceStep > 0) {
if (trialSucceeded()) {
trialState.ResetForBalance(balanceStep);
}
else {
trialState.ResetForBalance(-balanceStep);
}
balanceStep /= 2;
aReflowInput.mFloatManager->PopState(&floatManagerState);
continue;
}
// If we were attempting to balance, check whether the final iteration was
// successful, and if not, back up by one step.
if (balanceTarget.mOffset >= 0) {
if (!trialState.mInset || trialSucceeded()) {
break;
}
trialState.ResetForBalance(-1);
aReflowInput.mFloatManager->PopState(&floatManagerState);
continue;
}
// If we reach here, no balancing was required, so just exit; we don't
// reset (pop) the floatManager state because this is the reflow we're
// going to keep. So the saved state is just dropped.
break;
}
// End of text-wrap: balance retry loop
// 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
// logical direction progresses in a negative physical direction, and
// therefore block-dir coordinate conversion depends on knowing the width
// of the coordinate space in order to translate between the logical and
// physical origins.
if (aReflowInput.GetWritingMode().IsVerticalRL()) {
nsSize containerSize = aMetrics.PhysicalSize();
nscoord deltaX = containerSize.width - trialState.mContainerWidth;
if (deltaX != 0) {
// We compute our lines and markers' overflow areas later in
// ComputeOverflowAreas(), so we don't need to adjust their overflow areas
// here.
const nsPoint physicalDelta(deltaX, 0);
for (
auto& line : Lines()) {
UpdateLineContainerSize(&line, containerSize);
}
trialState.mFcBounds.Clear();
if (nsFrameList* floats = GetFloats()) {
for (nsIFrame* f : *floats) {
f->MovePositionBy(physicalDelta);
ConsiderChildOverflow(trialState.mFcBounds, f);
}
}
if (nsFrameList* markerList = GetOutsideMarkerList()) {
for (nsIFrame* f : *markerList) {
f->MovePositionBy(physicalDelta);
}
}
if (nsFrameList* overflowContainers = GetOverflowContainers()) {
trialState.mOcBounds.Clear();
for (nsIFrame* f : *overflowContainers) {
f->MovePositionBy(physicalDelta);
ConsiderChildOverflow(trialState.mOcBounds, f);
}
}
}
}
aMetrics.SetOverflowAreasToDesiredBounds();
ComputeOverflowAreas(aMetrics.mOverflowAreas, aReflowInput.mStyleDisplay);
// Factor overflow container child bounds into the overflow area
aMetrics.mOverflowAreas.UnionWith(trialState.mOcBounds);
// Factor pushed float child bounds into the overflow area
aMetrics.mOverflowAreas.UnionWith(trialState.mFcBounds);
// Let the absolutely positioned container reflow any absolutely positioned
// child frames that need to be reflowed, e.g., elements with a percentage
// based width/height
// We want to do this under either of two conditions:
// 1. If we didn't do the incremental reflow above.
// 2. If our size changed.
// Even though it's the padding edge that's the containing block, we
// can use our rect (the border edge) since if the border style
// changed, the reflow would have been targeted at us so we'd satisfy
// condition 1.
// XXX checking oldSize is bogus, there are various reasons we might have
// reflowed but our size might not have been changed to what we
// asked for (e.g., we ended up being pushed to a new page)
// When WillReflowAgainForClearance is true, we will reflow again without
// resetting the size. Because of this, we must not reflow our abs-pos
// children in that situation --- what we think is our "new size" will not be
// our real new size. This also happens to be more efficient.
WritingMode parentWM = aMetrics.GetWritingMode();
if (HasAbsolutelyPositionedChildren()) {
nsAbsoluteContainingBlock* absoluteContainer = GetAbsoluteContainingBlock();
bool haveInterrupt = aPresContext->HasPendingInterrupt();
if (aReflowInput.WillReflowAgainForClearance() || haveInterrupt) {
// Make sure that when we reflow again we'll actually reflow all the abs
// pos frames that might conceivably depend on our size (or all of them,
// if we're dirty right now and interrupted; in that case we also need
// to mark them all with NS_FRAME_IS_DIRTY). Sadly, we can't do much
// better than that, because we don't really know what our size will be,
// and it might in fact not change on the followup reflow!
if (haveInterrupt && HasAnyStateBits(NS_FRAME_IS_DIRTY)) {
absoluteContainer->MarkAllFramesDirty();
}
else {
absoluteContainer->MarkSizeDependentFramesDirty();
}
if (haveInterrupt) {
// We're not going to reflow absolute frames; make sure to account for
// their existing overflow areas, which is usually a side effect of this
// reflow.
//
// TODO(emilio): nsAbsoluteContainingBlock::Reflow already checks for
// interrupt, can we just rely on it and unconditionally take the else
// branch below? That's a bit more subtle / risky, since I don't see
// what would reflow them in that case if they depended on our size.
for (nsIFrame* kid = absoluteContainer->GetChildList().FirstChild();
kid; kid = kid->GetNextSibling()) {
ConsiderChildOverflow(aMetrics.mOverflowAreas, kid);
}
}
}
else {
LogicalSize containingBlockSize =
CalculateContainingBlockSizeForAbsolutes(parentWM, aReflowInput,
aMetrics.Size(parentWM));
// Mark frames that depend on changes we just made to this frame as dirty:
// Now we can assume that the padding edge hasn't moved.
// We need to reflow the absolutes if one of them depends on
// its placeholder position, or the containing block size in a
// direction in which the containing block size might have
// changed.
// XXX "width" and "height" in this block will become ISize and BSize
// when nsAbsoluteContainingBlock is logicalized
bool cbWidthChanged = aMetrics.Width() != oldSize.width;
bool isRoot = !GetContent()->GetParent();
// If isRoot and we have auto height, then we are the initial
// containing block and the containing block height is the
// viewport height, which can't change during incremental
// reflow.
bool cbHeightChanged =
!(isRoot && NS_UNCONSTRAINEDSIZE == aReflowInput.ComputedHeight()) &&
aMetrics.Height() != oldSize.height;
nsRect containingBlock(nsPoint(0, 0),
containingBlockSize.GetPhysicalSize(parentWM));
AbsPosReflowFlags flags = AbsPosReflowFlags::ConstrainHeight;
if (cbWidthChanged) {
flags |= AbsPosReflowFlags::CBWidthChanged;
}
if (cbHeightChanged) {
flags |= AbsPosReflowFlags::CBHeightChanged;
}
// Setup the line cursor here to optimize line searching for
// calculating hypothetical position of absolutely-positioned
// frames.
SetupLineCursorForQuery();
--> --------------------
--> maximum size reached
--> --------------------