layout/generic/PrintedSheetFrame.cpp
author Dragana Damjanovic <dd.mozilla@gmail.com>
Thu, 06 May 2021 18:10:45 +0000
changeset 578757 c9980e971a31b2bd47783dc4a9a26fca4a4c57d6
parent 578514 029a5e8fa8f3179b63a7bf291ea689c4dd642492
permissions -rw-r--r--
Bug 1708678 - Adapt code to neqo 0.4.24 r=necko-reviewers,valentin Differential Revision: https://phabricator.services.mozilla.com/D114011

/* -*- 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)

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);
  }

  for (auto* frame : mFrames) {
    if (!frame->HasAnyStateBits(NS_PAGE_SKIPPED_BY_CUSTOM_RANGE)) {
      BuildDisplayListForChild(aBuilder, frame, aLists);
    }
  }
}

// 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;
}

#ifdef DEBUG_FRAME_DUMP
nsresult PrintedSheetFrame::GetFrameName(nsAString& aResult) const {
  return MakeFrameName(u"PrintedSheet"_ns, aResult);
}
#endif

}  // namespace mozilla