summaryrefslogtreecommitdiffstats
path: root/layout/generic/nsPageFrame.cpp
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--layout/generic/nsPageFrame.cpp980
1 files changed, 980 insertions, 0 deletions
diff --git a/layout/generic/nsPageFrame.cpp b/layout/generic/nsPageFrame.cpp
new file mode 100644
index 0000000000..fb9cfa2e81
--- /dev/null
+++ b/layout/generic/nsPageFrame.cpp
@@ -0,0 +1,980 @@
+/* -*- 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/. */
+
+#include "nsPageFrame.h"
+
+#include "mozilla/AppUnits.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/StaticPrefs_layout.h"
+#include "mozilla/gfx/2D.h"
+#include "mozilla/intl/Segmenter.h"
+#include "gfxContext.h"
+#include "nsDeviceContext.h"
+#include "nsFontMetrics.h"
+#include "nsIFrame.h"
+#include "nsLayoutUtils.h"
+#include "nsPresContext.h"
+#include "nsGkAtoms.h"
+#include "nsFieldSetFrame.h"
+#include "nsPageContentFrame.h"
+#include "nsDisplayList.h"
+#include "nsPageSequenceFrame.h" // for nsSharedPageData
+#include "nsTextFormatter.h" // for page number localization formatting
+#include "nsBidiUtils.h"
+#include "nsIPrintSettings.h"
+#include "PrintedSheetFrame.h"
+
+#include "mozilla/Logging.h"
+extern mozilla::LazyLogModule gLayoutPrintingLog;
+#define PR_PL(_p1) MOZ_LOG(gLayoutPrintingLog, mozilla::LogLevel::Debug, _p1)
+
+using namespace mozilla;
+using namespace mozilla::gfx;
+
+nsPageFrame* NS_NewPageFrame(PresShell* aPresShell, ComputedStyle* aStyle) {
+ return new (aPresShell) nsPageFrame(aStyle, aPresShell->GetPresContext());
+}
+
+NS_IMPL_FRAMEARENA_HELPERS(nsPageFrame)
+
+NS_QUERYFRAME_HEAD(nsPageFrame)
+ NS_QUERYFRAME_ENTRY(nsPageFrame)
+NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame)
+
+nsPageFrame::nsPageFrame(ComputedStyle* aStyle, nsPresContext* aPresContext)
+ : nsContainerFrame(aStyle, aPresContext, kClassID) {}
+
+nsPageFrame::~nsPageFrame() = default;
+
+nsReflowStatus nsPageFrame::ReflowPageContent(
+ nsPresContext* aPresContext, const ReflowInput& aPageReflowInput) {
+ nsPageContentFrame* const frame = PageContentFrame();
+ // If this is the first page, it won't have its page name and computed style
+ // set yet. Before reflow, make sure that page name and computed style have
+ // been applied.
+ frame->EnsurePageName();
+ // XXX Pay attention to the page's border and padding...
+ //
+ // Reflow our ::-moz-page-content frame, allowing it only to be as big as we
+ // are (minus margins).
+ const nsSize pageSize = ComputePageSize();
+ // Scaling applied to the page in the single page-per-sheet case (used for
+ // down-scaling when the page is too large to fit on the sheet we are printing
+ // on). In the single page-per-sheet case, we need this here to preemptively
+ // increase the margins by the same amount that the scaling will reduce them
+ // in order to make sure that their physical size is unchanged (particularly
+ // important for the unwriteable margins).
+ const auto* ppsInfo = GetSharedPageData()->PagesPerSheetInfo();
+ const float pageSizeScale =
+ ppsInfo->mNumPages == 1 ? ComputeSinglePPSPageSizeScale(pageSize) : 1.0f;
+ // Scaling applied to content, as given by the print UI.
+ // This is an additional scale factor that is applied to the content in the
+ // nsPageContentFrame.
+ const float extraContentScale = aPresContext->GetPageScale();
+ // Size for the page content. This will be scaled by extraContentScale, and
+ // is used to calculate the computed size of the nsPageContentFrame content
+ // by subtracting margins.
+ nsSize availableSpace = pageSize;
+
+ // When the reflow size is NS_UNCONSTRAINEDSIZE it means we are reflowing
+ // a single page to print selection. So this means we want to use
+ // NS_UNCONSTRAINEDSIZE without altering it.
+ //
+ // FIXME(emilio): Is this still true?
+ availableSpace.width =
+ NSToCoordCeil(availableSpace.width / extraContentScale);
+ if (availableSpace.height != NS_UNCONSTRAINEDSIZE) {
+ availableSpace.height =
+ NSToCoordCeil(availableSpace.height / extraContentScale);
+ }
+
+ // Get the number of Twips per pixel from the PresContext
+ const nscoord onePixel = AppUnitsPerCSSPixel();
+
+ // insurance against infinite reflow, when reflowing less than a pixel
+ // XXX Shouldn't we do something more friendly when invalid margins
+ // are set?
+ if (availableSpace.width < onePixel || availableSpace.height < onePixel) {
+ NS_WARNING("Reflow aborted; no space for content");
+ return {};
+ }
+
+ ReflowInput kidReflowInput(
+ aPresContext, aPageReflowInput, frame,
+ LogicalSize(frame->GetWritingMode(), availableSpace));
+ kidReflowInput.mFlags.mIsTopOfPage = true;
+ kidReflowInput.mFlags.mTableIsSplittable = true;
+
+ nsMargin defaultMargins = aPresContext->GetDefaultPageMargin();
+ // The default margins are in the coordinate space of the physical paper.
+ // Scale them by the pageSizeScale to convert them to the content coordinate
+ // space.
+ for (const auto side : mozilla::AllPhysicalSides()) {
+ defaultMargins.Side(side) =
+ NSToCoordRound((float)defaultMargins.Side(side) / pageSizeScale);
+ }
+ mPageContentMargin = defaultMargins;
+
+ // Use the margins given in the @page rule if told to do so.
+ // We clamp to the paper's unwriteable margins to avoid clipping, *except*
+ // that we will respect a margin of zero if specified, assuming this means
+ // the document is intended to fit the paper size exactly, and the client is
+ // taking full responsibility for what happens around the edges.
+ if (mPD->mPrintSettings->GetHonorPageRuleMargins()) {
+ const auto& margin = kidReflowInput.mStyleMargin->mMargin;
+ for (const auto side : mozilla::AllPhysicalSides()) {
+ if (!margin.Get(side).IsAuto()) {
+ // Computed margins are already in the coordinate space of the content,
+ // do not scale.
+ const nscoord computed =
+ kidReflowInput.ComputedPhysicalMargin().Side(side);
+ // Respecting a zero margin is particularly important when the client
+ // is PDF.js where the PDF already contains the margins.
+ // User could also be asking to ignore unwriteable margins (Though
+ // currently, it is impossible through the print UI to set both
+ // `HonorPageRuleMargins` and `IgnoreUnwriteableMargins`).
+ if (computed == 0 ||
+ mPD->mPrintSettings->GetIgnoreUnwriteableMargins()) {
+ mPageContentMargin.Side(side) = computed;
+ } else {
+ // Unwriteable margins are in the coordinate space of the physical
+ // paper. Scale them by the pageSizeScale to convert them to the
+ // content coordinate space.
+ const int32_t unwriteableTwips =
+ mPD->mPrintSettings->GetUnwriteableMarginInTwips().Side(side);
+ const nscoord unwriteable = nsPresContext::CSSTwipsToAppUnits(
+ (float)unwriteableTwips / pageSizeScale);
+ mPageContentMargin.Side(side) = std::max(
+ kidReflowInput.ComputedPhysicalMargin().Side(side), unwriteable);
+ }
+ }
+ }
+ }
+
+ // TODO: This seems odd that we need to scale the margins by the extra
+ // scale factor, but this is needed for correct margins.
+ // Why are the margins already scaled? Shouldn't they be stored so that this
+ // scaling factor would be redundant?
+ nscoord computedWidth =
+ availableSpace.width - mPageContentMargin.LeftRight() / extraContentScale;
+ nscoord computedHeight;
+ if (availableSpace.height == NS_UNCONSTRAINEDSIZE) {
+ computedHeight = NS_UNCONSTRAINEDSIZE;
+ } else {
+ computedHeight = availableSpace.height -
+ mPageContentMargin.TopBottom() / extraContentScale;
+ }
+
+ // Check the width and height, if they're too small we reset the margins
+ // back to the default.
+ if (computedWidth < onePixel || computedHeight < onePixel) {
+ mPageContentMargin = defaultMargins;
+ computedWidth = availableSpace.width -
+ mPageContentMargin.LeftRight() / extraContentScale;
+ if (computedHeight != NS_UNCONSTRAINEDSIZE) {
+ computedHeight = availableSpace.height -
+ mPageContentMargin.TopBottom() / extraContentScale;
+ }
+ // And if they're still too small, we give up.
+ if (computedWidth < onePixel || computedHeight < onePixel) {
+ NS_WARNING("Reflow aborted; no space for content");
+ return {};
+ }
+ }
+
+ kidReflowInput.SetComputedWidth(computedWidth);
+ kidReflowInput.SetComputedHeight(computedHeight);
+
+ // calc location of frame
+ const nscoord xc = mPageContentMargin.left;
+ const nscoord yc = mPageContentMargin.top;
+
+ // Get the child's desired size
+ ReflowOutput kidOutput(kidReflowInput);
+ nsReflowStatus kidStatus;
+ ReflowChild(frame, aPresContext, kidOutput, kidReflowInput, xc, yc,
+ ReflowChildFlags::Default, kidStatus);
+
+ // Place and size the child
+ FinishReflowChild(frame, aPresContext, kidOutput, &kidReflowInput, xc, yc,
+ ReflowChildFlags::Default);
+
+ NS_ASSERTION(!kidStatus.IsFullyComplete() || !frame->GetNextInFlow(),
+ "bad child flow list");
+ return kidStatus;
+}
+
+void nsPageFrame::Reflow(nsPresContext* aPresContext,
+ ReflowOutput& aReflowOutput,
+ const ReflowInput& aReflowInput,
+ nsReflowStatus& aStatus) {
+ MarkInReflow();
+ DO_GLOBAL_REFLOW_COUNT("nsPageFrame");
+ DISPLAY_REFLOW(aPresContext, this, aReflowInput, aReflowOutput, aStatus);
+ MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!");
+ MOZ_ASSERT(mPD, "Need a pointer to nsSharedPageData before reflow starts");
+
+ // Our status is the same as our child's.
+ aStatus = ReflowPageContent(aPresContext, aReflowInput);
+
+ PR_PL(("PageFrame::Reflow %p ", this));
+ PR_PL(("[%d,%d][%d,%d]\n", aReflowOutput.Width(), aReflowOutput.Height(),
+ aReflowInput.AvailableWidth(), aReflowInput.AvailableHeight()));
+
+ // Return our desired size
+ WritingMode wm = aReflowInput.GetWritingMode();
+ aReflowOutput.ISize(wm) = aReflowInput.AvailableISize();
+ if (aReflowInput.AvailableBSize() != NS_UNCONSTRAINEDSIZE) {
+ aReflowOutput.BSize(wm) = aReflowInput.AvailableBSize();
+ }
+
+ aReflowOutput.SetOverflowAreasToDesiredBounds();
+ FinishAndStoreOverflow(&aReflowOutput);
+
+ PR_PL(("PageFrame::Reflow %p ", this));
+ PR_PL(("[%d,%d]\n", aReflowInput.AvailableWidth(),
+ aReflowInput.AvailableHeight()));
+}
+
+#ifdef DEBUG_FRAME_DUMP
+nsresult nsPageFrame::GetFrameName(nsAString& aResult) const {
+ return MakeFrameName(u"Page"_ns, aResult);
+}
+#endif
+
+void nsPageFrame::ProcessSpecialCodes(const nsString& aStr, nsString& aNewStr) {
+ aNewStr = aStr;
+
+ // Search to see if the &D code is in the string
+ // then subst in the current date/time
+ constexpr auto kDate = u"&D"_ns;
+ if (aStr.Find(kDate) != kNotFound) {
+ aNewStr.ReplaceSubstring(kDate, mPD->mDateTimeStr);
+ }
+
+ // NOTE: Must search for &PT before searching for &P
+ //
+ // Search to see if the "page number and page" total code are in the string
+ // and replace the page number and page total code with the actual
+ // values
+ constexpr auto kPageAndTotal = u"&PT"_ns;
+ if (aStr.Find(kPageAndTotal) != kNotFound) {
+ nsAutoString uStr;
+ nsTextFormatter::ssprintf(uStr, mPD->mPageNumAndTotalsFormat.get(),
+ mPageNum, mPD->mRawNumPages);
+ aNewStr.ReplaceSubstring(kPageAndTotal, uStr);
+ }
+
+ // Search to see if the page number code is in the string
+ // and replace the page number code with the actual value
+ constexpr auto kPage = u"&P"_ns;
+ if (aStr.Find(kPage) != kNotFound) {
+ nsAutoString uStr;
+ nsTextFormatter::ssprintf(uStr, mPD->mPageNumFormat.get(), mPageNum);
+ aNewStr.ReplaceSubstring(kPage, uStr);
+ }
+
+ constexpr auto kTitle = u"&T"_ns;
+ if (aStr.Find(kTitle) != kNotFound) {
+ aNewStr.ReplaceSubstring(kTitle, mPD->mDocTitle);
+ }
+
+ constexpr auto kDocURL = u"&U"_ns;
+ if (aStr.Find(kDocURL) != kNotFound) {
+ aNewStr.ReplaceSubstring(kDocURL, mPD->mDocURL);
+ }
+
+ constexpr auto kPageTotal = u"&L"_ns;
+ if (aStr.Find(kPageTotal) != kNotFound) {
+ nsAutoString uStr;
+ nsTextFormatter::ssprintf(uStr, mPD->mPageNumFormat.get(),
+ mPD->mRawNumPages);
+ aNewStr.ReplaceSubstring(kPageTotal, uStr);
+ }
+}
+
+//------------------------------------------------------------------------------
+nscoord nsPageFrame::GetXPosition(gfxContext& aRenderingContext,
+ nsFontMetrics& aFontMetrics,
+ const nsRect& aRect, int32_t aJust,
+ const nsString& aStr) {
+ nscoord width = nsLayoutUtils::AppUnitWidthOfStringBidi(
+ aStr, this, aFontMetrics, aRenderingContext);
+ nscoord x = aRect.x;
+ switch (aJust) {
+ case nsIPrintSettings::kJustLeft:
+ x += mPD->mEdgePaperMargin.left;
+ break;
+
+ case nsIPrintSettings::kJustCenter:
+ x += (aRect.width - width) / 2;
+ break;
+
+ case nsIPrintSettings::kJustRight:
+ x += aRect.width - width - mPD->mEdgePaperMargin.right;
+ break;
+ } // switch
+
+ return x;
+}
+
+// Draw a header or footer
+// @param aRenderingContext - rendering content to draw into
+// @param aHeaderFooter - indicates whether it is a header or footer
+// @param aStrLeft - string for the left header or footer; can be empty
+// @param aStrCenter - string for the center header or footer; can be empty
+// @param aStrRight - string for the right header or footer; can be empty
+// @param aRect - the rect of the page
+// @param aAscent - the ascent of the font
+// @param aHeight - the height of the font
+void nsPageFrame::DrawHeaderFooter(
+ gfxContext& aRenderingContext, nsFontMetrics& aFontMetrics,
+ nsHeaderFooterEnum aHeaderFooter, const nsString& aStrLeft,
+ const nsString& aStrCenter, const nsString& aStrRight, const nsRect& aRect,
+ nscoord aAscent, nscoord aHeight) {
+ int32_t numStrs = 0;
+ if (!aStrLeft.IsEmpty()) numStrs++;
+ if (!aStrCenter.IsEmpty()) numStrs++;
+ if (!aStrRight.IsEmpty()) numStrs++;
+
+ if (numStrs == 0) return;
+ const nscoord contentWidth =
+ aRect.width - (mPD->mEdgePaperMargin.left + mPD->mEdgePaperMargin.right);
+ const nscoord strSpace = contentWidth / numStrs;
+
+ if (!aStrLeft.IsEmpty()) {
+ DrawHeaderFooter(aRenderingContext, aFontMetrics, aHeaderFooter,
+ nsIPrintSettings::kJustLeft, aStrLeft, aRect, aAscent,
+ aHeight, strSpace);
+ }
+ if (!aStrCenter.IsEmpty()) {
+ DrawHeaderFooter(aRenderingContext, aFontMetrics, aHeaderFooter,
+ nsIPrintSettings::kJustCenter, aStrCenter, aRect, aAscent,
+ aHeight, strSpace);
+ }
+ if (!aStrRight.IsEmpty()) {
+ DrawHeaderFooter(aRenderingContext, aFontMetrics, aHeaderFooter,
+ nsIPrintSettings::kJustRight, aStrRight, aRect, aAscent,
+ aHeight, strSpace);
+ }
+}
+
+// Draw a header or footer string
+// @param aRenderingContext - rendering context to draw into
+// @param aHeaderFooter - indicates whether it is a header or footer
+// @param aJust - indicates where the string is located within the header/footer
+// @param aStr - the string to be drawn
+// @param aRect - the rect of the page
+// @param aHeight - the height of the font
+// @param aAscent - the ascent of the font
+// @param aWidth - available width for the string
+void nsPageFrame::DrawHeaderFooter(gfxContext& aRenderingContext,
+ nsFontMetrics& aFontMetrics,
+ nsHeaderFooterEnum aHeaderFooter,
+ int32_t aJust, const nsString& aStr,
+ const nsRect& aRect, nscoord aAscent,
+ nscoord aHeight, nscoord aWidth) {
+ DrawTarget* drawTarget = aRenderingContext.GetDrawTarget();
+
+ if ((aHeaderFooter == eHeader && aHeight < mPageContentMargin.top) ||
+ (aHeaderFooter == eFooter && aHeight < mPageContentMargin.bottom)) {
+ nsAutoString str;
+ ProcessSpecialCodes(aStr, str);
+
+ int32_t len = (int32_t)str.Length();
+ if (len == 0) {
+ return; // bail is empty string
+ }
+
+ int32_t index;
+ int32_t textWidth = 0;
+ const char16_t* text = str.get();
+ // find how much text fits, the "position" is the size of the available area
+ if (nsLayoutUtils::BinarySearchForPosition(drawTarget, aFontMetrics, text,
+ 0, 0, 0, len, int32_t(aWidth),
+ index, textWidth)) {
+ if (index < len - 1) {
+ // we can't fit in all the text, try to remove 3 glyphs and append
+ // three "." charactrers.
+
+ // TODO: This might not actually remove three glyphs in cases where
+ // ZWJ sequences, regional indicators, etc are used.
+ // We also have guarantee that removing three glyphs will make enough
+ // space for the ellipse, if they are zero-width or even just narrower
+ // than the "." character.
+ // See https://bugzilla.mozilla.org/1765008
+ mozilla::intl::GraphemeClusterBreakReverseIteratorUtf16 revIter(str);
+
+ // Start iteration at the point where the text does properly fit.
+ revIter.Seek(index);
+
+ // Step backwards 3 times, checking if we have any string left by the
+ // end.
+ revIter.Next();
+ revIter.Next();
+ if (const Maybe<uint32_t> maybeIndex = revIter.Next()) {
+ // TODO: We should consider checking for the ellipse character, or
+ // possibly for another continuation indicator based on
+ // localization.
+ // See https://bugzilla.mozilla.org/1765007
+ str.Truncate(*maybeIndex);
+ str.AppendLiteral("...");
+ } else {
+ // We can only fit 3 or fewer chars. Just show nothing
+ str.Truncate();
+ }
+ }
+ } else {
+ return; // bail if couldn't find the correct length
+ }
+
+ if (HasRTLChars(str)) {
+ PresContext()->SetBidiEnabled();
+ }
+
+ // calc the x and y positions of the text
+ nscoord x =
+ GetXPosition(aRenderingContext, aFontMetrics, aRect, aJust, str);
+ nscoord y;
+ if (aHeaderFooter == eHeader) {
+ y = aRect.y + mPD->mEdgePaperMargin.top;
+ } else {
+ y = aRect.YMost() - aHeight - mPD->mEdgePaperMargin.bottom;
+ }
+
+ // set up new clip and draw the text
+ aRenderingContext.Save();
+ aRenderingContext.Clip(NSRectToSnappedRect(
+ aRect, PresContext()->AppUnitsPerDevPixel(), *drawTarget));
+ aRenderingContext.SetColor(sRGBColor::OpaqueBlack());
+ nsLayoutUtils::DrawString(this, aFontMetrics, &aRenderingContext, str.get(),
+ str.Length(), nsPoint(x, y + aAscent), nullptr,
+ DrawStringFlags::ForceHorizontal);
+ aRenderingContext.Restore();
+ }
+}
+
+class nsDisplayHeaderFooter final : public nsPaintedDisplayItem {
+ public:
+ nsDisplayHeaderFooter(nsDisplayListBuilder* aBuilder, nsPageFrame* aFrame)
+ : nsPaintedDisplayItem(aBuilder, aFrame) {
+ MOZ_COUNT_CTOR(nsDisplayHeaderFooter);
+ }
+ MOZ_COUNTED_DTOR_OVERRIDE(nsDisplayHeaderFooter)
+
+ virtual void Paint(nsDisplayListBuilder* aBuilder,
+ gfxContext* aCtx) override {
+#ifdef DEBUG
+ nsPageFrame* pageFrame = do_QueryFrame(mFrame);
+ MOZ_ASSERT(pageFrame, "We should have an nsPageFrame");
+#endif
+ static_cast<nsPageFrame*>(mFrame)->PaintHeaderFooter(
+ *aCtx, ToReferenceFrame(), false);
+ }
+ NS_DISPLAY_DECL_NAME("HeaderFooter", TYPE_HEADER_FOOTER)
+
+ virtual nsRect GetComponentAlphaBounds(
+ nsDisplayListBuilder* aBuilder) const override {
+ bool snap;
+ return GetBounds(aBuilder, &snap);
+ }
+};
+
+static void PaintMarginGuides(nsIFrame* aFrame, DrawTarget* aDrawTarget,
+ const nsRect& aDirtyRect, nsPoint aPt) {
+ // Set up parameters needed to draw the guides: we draw them in blue,
+ // using 2px-long dashes with 2px separation and a line width of 0.5px.
+ // Drawing is antialiased, so on a non-hidpi screen where the line width is
+ // less than one device pixel, it doesn't disappear but renders fainter
+ // than a solid 1px-wide line would be.
+ // (In many cases, the entire preview is scaled down so that the guides
+ // will be nominally less than 1 dev px even on a hidpi screen, resulting
+ // in lighter antialiased rendering so they don't dominate the page.)
+ ColorPattern pattern(ToDeviceColor(sRGBColor(0.0f, 0.0f, 1.0f)));
+ Float dashes[] = {2.0f, 2.0f};
+ StrokeOptions stroke(/* line width (in CSS px) */ 0.5f,
+ JoinStyle::MITER_OR_BEVEL, CapStyle::BUTT,
+ /* mitre limit (default, not used) */ 10.0f,
+ /* set dash pattern of 2px stroke, 2px gap */
+ ArrayLength(dashes), dashes,
+ /* dash offset */ 0.0f);
+ DrawOptions options;
+
+ MOZ_RELEASE_ASSERT(aFrame->IsPageFrame());
+ const nsMargin& margin =
+ static_cast<nsPageFrame*>(aFrame)->GetUsedPageContentMargin();
+ int32_t appUnitsPerDevPx = aFrame->PresContext()->AppUnitsPerDevPixel();
+
+ // Get the frame's rect and inset by the margins to get the edges of the
+ // content area, where we want to draw the guides.
+ // We draw in two stages, first applying the top/bottom margins and drawing
+ // the horizontal guides across the full width of the page.
+ nsRect rect(aPt, aFrame->GetSize());
+ rect.Deflate(nsMargin(margin.top, 0, margin.bottom, 0));
+ Rect r = NSRectToRect(rect, appUnitsPerDevPx);
+ aDrawTarget->StrokeLine(r.TopLeft(), r.TopRight(), pattern, stroke, options);
+ aDrawTarget->StrokeLine(r.BottomLeft(), r.BottomRight(), pattern, stroke,
+ options);
+
+ // Then reset rect, apply the left/right margins, and draw vertical guides
+ // extending the full height of the page.
+ rect = nsRect(aPt, aFrame->GetSize());
+ rect.Deflate(nsMargin(0, margin.right, 0, margin.left));
+ r = NSRectToRect(rect, appUnitsPerDevPx);
+ aDrawTarget->StrokeLine(r.TopLeft(), r.BottomLeft(), pattern, stroke,
+ options);
+ aDrawTarget->StrokeLine(r.TopRight(), r.BottomRight(), pattern, stroke,
+ options);
+}
+
+static std::tuple<uint32_t, uint32_t> GetRowAndColFromIdx(uint32_t aIdxOnSheet,
+ uint32_t aNumCols) {
+ // Compute the row index by *dividing* the item's ordinal position by how
+ // many items fit in each row (i.e. the number of columns), and flooring.
+ // Compute the column index by getting the remainder of that division:
+ // Notably, mNumRows is irrelevant to this computation; that's because
+ // we're adding new items column-by-column rather than row-by-row.
+ return {aIdxOnSheet / aNumCols, aIdxOnSheet % aNumCols};
+}
+
+// Helper for BuildDisplayList:
+static gfx::Matrix4x4 ComputePagesPerSheetAndPageSizeTransform(
+ const nsIFrame* aFrame, float aAppUnitsPerPixel) {
+ MOZ_ASSERT(aFrame->IsPageFrame());
+ auto* pageFrame = static_cast<const nsPageFrame*>(aFrame);
+ const nsSize contentPageSize = pageFrame->ComputePageSize();
+ MOZ_ASSERT(contentPageSize.width > 0 && contentPageSize.height > 0);
+ nsSharedPageData* pd = pageFrame->GetSharedPageData();
+ const auto* ppsInfo = pd->PagesPerSheetInfo();
+
+ const nsContainerFrame* const parentFrame = pageFrame->GetParent();
+ MOZ_ASSERT(parentFrame->IsPrintedSheetFrame(),
+ "Parent of nsPageFrame should be PrintedSheetFrame");
+ const auto* sheetFrame = static_cast<const PrintedSheetFrame*>(parentFrame);
+
+ const double rotation =
+ pageFrame->GetPageOrientationRotation(pageFrame->GetSharedPageData());
+
+ gfx::Matrix4x4 transform;
+
+ if (ppsInfo->mNumPages == 1) {
+ if (rotation != 0.0) {
+ const nsSize sheetSize = sheetFrame->GetSizeForChildren();
+ const bool sheetIsPortrait = sheetSize.width < sheetSize.height;
+ const bool rotatingClockwise = rotation > 0.0;
+
+ // rotation point:
+ int32_t x, y;
+ if (rotatingClockwise != sheetIsPortrait) {
+ // rotating portrait clockwise, or landscape counterclockwise
+ x = y = std::min(sheetSize.width, sheetSize.height) / 2;
+ } else {
+ // rotating portrait counterclockwise, or landscape clockwise
+ x = y = std::max(sheetSize.width, sheetSize.height) / 2;
+ }
+
+ transform = gfx::Matrix4x4::Translation(
+ NSAppUnitsToFloatPixels(x, aAppUnitsPerPixel),
+ NSAppUnitsToFloatPixels(y, aAppUnitsPerPixel), 0);
+ transform.RotateZ(rotation);
+ transform.PreTranslate(NSAppUnitsToFloatPixels(-x, aAppUnitsPerPixel),
+ NSAppUnitsToFloatPixels(-y, aAppUnitsPerPixel), 0);
+ }
+
+ float scale = pageFrame->ComputeSinglePPSPageSizeScale(contentPageSize);
+ transform.PreScale(scale, scale, 1);
+ return transform;
+ }
+
+ // The multiple pages-per-sheet case.
+
+ // Begin with the translation of the page to its pages-per-sheet grid "cell"
+ // (the grid origin accounts for the sheet's unwriteable margins):
+ const nsPoint gridOrigin = sheetFrame->GetGridOrigin();
+ const nscoord cellWidth = sheetFrame->GetGridCellWidth();
+ const nscoord cellHeight = sheetFrame->GetGridCellHeight();
+ uint32_t rowIdx, colIdx;
+ std::tie(rowIdx, colIdx) = GetRowAndColFromIdx(pageFrame->IndexOnSheet(),
+ sheetFrame->GetGridNumCols());
+ transform = gfx::Matrix4x4::Translation(
+ NSAppUnitsToFloatPixels(gridOrigin.x + nscoord(colIdx) * cellWidth,
+ aAppUnitsPerPixel),
+ NSAppUnitsToFloatPixels(gridOrigin.y + nscoord(rowIdx) * cellHeight,
+ aAppUnitsPerPixel),
+ 0.0f);
+
+ // Scale the page to fit, centered, in the grid cell:
+ float scaleX = float(cellWidth) / float(contentPageSize.width);
+ float scaleY = float(cellHeight) / float(contentPageSize.height);
+ MOZ_ASSERT(scaleX > 0.0f && scaleX <= 1.0f && scaleY > 0.0f &&
+ scaleY <= 1.0f);
+ float scale;
+ float dx = 0.0f, dy = 0.0f;
+ if (scaleX < scaleY) {
+ scale = scaleX;
+ // We need to scale down more for the width than the height, so we'll have
+ // some spare space in the page's vertical direction. We offset the page
+ // to share that space equally above and below the page to center it.
+ nscoord extraSpace =
+ cellHeight - NSToCoordRound(float(contentPageSize.height) * scale);
+ dy = NSAppUnitsToFloatPixels(extraSpace / 2, aAppUnitsPerPixel);
+ } else {
+ scale = scaleY;
+ nscoord extraSpace =
+ cellWidth - NSToCoordRound(float(contentPageSize.width) * scale);
+ dx = NSAppUnitsToFloatPixels(extraSpace / 2, aAppUnitsPerPixel);
+ }
+ transform.PreTranslate(dx, dy, 0.0f);
+ transform.PreScale(scale, scale, 1.0f);
+
+ // Apply 'page-orientation' rotation, if applicable:
+ if (rotation != 0.0) {
+ // We've already translated and scaled the page to fit the cell, ignoring
+ // rotation. Here we rotate the page around its center and, if necessary,
+ // also scale it to fit it to its cell for its orientation change.
+
+ float fitScale = 1.0f;
+ if (MOZ_LIKELY(cellWidth != cellHeight &&
+ contentPageSize.width != contentPageSize.height)) {
+ // If neither the cell nor the page are square, the scale must change.
+ float cellRatio = float(cellWidth) / float(cellHeight);
+ float pageRatio =
+ float(contentPageSize.width) / float(contentPageSize.height);
+ const bool orientationWillMatchAfterRotation =
+ floor(cellRatio) != floor(pageRatio);
+ if (cellRatio > 1.0f) {
+ cellRatio = 1.0f / cellRatio; // normalize
+ }
+ if (pageRatio > 1.0f) {
+ pageRatio = 1.0f / pageRatio; // normalize
+ }
+ fitScale = std::max(cellRatio, pageRatio);
+ if (orientationWillMatchAfterRotation) {
+ // Scale up, not down
+ fitScale = 1.0f / fitScale;
+ }
+ }
+
+ transform.PreTranslate(
+ NSAppUnitsToFloatPixels(contentPageSize.width / 2, aAppUnitsPerPixel),
+ NSAppUnitsToFloatPixels(contentPageSize.height / 2, aAppUnitsPerPixel),
+ 0);
+ if (MOZ_LIKELY(fitScale != 1.0f)) {
+ transform.PreScale(fitScale, fitScale, 1.0f);
+ }
+ transform.RotateZ(rotation);
+ transform.PreTranslate(
+ NSAppUnitsToFloatPixels(-contentPageSize.width / 2, aAppUnitsPerPixel),
+ NSAppUnitsToFloatPixels(-contentPageSize.height / 2, aAppUnitsPerPixel),
+ 0);
+ }
+
+ return transform;
+}
+
+nsIFrame::ComputeTransformFunction nsPageFrame::GetTransformGetter() const {
+ return ComputePagesPerSheetAndPageSizeTransform;
+}
+
+nsPageContentFrame* nsPageFrame::PageContentFrame() const {
+ nsIFrame* const frame = mFrames.FirstChild();
+ MOZ_ASSERT(frame, "pageFrame must have one child");
+ MOZ_ASSERT(frame->IsPageContentFrame(),
+ "pageFrame must have pageContentFrame as the first child");
+ return static_cast<nsPageContentFrame*>(frame);
+}
+
+nsSize nsPageFrame::ComputePageSize() const {
+ // Compute the expected page-size.
+ const nsPageFrame* const frame =
+ StaticPrefs::layout_css_allow_mixed_page_sizes()
+ ? this
+ : static_cast<nsPageFrame*>(FirstContinuation());
+ const StylePageSize& pageSize = frame->PageContentFrame()->StylePage()->mSize;
+ nsSize size = PresContext()->GetPageSize();
+ if (pageSize.IsSize()) {
+ // Use the specified size,
+ // ignoring sizes that include a zero width or height.
+ // These are also ignored in ServoStyleSet::GetPageSizeForPageName()
+ // when getting the paper size.
+ nscoord cssPageWidth = pageSize.AsSize().width.ToAppUnits();
+ nscoord cssPageHeight = pageSize.AsSize().height.ToAppUnits();
+ if (cssPageWidth > 0 && cssPageHeight > 0) {
+ return nsSize{cssPageWidth, cssPageHeight};
+ }
+ // Invalid size; just return the default
+ return size;
+ }
+
+ if (pageSize.IsOrientation()) {
+ // Ensure the correct orientation is applied.
+ if (pageSize.AsOrientation() == StylePageSizeOrientation::Portrait) {
+ if (size.width > size.height) {
+ std::swap(size.width, size.height);
+ }
+ } else {
+ MOZ_ASSERT(pageSize.AsOrientation() ==
+ StylePageSizeOrientation::Landscape);
+ if (size.width < size.height) {
+ std::swap(size.width, size.height);
+ }
+ }
+ } else {
+ MOZ_ASSERT(pageSize.IsAuto(), "Impossible page-size value?");
+ }
+ return size;
+}
+
+float nsPageFrame::ComputeSinglePPSPageSizeScale(
+ const nsSize aContentPageSize) const {
+ MOZ_ASSERT(GetSharedPageData()->PagesPerSheetInfo()->mNumPages == 1,
+ "Only intended for the pps==1 case");
+ MOZ_ASSERT(aContentPageSize == ComputePageSize(),
+ "Incorrect content page size");
+
+ // Check for the simplest case first, an auto page-size which requires no
+ // scaling at all.
+ {
+ const nsPageFrame* const frame =
+ StaticPrefs::layout_css_allow_mixed_page_sizes()
+ ? this
+ : static_cast<nsPageFrame*>(FirstContinuation());
+ const StylePageSize& pageSize =
+ frame->PageContentFrame()->StylePage()->mSize;
+ if (pageSize.IsAuto()) {
+ return 1.0f;
+ }
+ }
+
+ const nsContainerFrame* const parent = GetParent();
+ MOZ_ASSERT(parent && parent->IsPrintedSheetFrame(),
+ "Parent of nsPageFrame should be PrintedSheetFrame");
+ const auto* sheet = static_cast<const PrintedSheetFrame*>(parent);
+
+ // Compute scaling due to a possible mismatch in the paper size we are
+ // printing to (from the pres context) and the specified page size when the
+ // content uses "@page {size: ...}" to specify a page size for the content.
+ float scale = 1.0f;
+
+ const nsSize sheetSize = sheet->GetSizeForChildren();
+ nscoord contentPageHeight = aContentPageSize.height;
+ // Scale down if the target is too wide.
+ if (aContentPageSize.width > sheetSize.width) {
+ scale *= float(sheetSize.width) / float(aContentPageSize.width);
+ contentPageHeight = NSToCoordRound(contentPageHeight * scale);
+ }
+ // Scale down if the target is too tall.
+ if (contentPageHeight > sheetSize.height) {
+ scale *= float(sheetSize.height) / float(contentPageHeight);
+ }
+ MOZ_ASSERT(
+ scale <= 1.0f,
+ "Page-size mismatches should only have caused us to scale down, not up.");
+ return scale;
+}
+
+double nsPageFrame::GetPageOrientationRotation(nsSharedPageData* aPD) const {
+ if (!StaticPrefs::layout_css_page_orientation_enabled()) {
+ return 0.0;
+ }
+
+ if (aPD->PagesPerSheetInfo()->mNumPages == 1 && !PresContext()->IsScreen() &&
+ aPD->mPrintSettings->GetOutputFormat() !=
+ nsIPrintSettings::kOutputFormatPDF) {
+ // In the single page-per-sheet case we rotate the page by essentially
+ // rotating the entire sheet. But we can't do that when the output device
+ // doesn't support mixed sheet orientations.
+ return 0.0;
+ }
+
+ const StylePageOrientation& orientation =
+ PageContentFrame()->StylePage()->mPageOrientation;
+
+ if (orientation == StylePageOrientation::RotateLeft) {
+ return -M_PI / 2.0;
+ }
+ if (orientation == StylePageOrientation::RotateRight) {
+ return M_PI / 2.0;
+ }
+ return 0.0;
+}
+
+void nsPageFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder,
+ const nsDisplayListSet& aLists) {
+ nsDisplayList content(aBuilder);
+ nsDisplayListSet set(&content, &content, &content, &content, &content,
+ &content);
+ {
+ DisplayListClipState::AutoSaveRestore clipState(aBuilder);
+ clipState.Clear();
+
+ nsPresContext* const pc = PresContext();
+ {
+ // We need to extend the building rect to include the specified page size
+ // (scaled by the print scaling factor), in case it is larger than the
+ // physical page size. In that case the nsPageFrame will be the size of
+ // the physical page, but the child nsPageContentFrame will be the larger
+ // specified page size. The more correct way to do this would be to fully
+ // reverse the result of ComputePagesPerSheetAndPageSizeTransform to
+ // handle this scaling, but this should have the same result and is
+ // easier.
+ const float scale = pc->GetPageScale();
+ const nsSize pageSize = ComputePageSize();
+ const nsRect scaledPageRect{0, 0, NSToCoordCeil(pageSize.width / scale),
+ NSToCoordCeil(pageSize.height / scale)};
+ nsDisplayListBuilder::AutoBuildingDisplayList buildingForPageContentFrame(
+ aBuilder, this, scaledPageRect, scaledPageRect);
+
+ nsContainerFrame::BuildDisplayList(aBuilder, set);
+ }
+
+ if (pc->IsRootPaginatedDocument()) {
+ content.AppendNewToTop<nsDisplayHeaderFooter>(aBuilder, this);
+
+ // For print-preview, show margin guides if requested in the settings.
+ if (pc->Type() == nsPresContext::eContext_PrintPreview &&
+ mPD->mPrintSettings->GetShowMarginGuides()) {
+ content.AppendNewToTop<nsDisplayGeneric>(
+ aBuilder, this, PaintMarginGuides, "MarginGuides",
+ DisplayItemType::TYPE_MARGIN_GUIDES);
+ }
+ }
+ }
+
+ // We'll be drawing the page with a (usually-trivial)
+ // N-pages-per-sheet transform applied, so our passed-in visible rect
+ // isn't meaningful while we're drawing our children, because the
+ // transform could scale down content whose coordinates are off-screen
+ // such that it ends up on-screen. So: we temporarily update the visible
+ // rect to be the child nsPageFrame's whole frame-rect (represented in
+ // this PrintedSheetFrame's coordinate space.
+ content.AppendNewToTop<nsDisplayTransform>(
+ aBuilder, this, &content, content.GetBuildingRect(),
+ nsDisplayTransform::WithTransformGetter);
+
+ set.MoveTo(aLists);
+}
+
+//------------------------------------------------------------------------------
+void nsPageFrame::DeterminePageNum() {
+ // If we have no previous continuation, we're page 1. Otherwise, we're
+ // just one more than our previous continuation's page number.
+ auto* prevContinuation = static_cast<nsPageFrame*>(GetPrevContinuation());
+ mPageNum = prevContinuation ? prevContinuation->GetPageNum() + 1 : 1;
+}
+
+void nsPageFrame::PaintHeaderFooter(gfxContext& aRenderingContext, nsPoint aPt,
+ bool aDisableSubpixelAA) {
+ nsPresContext* pc = PresContext();
+
+ nsRect rect(aPt, ComputePageSize());
+ aRenderingContext.SetColor(sRGBColor::OpaqueBlack());
+
+ DrawTargetAutoDisableSubpixelAntialiasing disable(
+ aRenderingContext.GetDrawTarget(), aDisableSubpixelAA);
+
+ // Get the FontMetrics to determine width.height of strings
+ nsFontMetrics::Params params;
+ params.userFontSet = pc->GetUserFontSet();
+ params.textPerf = pc->GetTextPerfMetrics();
+ params.featureValueLookup = pc->GetFontFeatureValuesLookup();
+ RefPtr<nsFontMetrics> fontMet = pc->GetMetricsFor(mPD->mHeadFootFont, params);
+
+ nscoord ascent = fontMet->MaxAscent();
+ nscoord visibleHeight = fontMet->MaxHeight();
+
+ // print document headers and footers
+ nsString headerLeft, headerCenter, headerRight;
+ mPD->mPrintSettings->GetHeaderStrLeft(headerLeft);
+ mPD->mPrintSettings->GetHeaderStrCenter(headerCenter);
+ mPD->mPrintSettings->GetHeaderStrRight(headerRight);
+ DrawHeaderFooter(aRenderingContext, *fontMet, eHeader, headerLeft,
+ headerCenter, headerRight, rect, ascent, visibleHeight);
+
+ nsString footerLeft, footerCenter, footerRight;
+ mPD->mPrintSettings->GetFooterStrLeft(footerLeft);
+ mPD->mPrintSettings->GetFooterStrCenter(footerCenter);
+ mPD->mPrintSettings->GetFooterStrRight(footerRight);
+ DrawHeaderFooter(aRenderingContext, *fontMet, eFooter, footerLeft,
+ footerCenter, footerRight, rect, ascent, visibleHeight);
+}
+
+void nsPageFrame::SetSharedPageData(nsSharedPageData* aPD) {
+ mPD = aPD;
+ // Set the shared data into the page frame before reflow
+ PageContentFrame()->SetSharedPageData(mPD);
+}
+
+nsIFrame* NS_NewPageBreakFrame(PresShell* aPresShell, ComputedStyle* aStyle) {
+ MOZ_ASSERT(aPresShell, "null PresShell");
+ // check that we are only creating page break frames when printing
+ NS_ASSERTION(aPresShell->GetPresContext()->IsPaginated(),
+ "created a page break frame while not printing");
+
+ return new (aPresShell)
+ nsPageBreakFrame(aStyle, aPresShell->GetPresContext());
+}
+
+NS_IMPL_FRAMEARENA_HELPERS(nsPageBreakFrame)
+
+nsPageBreakFrame::nsPageBreakFrame(ComputedStyle* aStyle,
+ nsPresContext* aPresContext)
+ : nsLeafFrame(aStyle, aPresContext, kClassID) {}
+
+nsPageBreakFrame::~nsPageBreakFrame() = default;
+
+nscoord nsPageBreakFrame::GetIntrinsicISize() {
+ return nsPresContext::CSSPixelsToAppUnits(1);
+}
+
+nscoord nsPageBreakFrame::GetIntrinsicBSize() { return 0; }
+
+void nsPageBreakFrame::Reflow(nsPresContext* aPresContext,
+ ReflowOutput& aReflowOutput,
+ const ReflowInput& aReflowInput,
+ nsReflowStatus& aStatus) {
+ DO_GLOBAL_REFLOW_COUNT("nsPageBreakFrame");
+ DISPLAY_REFLOW(aPresContext, this, aReflowInput, aReflowOutput, aStatus);
+ MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!");
+
+ // Override reflow, since we don't want to deal with what our
+ // computed values are.
+ const WritingMode wm = aReflowInput.GetWritingMode();
+ nscoord bSize = aReflowInput.AvailableBSize();
+ if (aReflowInput.AvailableBSize() == NS_UNCONSTRAINEDSIZE) {
+ bSize = nscoord(0);
+ } else if (GetContent()->IsHTMLElement(nsGkAtoms::legend)) {
+ // If this is a page break frame for a _rendered legend_ then it should be
+ // ignored since these frames are inserted inside the fieldset's inner
+ // frame and thus "misplaced". nsFieldSetFrame::Reflow deals with these
+ // forced breaks explicitly instead.
+ const nsContainerFrame* parent = GetParent();
+ if (parent &&
+ parent->Style()->GetPseudoType() == PseudoStyleType::fieldsetContent) {
+ while ((parent = parent->GetParent())) {
+ if (const nsFieldSetFrame* const fieldset = do_QueryFrame(parent)) {
+ const auto* const legend = fieldset->GetLegend();
+ if (legend && legend->GetContent() == GetContent()) {
+ bSize = nscoord(0);
+ }
+ break;
+ }
+ }
+ }
+ }
+ LogicalSize finalSize(wm, GetIntrinsicISize(), bSize);
+ // round the height down to the nearest pixel
+ // XXX(mats) why???
+ finalSize.BSize(wm) -=
+ finalSize.BSize(wm) % nsPresContext::CSSPixelsToAppUnits(1);
+ aReflowOutput.SetSize(wm, finalSize);
+}
+
+#ifdef DEBUG_FRAME_DUMP
+nsresult nsPageBreakFrame::GetFrameName(nsAString& aResult) const {
+ return MakeFrameName(u"PageBreak"_ns, aResult);
+}
+#endif