summaryrefslogtreecommitdiffstats
path: root/layout/generic/PrintedSheetFrame.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'layout/generic/PrintedSheetFrame.cpp')
-rw-r--r--layout/generic/PrintedSheetFrame.cpp418
1 files changed, 418 insertions, 0 deletions
diff --git a/layout/generic/PrintedSheetFrame.cpp b/layout/generic/PrintedSheetFrame.cpp
new file mode 100644
index 0000000000..06fdbad3f5
--- /dev/null
+++ b/layout/generic/PrintedSheetFrame.cpp
@@ -0,0 +1,418 @@
+/* -*- 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 https://mozilla.org/MPL/2.0/. */
+
+/* Rendering object for a printed or print-previewed sheet of paper */
+
+#include "mozilla/PrintedSheetFrame.h"
+
+#include <tuple>
+
+#include "mozilla/StaticPrefs_print.h"
+#include "nsCSSFrameConstructor.h"
+#include "nsPageFrame.h"
+#include "nsPageSequenceFrame.h"
+
+using namespace mozilla;
+
+PrintedSheetFrame* NS_NewPrintedSheetFrame(PresShell* aPresShell,
+ ComputedStyle* aStyle) {
+ return new (aPresShell)
+ PrintedSheetFrame(aStyle, aPresShell->GetPresContext());
+}
+
+namespace mozilla {
+
+NS_QUERYFRAME_HEAD(PrintedSheetFrame)
+ NS_QUERYFRAME_ENTRY(PrintedSheetFrame)
+NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame)
+
+NS_IMPL_FRAMEARENA_HELPERS(PrintedSheetFrame)
+
+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:
+gfx::Matrix4x4 ComputePagesPerSheetTransform(nsIFrame* aFrame,
+ float aAppUnitsPerPixel) {
+ MOZ_ASSERT(aFrame->IsPageFrame());
+ auto* pageFrame = static_cast<nsPageFrame*>(aFrame);
+
+ // Variables that we use in our transform (initialized with reasonable
+ // defaults that work for the regular one-page-per-sheet scenario):
+ float scale = 1.0f;
+ nsPoint gridOrigin;
+ uint32_t rowIdx = 0;
+ uint32_t colIdx = 0;
+
+ nsSharedPageData* pd = pageFrame->GetSharedPageData();
+ if (pd) {
+ const auto* ppsInfo = pd->PagesPerSheetInfo();
+ if (ppsInfo->mNumPages > 1) {
+ scale = pd->mPagesPerSheetScale;
+ gridOrigin = pd->mPagesPerSheetGridOrigin;
+ std::tie(rowIdx, colIdx) = GetRowAndColFromIdx(pageFrame->IndexOnSheet(),
+ pd->mPagesPerSheetNumCols);
+ }
+ }
+
+ // Scale down the page based on the above-computed scale:
+ auto transform = gfx::Matrix4x4::Scaling(scale, scale, 1);
+
+ // Draw the page at an offset, to get it in its pages-per-sheet "cell":
+ nsSize pageSize = pageFrame->PresContext()->GetPageSize();
+ transform.PreTranslate(
+ NSAppUnitsToFloatPixels(colIdx * pageSize.width, aAppUnitsPerPixel),
+ NSAppUnitsToFloatPixels(rowIdx * pageSize.height, aAppUnitsPerPixel), 0);
+
+ // Also add the grid origin as an offset (so that we're not drawing into the
+ // sheet's unwritable area). Note that this is a PostTranslate operation
+ // (vs. PreTranslate above), since gridOrigin is an offset on the sheet
+ // itself, whereas the offset above was in the scaled coordinate space of the
+ // pages.
+ return transform.PostTranslate(
+ NSAppUnitsToFloatPixels(gridOrigin.x, aAppUnitsPerPixel),
+ NSAppUnitsToFloatPixels(gridOrigin.y, aAppUnitsPerPixel), 0);
+}
+
+void PrintedSheetFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder,
+ const nsDisplayListSet& aLists) {
+ if (PresContext()->IsScreen()) {
+ // Draw the background/shadow/etc. of a blank sheet of paper, for
+ // print-preview.
+ DisplayBorderBackgroundOutline(aBuilder, aLists);
+ }
+
+ // Let each of our children (pages) draw itself, with a supplemental
+ // transform to shrink it & place it in its pages-per-sheet cell:
+ for (auto* frame : mFrames) {
+ if (!frame->HasAnyStateBits(NS_PAGE_SKIPPED_BY_CUSTOM_RANGE)) {
+ // We'll be drawing our nsPageFrame children 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.
+ nsDisplayList content;
+ {
+ nsRect visibleRect = frame->GetRect();
+ nsDisplayListBuilder::AutoBuildingDisplayList buildingForChild(
+ aBuilder, this, visibleRect, visibleRect);
+
+ frame->BuildDisplayListForStackingContext(aBuilder, &content);
+ }
+ content.AppendNewToTop<nsDisplayTransform>(aBuilder, frame, &content,
+ content.GetBuildingRect(),
+ ComputePagesPerSheetTransform);
+
+ aLists.Content()->AppendToTop(&content);
+ }
+ }
+}
+
+// If the given page is included in the user's page range, this function
+// returns false. Otherwise, it tags the page with the
+// NS_PAGE_SKIPPED_BY_CUSTOM_RANGE state bit and returns true.
+static bool TagIfSkippedByCustomRange(nsPageFrame* aPageFrame, int32_t aPageNum,
+ nsSharedPageData* aPD) {
+ if (!nsIPrintSettings::IsPageSkipped(aPageNum, aPD->mPageRanges)) {
+ MOZ_ASSERT(!aPageFrame->HasAnyStateBits(NS_PAGE_SKIPPED_BY_CUSTOM_RANGE),
+ "page frames NS_PAGE_SKIPPED_BY_CUSTOM_RANGE state should "
+ "only be set if we actually want to skip the page");
+ return false;
+ }
+
+ aPageFrame->AddStateBits(NS_PAGE_SKIPPED_BY_CUSTOM_RANGE);
+ return true;
+}
+
+void PrintedSheetFrame::Reflow(nsPresContext* aPresContext,
+ ReflowOutput& aReflowOutput,
+ const ReflowInput& aReflowInput,
+ nsReflowStatus& aStatus) {
+ MarkInReflow();
+ DO_GLOBAL_REFLOW_COUNT("PrintedSheetFrame");
+ DISPLAY_REFLOW(aPresContext, this, aReflowInput, aReflowOutput, aStatus);
+ MOZ_ASSERT(aStatus.IsEmpty(), "Caller should pass a fresh reflow status!");
+
+ // If we have a prev-in-flow, take its overflowing content:
+ MoveOverflowToChildList();
+
+ const WritingMode wm = aReflowInput.GetWritingMode();
+
+ // This is the app-unit size of each page (in physical & logical units):
+ const nsSize physPageSize = aPresContext->GetPageSize();
+ const LogicalSize pageSize(wm, physPageSize);
+
+ // Count the number of pages that are displayed on this sheet (i.e. how many
+ // child frames we end up laying out, excluding any pages that are skipped
+ // due to not being in the user's page-range selection).
+ uint32_t numPagesOnThisSheet = 0;
+
+ // Target for numPagesOnThisSheet.
+ const uint32_t desiredPagesPerSheet = mPD->PagesPerSheetInfo()->mNumPages;
+
+ // If we're the first continuation and we're doing >1 pages per sheet,
+ // precompute some metrics that we'll use when painting the pages:
+ if (desiredPagesPerSheet > 1 && !GetPrevContinuation()) {
+ ComputePagesPerSheetOriginAndScale();
+ }
+
+ // NOTE: I'm intentionally *not* using a range-based 'for' loop here, since
+ // we potentially mutate the frame list (appending to the end) during the
+ // list, which is not generally safe with range-based 'for' loops.
+ for (auto* childFrame = mFrames.FirstChild(); childFrame;
+ childFrame = childFrame->GetNextSibling()) {
+ MOZ_ASSERT(childFrame->IsPageFrame(),
+ "we're only expecting page frames as children");
+ auto* pageFrame = static_cast<nsPageFrame*>(childFrame);
+
+ // Be sure our child has a pointer to the nsSharedPageData and knows its
+ // page number:
+ pageFrame->SetSharedPageData(mPD);
+ pageFrame->DeterminePageNum();
+
+ if (!TagIfSkippedByCustomRange(pageFrame, pageFrame->GetPageNum(), mPD)) {
+ // The page is going to be displayed on this sheet. Tell it its index
+ // among the displayed pages, so we can use that to compute its "cell"
+ // when painting.
+ pageFrame->SetIndexOnSheet(numPagesOnThisSheet);
+ numPagesOnThisSheet++;
+ }
+
+ ReflowInput pageReflowInput(aPresContext, aReflowInput, pageFrame,
+ pageSize);
+
+ // For layout purposes, we position *all* our nsPageFrame children at our
+ // origin. Then, if we have multiple pages-per-sheet, we'll shrink & shift
+ // each one into the right position as a paint-time effect, in
+ // BuildDisplayList.
+ LogicalPoint pagePos(wm);
+
+ // Outparams for reflow:
+ ReflowOutput pageReflowOutput(pageReflowInput);
+ nsReflowStatus status;
+
+ ReflowChild(pageFrame, aPresContext, pageReflowOutput, pageReflowInput, wm,
+ pagePos, physPageSize, ReflowChildFlags::Default, status);
+
+ FinishReflowChild(pageFrame, aPresContext, pageReflowOutput,
+ &pageReflowInput, wm, pagePos, physPageSize,
+ ReflowChildFlags::Default);
+
+ // Since we don't support incremental reflow in printed documents (see the
+ // early-return in nsPageSequenceFrame::Reflow), we can assume that this
+ // was the first time that pageFrame has been reflowed, and so there's no
+ // way that it could already have a next-in-flow. If it *did* have a
+ // next-in-flow, we would need to handle it in the 'status' logic below.
+ NS_ASSERTION(!pageFrame->GetNextInFlow(), "bad child flow list");
+
+ // Did this page complete the document, or do we need to generate
+ // another page frame?
+ if (status.IsFullyComplete()) {
+ // The page we just reflowed is the final page! Record its page number
+ // as the number of pages:
+ mPD->mRawNumPages = pageFrame->GetPageNum();
+ } else {
+ // Create a continuation for our page frame. We add the continuation to
+ // our child list, and then potentially push it to our overflow list, if
+ // it really belongs on the next sheet.
+ nsIFrame* continuingPage =
+ PresShell()->FrameConstructor()->CreateContinuingFrame(pageFrame,
+ this);
+ mFrames.InsertFrame(nullptr, pageFrame, continuingPage);
+ const bool isContinuingPageSkipped =
+ TagIfSkippedByCustomRange(static_cast<nsPageFrame*>(continuingPage),
+ pageFrame->GetPageNum() + 1, mPD);
+
+ // If we've already reached the target number of pages for this sheet,
+ // and this continuation page that we just created is meant to be
+ // displayed (i.e. it's in the chosen page range), then we need to push it
+ // to our overflow list so that it'll go onto a subsequent sheet.
+ // Otherwise we leave it on this sheet. This ensures we *only* generate
+ // another sheet IFF there's a displayable page that will end up on it.
+ if (numPagesOnThisSheet >= desiredPagesPerSheet &&
+ !isContinuingPageSkipped) {
+ PushChildrenToOverflow(continuingPage, pageFrame);
+ aStatus.SetIncomplete();
+ }
+ }
+ }
+
+ // This should hold for the first sheet, because our UI should prevent the
+ // user from creating a 0-length page range; and it should hold for
+ // subsequent sheets because we should only create an additional sheet when
+ // we discover a displayable (i.e. non-skipped) page that we need to push
+ // to that new sheet.
+
+ // XXXdholbert In certain edge cases (e.g. after a page-orientation-flip that
+ // reduces the page count), it's possible for us to be given a page range
+ // that is *entirely out-of-bounds* (with "from" & "to" both being larger
+ // than our actual page-number count). This scenario produces a single
+ // PrintedSheetFrame with zero displayable pages on it, which is a weird
+ // state to be in. This is hopefully a scenario that the frontend code can
+ // detect and recover from (e.g. by clamping the range to our reported
+ // `rawNumPages`), but it can't do that until *after* we've completed this
+ // problematic reflow and can reported an up-to-date `rawNumPages` to the
+ // frontend. So: to give the frontend a chance to intervene and apply some
+ // correction/clamping to its print-range parameters, we soften this
+ // assertion *specifically for the first printed sheet*.
+ if (!GetPrevContinuation()) {
+ NS_WARNING_ASSERTION(numPagesOnThisSheet > 0,
+ "Shouldn't create a sheet with no displayable pages "
+ "on it");
+ } else {
+ MOZ_ASSERT(numPagesOnThisSheet > 0,
+ "Shouldn't create a sheet with no displayable pages on it");
+ }
+
+ MOZ_ASSERT(numPagesOnThisSheet <= desiredPagesPerSheet,
+ "Shouldn't have more than desired number of displayable pages "
+ "on this sheet");
+ mNumPages = numPagesOnThisSheet;
+
+ // Populate our ReflowOutput outparam -- just use up all the
+ // available space, for both our desired size & overflow areas.
+ aReflowOutput.ISize(wm) = aReflowInput.AvailableISize();
+ if (aReflowInput.AvailableBSize() != NS_UNCONSTRAINEDSIZE) {
+ aReflowOutput.BSize(wm) = aReflowInput.AvailableBSize();
+ }
+ aReflowOutput.SetOverflowAreasToDesiredBounds();
+
+ FinishAndStoreOverflow(&aReflowOutput);
+ NS_FRAME_SET_TRUNCATION(aStatus, aReflowInput, aReflowOutput);
+}
+
+void PrintedSheetFrame::ComputePagesPerSheetOriginAndScale() {
+ MOZ_ASSERT(mPD->PagesPerSheetInfo()->mNumPages > 1,
+ "Unnecessary to call this in a regular 1-page-per-sheet scenario; "
+ "the computed values won't ever be used in that case");
+ MOZ_ASSERT(!GetPrevContinuation(),
+ "Only needs to be called once, so 1st continuation handles it");
+
+ // The "full-scale" size of a page (if it weren't shrunk down into a grid):
+ const nsSize pageSize = PresContext()->GetPageSize();
+
+ // Compute the space available for the pages-per-sheet "page grid" (just
+ // subtract the sheet's unwriteable margin area):
+ nsSize availSpaceOnSheet = pageSize;
+ nsMargin uwm = nsPresContext::CSSTwipsToAppUnits(
+ mPD->mPrintSettings->GetUnwriteableMarginInTwips());
+
+ if (mPD->mPrintSettings->HasOrthogonalSheetsAndPages()) {
+ // The pages will be rotated to be orthogonal to the physical sheet. To
+ // account for that, we rotate the components of availSpaceOnSheet and uwm,
+ // so that we can reason about them here from the perspective of a
+ // "pageSize"-oriented *page*.
+ std::swap(availSpaceOnSheet.width, availSpaceOnSheet.height);
+
+ // Note that the pages are rotated 90 degrees clockwise when placed onto a
+ // sheet (so that, e.g. in a scenario with two side-by-side portait pages
+ // that are rotated & placed onto a sheet, the "left" edge of the first
+ // page is at the "top" of the sheet and hence comes out of the printer
+ // first, etc). So: given `nsMargin uwm` whose sides correspond to the
+ // physical sheet's sides, we have to rotate 90 degrees *counter-clockwise*
+ // in order to "cancel out" the page rotation and to represent it in the
+ // page's perspective. From a page's perspective, its own "top" side
+ // corresponds to the physical sheet's right side, which is why we're
+ // passing "uwm.right" as the "top" component here; and so on.
+ nsMargin rotated(uwm.right, uwm.bottom, uwm.left, uwm.top);
+ uwm = rotated;
+ }
+
+ availSpaceOnSheet.width -= uwm.LeftRight();
+ availSpaceOnSheet.height -= uwm.TopBottom();
+ nsPoint pageGridOrigin(uwm.left, uwm.top);
+
+ // If there are a different number of rows vs. cols, we'll aim to put
+ // the larger number of items in the longer axis.
+ const auto* ppsInfo = mPD->PagesPerSheetInfo();
+ uint32_t smallerNumTracks = ppsInfo->mNumPages / ppsInfo->mLargerNumTracks;
+ bool pageSizeIsPortraitLike = pageSize.width > pageSize.height;
+ auto numCols =
+ pageSizeIsPortraitLike ? smallerNumTracks : ppsInfo->mLargerNumTracks;
+ auto numRows =
+ pageSizeIsPortraitLike ? ppsInfo->mLargerNumTracks : smallerNumTracks;
+
+ // Compute the full size of the "page grid" that we'll be scaling down &
+ // placing onto a given sheet:
+ nsSize pageGridFullSize(numCols * pageSize.width, numRows * pageSize.height);
+
+ if (MOZ_UNLIKELY(availSpaceOnSheet.IsEmpty() || pageGridFullSize.IsEmpty())) {
+ // Either we have a 0-sized available area, or we have a 0-sized page-grid
+ // to draw into the available area. This sort of thing should be rare, but
+ // it can happen if there are bizarre page sizes, and/or if there's an
+ // unexpectedly large unwritable margin area. Regardless: if we get here,
+ // we shouldn't be drawing anything onto the sheet; so let's just use a
+ // scale factor of 0, and bail early to avoid division by 0 in hScale &
+ // vScale computations below.
+ NS_WARNING("Zero area for pages-per-sheet grid, or zero-sized grid");
+ mPD->mPagesPerSheetGridOrigin = pageGridOrigin;
+ mPD->mPagesPerSheetNumCols = 1;
+ mPD->mPagesPerSheetScale = 0.0f;
+ return;
+ }
+
+ // Compute the scale factors required in each axis:
+ float hScale =
+ availSpaceOnSheet.width / static_cast<float>(pageGridFullSize.width);
+ float vScale =
+ availSpaceOnSheet.height / static_cast<float>(pageGridFullSize.height);
+
+ // Choose the more restrictive scale factor (so that we don't overflow the
+ // sheet's printable area in either axis). And center the page-grid in the
+ // other axis (since it probably ends up with extra space).
+ float scale = std::min(hScale, vScale);
+ if (hScale < vScale) {
+ // hScale is the more restrictive scale-factor, so we'll be using that.
+ // Nudge the grid in the vertical axis to center it:
+ nscoord extraSpace = availSpaceOnSheet.height -
+ NSToCoordFloor(scale * pageGridFullSize.height);
+ if (MOZ_LIKELY(extraSpace > 0)) {
+ pageGridOrigin.y += extraSpace / 2;
+ }
+ } else if (vScale < hScale) {
+ // vScale is the more restrictive scale-factor, so we'll be using that.
+ // Nudge the grid in the vertical axis to center it:
+ nscoord extraSpace = availSpaceOnSheet.width -
+ NSToCoordFloor(scale * pageGridFullSize.width);
+ if (MOZ_LIKELY(extraSpace > 0)) {
+ pageGridOrigin.x += extraSpace / 2;
+ }
+ }
+ // else, we fit exactly in both axes, with the same scale factor, so there's
+ // no extra space in either axis, i.e. no need to center.
+
+ // Update the nsSharedPageData member data:
+ mPD->mPagesPerSheetGridOrigin = pageGridOrigin;
+ mPD->mPagesPerSheetNumCols = numCols;
+ mPD->mPagesPerSheetScale = scale;
+}
+
+void PrintedSheetFrame::AppendDirectlyOwnedAnonBoxes(
+ nsTArray<OwnedAnonBox>& aResult) {
+ MOZ_ASSERT(mFrames.FirstChild() && mFrames.FirstChild()->IsPageFrame(),
+ "PrintedSheetFrame must have a nsPageFrame child");
+ // Only append the first child; all our children are expected to be
+ // continuations of each other, and our anon box handling always walks
+ // continuations.
+ aResult.AppendElement(mFrames.FirstChild());
+}
+
+#ifdef DEBUG_FRAME_DUMP
+nsresult PrintedSheetFrame::GetFrameName(nsAString& aResult) const {
+ return MakeFrameName(u"PrintedSheet"_ns, aResult);
+}
+#endif
+
+} // namespace mozilla