1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
7 /* Rendering object for a printed or print-previewed sheet of paper */
9 #include "mozilla/PrintedSheetFrame.h"
13 #include "mozilla/StaticPrefs_layout.h"
14 #include "mozilla/StaticPrefs_print.h"
15 #include "nsCSSFrameConstructor.h"
16 #include "nsPageContentFrame.h"
17 #include "nsPageFrame.h"
18 #include "nsPageSequenceFrame.h"
20 using namespace mozilla
;
22 PrintedSheetFrame
* NS_NewPrintedSheetFrame(PresShell
* aPresShell
,
23 ComputedStyle
* aStyle
) {
24 return new (aPresShell
)
25 PrintedSheetFrame(aStyle
, aPresShell
->GetPresContext());
30 NS_QUERYFRAME_HEAD(PrintedSheetFrame
)
31 NS_QUERYFRAME_ENTRY(PrintedSheetFrame
)
32 NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame
)
34 NS_IMPL_FRAMEARENA_HELPERS(PrintedSheetFrame
)
36 void PrintedSheetFrame::BuildDisplayList(nsDisplayListBuilder
* aBuilder
,
37 const nsDisplayListSet
& aLists
) {
38 if (PresContext()->IsScreen()) {
39 // Draw the background/shadow/etc. of a blank sheet of paper, for
41 DisplayBorderBackgroundOutline(aBuilder
, aLists
);
44 for (auto* frame
: mFrames
) {
45 if (!frame
->HasAnyStateBits(NS_PAGE_SKIPPED_BY_CUSTOM_RANGE
)) {
46 BuildDisplayListForChild(aBuilder
, frame
, aLists
);
51 // If the given page is included in the user's page range, this function
52 // returns false. Otherwise, it tags the page with the
53 // NS_PAGE_SKIPPED_BY_CUSTOM_RANGE state bit and returns true.
54 static bool TagIfSkippedByCustomRange(nsPageFrame
* aPageFrame
, int32_t aPageNum
,
55 nsSharedPageData
* aPD
) {
56 if (!nsIPrintSettings::IsPageSkipped(aPageNum
, aPD
->mPageRanges
)) {
57 MOZ_ASSERT(!aPageFrame
->HasAnyStateBits(NS_PAGE_SKIPPED_BY_CUSTOM_RANGE
),
58 "page frames NS_PAGE_SKIPPED_BY_CUSTOM_RANGE state should "
59 "only be set if we actually want to skip the page");
63 aPageFrame
->AddStateBits(NS_PAGE_SKIPPED_BY_CUSTOM_RANGE
);
67 void PrintedSheetFrame::ClaimPageFrameFromPrevInFlow() {
68 MoveOverflowToChildList();
69 if (!GetPrevContinuation()) {
70 // The first page content frame of each document will not yet have its page
71 // style set yet. This is because normally page style is set either from
72 // the previous page content frame, or using the new page name when named
73 // pages cause a page break in block reflow. Ensure that, for the first
74 // page, it is set here so that all nsPageContentFrames have their page
75 // style set before reflow.
76 auto* firstChild
= PrincipalChildList().FirstChild();
77 MOZ_ASSERT(firstChild
&& firstChild
->IsPageFrame(),
78 "PrintedSheetFrame only has nsPageFrame children");
79 auto* pageFrame
= static_cast<nsPageFrame
*>(firstChild
);
80 pageFrame
->PageContentFrame()->EnsurePageName();
84 void PrintedSheetFrame::Reflow(nsPresContext
* aPresContext
,
85 ReflowOutput
& aReflowOutput
,
86 const ReflowInput
& aReflowInput
,
87 nsReflowStatus
& aStatus
) {
89 DO_GLOBAL_REFLOW_COUNT("PrintedSheetFrame");
90 DISPLAY_REFLOW(aPresContext
, this, aReflowInput
, aReflowOutput
, aStatus
);
91 MOZ_ASSERT(aStatus
.IsEmpty(), "Caller should pass a fresh reflow status!");
93 // If we have a prev-in-flow, take its overflowing content:
94 MoveOverflowToChildList();
96 const WritingMode wm
= aReflowInput
.GetWritingMode();
98 // See the comments for GetSizeForChildren.
99 // Note that nsPageFrame::ComputeSinglePPSPageSizeScale depends on this value
100 // and is currently called while reflowing a single nsPageFrame child (i.e.
101 // before we've finished reflowing ourself). Ideally our children wouldn't be
102 // accessing our dimensions until after we've finished reflowing ourself -
105 nsSize(aReflowInput
.AvailableISize(), aReflowInput
.AvailableBSize());
106 if (mPD
->PagesPerSheetInfo()->mNumPages
== 1) {
107 auto* firstChild
= PrincipalChildList().FirstChild();
108 MOZ_ASSERT(firstChild
&& firstChild
->IsPageFrame(),
109 "PrintedSheetFrame only has nsPageFrame children");
110 if (static_cast<nsPageFrame
*>(firstChild
)
111 ->GetPageOrientationRotation(mPD
) != 0.0) {
112 std::swap(mSizeForChildren
.width
, mSizeForChildren
.height
);
116 // Count the number of pages that are displayed on this sheet (i.e. how many
117 // child frames we end up laying out, excluding any pages that are skipped
118 // due to not being in the user's page-range selection).
119 uint32_t numPagesOnThisSheet
= 0;
121 // Target for numPagesOnThisSheet.
122 const uint32_t desiredPagesPerSheet
= mPD
->PagesPerSheetInfo()->mNumPages
;
124 if (desiredPagesPerSheet
> 1) {
125 ComputePagesPerSheetGridMetrics(mSizeForChildren
);
128 // NOTE: I'm intentionally *not* using a range-based 'for' loop here, since
129 // we potentially mutate the frame list (appending to the end) during the
130 // list, which is not generally safe with range-based 'for' loops.
131 for (auto* childFrame
= mFrames
.FirstChild(); childFrame
;
132 childFrame
= childFrame
->GetNextSibling()) {
133 MOZ_ASSERT(childFrame
->IsPageFrame(),
134 "we're only expecting page frames as children");
135 auto* pageFrame
= static_cast<nsPageFrame
*>(childFrame
);
137 // Be sure our child has a pointer to the nsSharedPageData and knows its
139 pageFrame
->SetSharedPageData(mPD
);
140 pageFrame
->DeterminePageNum();
142 if (!TagIfSkippedByCustomRange(pageFrame
, pageFrame
->GetPageNum(), mPD
)) {
143 // The page is going to be displayed on this sheet. Tell it its index
144 // among the displayed pages, so we can use that to compute its "cell"
146 pageFrame
->SetIndexOnSheet(numPagesOnThisSheet
);
147 numPagesOnThisSheet
++;
150 // This is the app-unit size of the page (in physical & logical units).
151 // Note: The page sizes come from CSS or else from the user selected size;
152 // pages are never reflowed to fit their sheet - if/when necessary they are
153 // scaled to fit their sheet. Hence why we get the page's own dimensions to
154 // use as its "available space"/"container size" here.
155 const nsSize physPageSize
= pageFrame
->ComputePageSize();
156 const LogicalSize
pageSize(wm
, physPageSize
);
158 ReflowInput
pageReflowInput(aPresContext
, aReflowInput
, pageFrame
,
161 // For layout purposes, we position *all* our nsPageFrame children at our
162 // origin. Then, if we have multiple pages-per-sheet, we'll shrink & shift
163 // each one into the right position as a paint-time effect, in
165 LogicalPoint
pagePos(wm
);
167 // Outparams for reflow:
168 ReflowOutput
pageReflowOutput(pageReflowInput
);
169 nsReflowStatus status
;
171 ReflowChild(pageFrame
, aPresContext
, pageReflowOutput
, pageReflowInput
, wm
,
172 pagePos
, physPageSize
, ReflowChildFlags::Default
, status
);
174 FinishReflowChild(pageFrame
, aPresContext
, pageReflowOutput
,
175 &pageReflowInput
, wm
, pagePos
, physPageSize
,
176 ReflowChildFlags::Default
);
178 // Since we don't support incremental reflow in printed documents (see the
179 // early-return in nsPageSequenceFrame::Reflow), we can assume that this
180 // was the first time that pageFrame has been reflowed, and so there's no
181 // way that it could already have a next-in-flow. If it *did* have a
182 // next-in-flow, we would need to handle it in the 'status' logic below.
183 NS_ASSERTION(!pageFrame
->GetNextInFlow(), "bad child flow list");
185 // Did this page complete the document, or do we need to generate
186 // another page frame?
187 if (status
.IsFullyComplete()) {
188 // The page we just reflowed is the final page! Record its page number
189 // as the number of pages:
190 mPD
->mRawNumPages
= pageFrame
->GetPageNum();
192 // Create a continuation for our page frame. We add the continuation to
193 // our child list, and then potentially push it to our overflow list, if
194 // it really belongs on the next sheet.
195 nsIFrame
* continuingPage
=
196 PresShell()->FrameConstructor()->CreateContinuingFrame(pageFrame
,
198 mFrames
.InsertFrame(nullptr, pageFrame
, continuingPage
);
199 const bool isContinuingPageSkipped
=
200 TagIfSkippedByCustomRange(static_cast<nsPageFrame
*>(continuingPage
),
201 pageFrame
->GetPageNum() + 1, mPD
);
203 // If we've already reached the target number of pages for this sheet,
204 // and this continuation page that we just created is meant to be
205 // displayed (i.e. it's in the chosen page range), then we need to push it
206 // to our overflow list so that it'll go onto a subsequent sheet.
207 // Otherwise we leave it on this sheet. This ensures we *only* generate
208 // another sheet IFF there's a displayable page that will end up on it.
209 if (numPagesOnThisSheet
>= desiredPagesPerSheet
&&
210 !isContinuingPageSkipped
) {
211 PushChildrenToOverflow(continuingPage
, pageFrame
);
212 aStatus
.SetIncomplete();
217 // This should hold for the first sheet, because our UI should prevent the
218 // user from creating a 0-length page range; and it should hold for
219 // subsequent sheets because we should only create an additional sheet when
220 // we discover a displayable (i.e. non-skipped) page that we need to push
221 // to that new sheet.
223 // XXXdholbert In certain edge cases (e.g. after a page-orientation-flip that
224 // reduces the page count), it's possible for us to be given a page range
225 // that is *entirely out-of-bounds* (with "from" & "to" both being larger
226 // than our actual page-number count). This scenario produces a single
227 // PrintedSheetFrame with zero displayable pages on it, which is a weird
228 // state to be in. This is hopefully a scenario that the frontend code can
229 // detect and recover from (e.g. by clamping the range to our reported
230 // `rawNumPages`), but it can't do that until *after* we've completed this
231 // problematic reflow and can reported an up-to-date `rawNumPages` to the
232 // frontend. So: to give the frontend a chance to intervene and apply some
233 // correction/clamping to its print-range parameters, we soften this
234 // assertion *specifically for the first printed sheet*.
235 if (!GetPrevContinuation()) {
236 NS_WARNING_ASSERTION(numPagesOnThisSheet
> 0,
237 "Shouldn't create a sheet with no displayable pages "
240 MOZ_ASSERT(numPagesOnThisSheet
> 0,
241 "Shouldn't create a sheet with no displayable pages on it");
244 MOZ_ASSERT(numPagesOnThisSheet
<= desiredPagesPerSheet
,
245 "Shouldn't have more than desired number of displayable pages "
247 mNumPages
= numPagesOnThisSheet
;
249 // Populate our ReflowOutput outparam -- just use up all the
250 // available space, for both our desired size & overflow areas.
251 aReflowOutput
.ISize(wm
) = aReflowInput
.AvailableISize();
252 if (aReflowInput
.AvailableBSize() != NS_UNCONSTRAINEDSIZE
) {
253 aReflowOutput
.BSize(wm
) = aReflowInput
.AvailableBSize();
255 aReflowOutput
.SetOverflowAreasToDesiredBounds();
257 FinishAndStoreOverflow(&aReflowOutput
);
260 nsSize
PrintedSheetFrame::ComputeSheetSize(const nsPresContext
* aPresContext
) {
261 // We use the user selected page (sheet) dimensions, and default to the
262 // orientation as specified by the user.
263 nsSize sheetSize
= aPresContext
->GetPageSize();
265 // Don't waste cycles changing the orientation of a square.
266 if (sheetSize
.width
== sheetSize
.height
) {
270 if (!StaticPrefs::layout_css_page_orientation_enabled()) {
271 if (mPD
->mPrintSettings
->HasOrthogonalSheetsAndPages()) {
272 std::swap(sheetSize
.width
, sheetSize
.height
);
277 auto* firstChild
= PrincipalChildList().FirstChild();
278 MOZ_ASSERT(firstChild
->IsPageFrame(),
279 "PrintedSheetFrame only has nsPageFrame children");
280 auto* sheetsFirstPageFrame
= static_cast<nsPageFrame
*>(firstChild
);
282 nsSize pageSize
= sheetsFirstPageFrame
->ComputePageSize();
284 // Don't waste cycles changing the orientation of a square.
285 if (pageSize
.width
== pageSize
.height
) {
289 const bool pageIsRotated
=
290 sheetsFirstPageFrame
->GetPageOrientationRotation(mPD
) != 0.0;
292 if (pageIsRotated
&& pageSize
.width
== pageSize
.height
) {
293 // Straighforward rotation without needing sheet orientation optimization.
294 std::swap(sheetSize
.width
, sheetSize
.height
);
298 // Try to orient the sheet optimally based on the physical orientation of the
299 // first/sole page on the sheet. (In the multiple pages-per-sheet case, the
300 // first page is the only one that exists at this point in the code, so it is
301 // the only one we can reason about. Any other pages may, or may not, have
302 // the same physical orientation.)
305 // Fix up for its physical orientation:
306 std::swap(pageSize
.width
, pageSize
.height
);
309 const bool pageIsPortrait
= pageSize
.width
< pageSize
.height
;
310 const bool sheetIsPortrait
= sheetSize
.width
< sheetSize
.height
;
312 // Switch the sheet orientation if the page orientation is different, or
313 // if we need to switch it because the number of pages-per-sheet demands
314 // orthogonal sheet layout, but not if both are true since then we'd
315 // actually need to double switch.
316 if ((sheetIsPortrait
!= pageIsPortrait
) !=
317 mPD
->mPrintSettings
->HasOrthogonalSheetsAndPages()) {
318 std::swap(sheetSize
.width
, sheetSize
.height
);
324 void PrintedSheetFrame::ComputePagesPerSheetGridMetrics(
325 const nsSize
& aSheetSize
) {
326 MOZ_ASSERT(mPD
->PagesPerSheetInfo()->mNumPages
> 1,
327 "Unnecessary to call this in a regular 1-page-per-sheet scenario; "
328 "the computed values won't ever be used in that case");
330 // Compute the space available for the pages-per-sheet "page grid" (just
331 // subtract the sheet's unwriteable margin area):
332 nsSize availSpaceOnSheet
= aSheetSize
;
333 nsMargin uwm
= mPD
->mPrintSettings
->GetIgnoreUnwriteableMargins()
335 : nsPresContext::CSSTwipsToAppUnits(
336 mPD
->mPrintSettings
->GetUnwriteableMarginInTwips());
338 // XXXjwatt Once we support heterogeneous sheet orientations, we'll also need
339 // to rotate uwm if this sheet is not the primary orientation.
340 if (mPD
->mPrintSettings
->HasOrthogonalSheetsAndPages()) {
341 // aSheetSize already takes account of the switch of *sheet* orientation
342 // that we do in this case (the orientation implied by the page size
343 // dimensions in the nsIPrintSettings applies to *pages*). That is not the
344 // case for the unwriteable margins since we got them from the
345 // nsIPrintSettings object ourself, so we need to adjust `uwm` here.
347 // Note: In practice, sheets with an orientation that is orthogonal to the
348 // physical orientation of sheets output by a printer must be rotated 90
349 // degrees for/by the printer. In that case the convention seems to be that
350 // the "left" edge of the orthogonally oriented sheet becomes the "top",
351 // and so forth. The rotation direction will matter in the case that the
352 // top and bottom unwriteable margins are different, or the left and right
353 // unwriteable margins are different. So we need to match this behavior,
354 // which means we must rotate the `uwm` 90 degrees *counter-clockwise*.
355 nsMargin
rotated(uwm
.right
, uwm
.bottom
, uwm
.left
, uwm
.top
);
359 availSpaceOnSheet
.width
-= uwm
.LeftRight();
360 availSpaceOnSheet
.height
-= uwm
.TopBottom();
362 if (MOZ_UNLIKELY(availSpaceOnSheet
.IsEmpty())) {
363 // This sort of thing should be rare, but it can happen if there are
364 // bizarre page sizes, and/or if there's an unexpectedly large unwriteable
366 NS_WARNING("Zero area for pages-per-sheet grid, or zero-sized grid");
367 mGridOrigin
= nsPoint(0, 0);
372 // If there are a different number of rows vs. cols, we'll aim to put
373 // the larger number of items in the longer axis.
374 const auto* ppsInfo
= mPD
->PagesPerSheetInfo();
375 uint32_t smallerNumTracks
= ppsInfo
->mNumPages
/ ppsInfo
->mLargerNumTracks
;
376 bool sheetIsPortraitLike
= aSheetSize
.width
< aSheetSize
.height
;
378 sheetIsPortraitLike
? smallerNumTracks
: ppsInfo
->mLargerNumTracks
;
380 sheetIsPortraitLike
? ppsInfo
->mLargerNumTracks
: smallerNumTracks
;
382 mGridOrigin
= nsPoint(uwm
.left
, uwm
.top
);
383 mGridNumCols
= numCols
;
384 mGridCellWidth
= availSpaceOnSheet
.width
/ nscoord(numCols
);
385 mGridCellHeight
= availSpaceOnSheet
.height
/ nscoord(numRows
);
388 gfx::IntSize
PrintedSheetFrame::GetPrintTargetSizeInPoints(
389 const int32_t aAppUnitsPerPhysicalInch
) const {
390 const auto size
= GetSize();
391 MOZ_ASSERT(size
.width
> 0 && size
.height
> 0);
392 const float pointsPerAppUnit
=
393 POINTS_PER_INCH_FLOAT
/ float(aAppUnitsPerPhysicalInch
);
394 return IntSize::Ceil(float(size
.width
) * pointsPerAppUnit
,
395 float(size
.height
) * pointsPerAppUnit
);
398 #ifdef DEBUG_FRAME_DUMP
399 nsresult
PrintedSheetFrame::GetFrameName(nsAString
& aResult
) const {
400 return MakeFrameName(u
"PrintedSheet"_ns
, aResult
);
404 } // namespace mozilla