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_print.h"
14 #include "nsCSSFrameConstructor.h"
15 #include "nsPageFrame.h"
16 #include "nsPageSequenceFrame.h"
18 using namespace mozilla
;
20 PrintedSheetFrame
* NS_NewPrintedSheetFrame(PresShell
* aPresShell
,
21 ComputedStyle
* aStyle
) {
22 return new (aPresShell
)
23 PrintedSheetFrame(aStyle
, aPresShell
->GetPresContext());
28 NS_QUERYFRAME_HEAD(PrintedSheetFrame
)
29 NS_QUERYFRAME_ENTRY(PrintedSheetFrame
)
30 NS_QUERYFRAME_TAIL_INHERITING(nsContainerFrame
)
32 NS_IMPL_FRAMEARENA_HELPERS(PrintedSheetFrame
)
34 void PrintedSheetFrame::BuildDisplayList(nsDisplayListBuilder
* aBuilder
,
35 const nsDisplayListSet
& aLists
) {
36 if (PresContext()->IsScreen()) {
37 // Draw the background/shadow/etc. of a blank sheet of paper, for
39 DisplayBorderBackgroundOutline(aBuilder
, aLists
);
42 for (auto* frame
: mFrames
) {
43 if (!frame
->HasAnyStateBits(NS_PAGE_SKIPPED_BY_CUSTOM_RANGE
)) {
44 BuildDisplayListForChild(aBuilder
, frame
, aLists
);
49 // If the given page is included in the user's page range, this function
50 // returns false. Otherwise, it tags the page with the
51 // NS_PAGE_SKIPPED_BY_CUSTOM_RANGE state bit and returns true.
52 static bool TagIfSkippedByCustomRange(nsPageFrame
* aPageFrame
, int32_t aPageNum
,
53 nsSharedPageData
* aPD
) {
54 if (!nsIPrintSettings::IsPageSkipped(aPageNum
, aPD
->mPageRanges
)) {
55 MOZ_ASSERT(!aPageFrame
->HasAnyStateBits(NS_PAGE_SKIPPED_BY_CUSTOM_RANGE
),
56 "page frames NS_PAGE_SKIPPED_BY_CUSTOM_RANGE state should "
57 "only be set if we actually want to skip the page");
61 aPageFrame
->AddStateBits(NS_PAGE_SKIPPED_BY_CUSTOM_RANGE
);
65 void PrintedSheetFrame::Reflow(nsPresContext
* aPresContext
,
66 ReflowOutput
& aReflowOutput
,
67 const ReflowInput
& aReflowInput
,
68 nsReflowStatus
& aStatus
) {
70 DO_GLOBAL_REFLOW_COUNT("PrintedSheetFrame");
71 DISPLAY_REFLOW(aPresContext
, this, aReflowInput
, aReflowOutput
, aStatus
);
72 MOZ_ASSERT(aStatus
.IsEmpty(), "Caller should pass a fresh reflow status!");
74 // If we have a prev-in-flow, take its overflowing content:
75 MoveOverflowToChildList();
77 const WritingMode wm
= aReflowInput
.GetWritingMode();
79 // This is the app-unit size of each page (in physical & logical units):
80 const nsSize physPageSize
= aPresContext
->GetPageSize();
81 const LogicalSize
pageSize(wm
, physPageSize
);
83 // Count the number of pages that are displayed on this sheet (i.e. how many
84 // child frames we end up laying out, excluding any pages that are skipped
85 // due to not being in the user's page-range selection).
86 uint32_t numPagesOnThisSheet
= 0;
88 // Target for numPagesOnThisSheet.
89 const uint32_t desiredPagesPerSheet
= mPD
->PagesPerSheetInfo()->mNumPages
;
91 // If we're the first continuation and we're doing >1 pages per sheet,
92 // precompute some metrics that we'll use when painting the pages:
93 if (desiredPagesPerSheet
> 1 && !GetPrevContinuation()) {
94 ComputePagesPerSheetOriginAndScale();
97 // NOTE: I'm intentionally *not* using a range-based 'for' loop here, since
98 // we potentially mutate the frame list (appending to the end) during the
99 // list, which is not generally safe with range-based 'for' loops.
100 for (auto* childFrame
= mFrames
.FirstChild(); childFrame
;
101 childFrame
= childFrame
->GetNextSibling()) {
102 MOZ_ASSERT(childFrame
->IsPageFrame(),
103 "we're only expecting page frames as children");
104 auto* pageFrame
= static_cast<nsPageFrame
*>(childFrame
);
106 // Be sure our child has a pointer to the nsSharedPageData and knows its
108 pageFrame
->SetSharedPageData(mPD
);
109 pageFrame
->DeterminePageNum();
111 if (!TagIfSkippedByCustomRange(pageFrame
, pageFrame
->GetPageNum(), mPD
)) {
112 // The page is going to be displayed on this sheet. Tell it its index
113 // among the displayed pages, so we can use that to compute its "cell"
115 pageFrame
->SetIndexOnSheet(numPagesOnThisSheet
);
116 numPagesOnThisSheet
++;
119 ReflowInput
pageReflowInput(aPresContext
, aReflowInput
, pageFrame
,
122 // For layout purposes, we position *all* our nsPageFrame children at our
123 // origin. Then, if we have multiple pages-per-sheet, we'll shrink & shift
124 // each one into the right position as a paint-time effect, in
126 LogicalPoint
pagePos(wm
);
128 // Outparams for reflow:
129 ReflowOutput
pageReflowOutput(pageReflowInput
);
130 nsReflowStatus status
;
132 ReflowChild(pageFrame
, aPresContext
, pageReflowOutput
, pageReflowInput
, wm
,
133 pagePos
, physPageSize
, ReflowChildFlags::Default
, status
);
135 FinishReflowChild(pageFrame
, aPresContext
, pageReflowOutput
,
136 &pageReflowInput
, wm
, pagePos
, physPageSize
,
137 ReflowChildFlags::Default
);
139 // Since we don't support incremental reflow in printed documents (see the
140 // early-return in nsPageSequenceFrame::Reflow), we can assume that this
141 // was the first time that pageFrame has been reflowed, and so there's no
142 // way that it could already have a next-in-flow. If it *did* have a
143 // next-in-flow, we would need to handle it in the 'status' logic below.
144 NS_ASSERTION(!pageFrame
->GetNextInFlow(), "bad child flow list");
146 // Did this page complete the document, or do we need to generate
147 // another page frame?
148 if (status
.IsFullyComplete()) {
149 // The page we just reflowed is the final page! Record its page number
150 // as the number of pages:
151 mPD
->mRawNumPages
= pageFrame
->GetPageNum();
153 // Create a continuation for our page frame. We add the continuation to
154 // our child list, and then potentially push it to our overflow list, if
155 // it really belongs on the next sheet.
156 nsIFrame
* continuingPage
=
157 PresShell()->FrameConstructor()->CreateContinuingFrame(pageFrame
,
159 mFrames
.InsertFrame(nullptr, pageFrame
, continuingPage
);
160 const bool isContinuingPageSkipped
=
161 TagIfSkippedByCustomRange(static_cast<nsPageFrame
*>(continuingPage
),
162 pageFrame
->GetPageNum() + 1, mPD
);
164 // If we've already reached the target number of pages for this sheet,
165 // and this continuation page that we just created is meant to be
166 // displayed (i.e. it's in the chosen page range), then we need to push it
167 // to our overflow list so that it'll go onto a subsequent sheet.
168 // Otherwise we leave it on this sheet. This ensures we *only* generate
169 // another sheet IFF there's a displayable page that will end up on it.
170 if (numPagesOnThisSheet
>= desiredPagesPerSheet
&&
171 !isContinuingPageSkipped
) {
172 PushChildrenToOverflow(continuingPage
, pageFrame
);
173 aStatus
.SetIncomplete();
178 // This should hold for the first sheet, because our UI should prevent the
179 // user from creating a 0-length page range; and it should hold for
180 // subsequent sheets because we should only create an additional sheet when
181 // we discover a displayable (i.e. non-skipped) page that we need to push
182 // to that new sheet.
184 // XXXdholbert In certain edge cases (e.g. after a page-orientation-flip that
185 // reduces the page count), it's possible for us to be given a page range
186 // that is *entirely out-of-bounds* (with "from" & "to" both being larger
187 // than our actual page-number count). This scenario produces a single
188 // PrintedSheetFrame with zero displayable pages on it, which is a weird
189 // state to be in. This is hopefully a scenario that the frontend code can
190 // detect and recover from (e.g. by clamping the range to our reported
191 // `rawNumPages`), but it can't do that until *after* we've completed this
192 // problematic reflow and can reported an up-to-date `rawNumPages` to the
193 // frontend. So: to give the frontend a chance to intervene and apply some
194 // correction/clamping to its print-range parameters, we soften this
195 // assertion *specifically for the first printed sheet*.
196 if (!GetPrevContinuation()) {
197 NS_WARNING_ASSERTION(numPagesOnThisSheet
> 0,
198 "Shouldn't create a sheet with no displayable pages "
201 MOZ_ASSERT(numPagesOnThisSheet
> 0,
202 "Shouldn't create a sheet with no displayable pages on it");
205 MOZ_ASSERT(numPagesOnThisSheet
<= desiredPagesPerSheet
,
206 "Shouldn't have more than desired number of displayable pages "
208 mNumPages
= numPagesOnThisSheet
;
210 // Populate our ReflowOutput outparam -- just use up all the
211 // available space, for both our desired size & overflow areas.
212 aReflowOutput
.ISize(wm
) = aReflowInput
.AvailableISize();
213 if (aReflowInput
.AvailableBSize() != NS_UNCONSTRAINEDSIZE
) {
214 aReflowOutput
.BSize(wm
) = aReflowInput
.AvailableBSize();
216 aReflowOutput
.SetOverflowAreasToDesiredBounds();
218 FinishAndStoreOverflow(&aReflowOutput
);
221 void PrintedSheetFrame::ComputePagesPerSheetOriginAndScale() {
222 MOZ_ASSERT(mPD
->PagesPerSheetInfo()->mNumPages
> 1,
223 "Unnecessary to call this in a regular 1-page-per-sheet scenario; "
224 "the computed values won't ever be used in that case");
225 MOZ_ASSERT(!GetPrevContinuation(),
226 "Only needs to be called once, so 1st continuation handles it");
228 // The "full-scale" size of a page (if it weren't shrunk down into a grid):
229 const nsSize pageSize
= PresContext()->GetPageSize();
231 // Compute the space available for the pages-per-sheet "page grid" (just
232 // subtract the sheet's unwriteable margin area):
233 nsSize availSpaceOnSheet
= pageSize
;
234 nsMargin uwm
= mPD
->mPrintSettings
->GetIgnoreUnwriteableMargins()
236 : nsPresContext::CSSTwipsToAppUnits(
237 mPD
->mPrintSettings
->GetUnwriteableMarginInTwips());
239 if (mPD
->mPrintSettings
->HasOrthogonalSheetsAndPages()) {
240 // The pages will be rotated to be orthogonal to the physical sheet. To
241 // account for that, we rotate the components of availSpaceOnSheet and uwm,
242 // so that we can reason about them here from the perspective of a
243 // "pageSize"-oriented *page*.
244 std::swap(availSpaceOnSheet
.width
, availSpaceOnSheet
.height
);
246 // Note that the pages are rotated 90 degrees clockwise when placed onto a
247 // sheet (so that, e.g. in a scenario with two side-by-side portait pages
248 // that are rotated & placed onto a sheet, the "left" edge of the first
249 // page is at the "top" of the sheet and hence comes out of the printer
250 // first, etc). So: given `nsMargin uwm` whose sides correspond to the
251 // physical sheet's sides, we have to rotate 90 degrees *counter-clockwise*
252 // in order to "cancel out" the page rotation and to represent it in the
253 // page's perspective. From a page's perspective, its own "top" side
254 // corresponds to the physical sheet's right side, which is why we're
255 // passing "uwm.right" as the "top" component here; and so on.
256 nsMargin
rotated(uwm
.right
, uwm
.bottom
, uwm
.left
, uwm
.top
);
260 availSpaceOnSheet
.width
-= uwm
.LeftRight();
261 availSpaceOnSheet
.height
-= uwm
.TopBottom();
262 nsPoint
pageGridOrigin(uwm
.left
, uwm
.top
);
264 // If there are a different number of rows vs. cols, we'll aim to put
265 // the larger number of items in the longer axis.
266 const auto* ppsInfo
= mPD
->PagesPerSheetInfo();
267 uint32_t smallerNumTracks
= ppsInfo
->mNumPages
/ ppsInfo
->mLargerNumTracks
;
268 bool pageSizeIsPortraitLike
= pageSize
.width
> pageSize
.height
;
270 pageSizeIsPortraitLike
? smallerNumTracks
: ppsInfo
->mLargerNumTracks
;
272 pageSizeIsPortraitLike
? ppsInfo
->mLargerNumTracks
: smallerNumTracks
;
274 // Compute the full size of the "page grid" that we'll be scaling down &
275 // placing onto a given sheet:
276 nsSize
pageGridFullSize(numCols
* pageSize
.width
, numRows
* pageSize
.height
);
278 if (MOZ_UNLIKELY(availSpaceOnSheet
.IsEmpty() || pageGridFullSize
.IsEmpty())) {
279 // Either we have a 0-sized available area, or we have a 0-sized page-grid
280 // to draw into the available area. This sort of thing should be rare, but
281 // it can happen if there are bizarre page sizes, and/or if there's an
282 // unexpectedly large unwritable margin area. Regardless: if we get here,
283 // we shouldn't be drawing anything onto the sheet; so let's just use a
284 // scale factor of 0, and bail early to avoid division by 0 in hScale &
285 // vScale computations below.
286 NS_WARNING("Zero area for pages-per-sheet grid, or zero-sized grid");
287 mPD
->mPagesPerSheetGridOrigin
= pageGridOrigin
;
288 mPD
->mPagesPerSheetNumCols
= 1;
289 mPD
->mPagesPerSheetScale
= 0.0f
;
293 // Compute the scale factors required in each axis:
295 availSpaceOnSheet
.width
/ static_cast<float>(pageGridFullSize
.width
);
297 availSpaceOnSheet
.height
/ static_cast<float>(pageGridFullSize
.height
);
299 // Choose the more restrictive scale factor (so that we don't overflow the
300 // sheet's printable area in either axis). And center the page-grid in the
301 // other axis (since it probably ends up with extra space).
302 float scale
= std::min(hScale
, vScale
);
303 if (hScale
< vScale
) {
304 // hScale is the more restrictive scale-factor, so we'll be using that.
305 // Nudge the grid in the vertical axis to center it:
306 nscoord extraSpace
= availSpaceOnSheet
.height
-
307 NSToCoordFloor(scale
* pageGridFullSize
.height
);
308 if (MOZ_LIKELY(extraSpace
> 0)) {
309 pageGridOrigin
.y
+= extraSpace
/ 2;
311 } else if (vScale
< hScale
) {
312 // vScale is the more restrictive scale-factor, so we'll be using that.
313 // Nudge the grid in the vertical axis to center it:
314 nscoord extraSpace
= availSpaceOnSheet
.width
-
315 NSToCoordFloor(scale
* pageGridFullSize
.width
);
316 if (MOZ_LIKELY(extraSpace
> 0)) {
317 pageGridOrigin
.x
+= extraSpace
/ 2;
320 // else, we fit exactly in both axes, with the same scale factor, so there's
321 // no extra space in either axis, i.e. no need to center.
323 // Update the nsSharedPageData member data:
324 mPD
->mPagesPerSheetGridOrigin
= pageGridOrigin
;
325 mPD
->mPagesPerSheetNumCols
= numCols
;
326 mPD
->mPagesPerSheetScale
= scale
;
329 #ifdef DEBUG_FRAME_DUMP
330 nsresult
PrintedSheetFrame::GetFrameName(nsAString
& aResult
) const {
331 return MakeFrameName(u
"PrintedSheet"_ns
, aResult
);
335 } // namespace mozilla