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 http://mozilla.org/MPL/2.0/. */
6
7 /* Per-block-formatting-context manager of font size inflation for pan and zoom
8 * UI. */
9
10 #include "nsFontInflationData.h"
11 #include "FrameProperties.h"
12 #include "nsTextControlFrame.h"
13 #include "nsListControlFrame.h"
14 #include "nsComboboxControlFrame.h"
15 #include "mozilla/dom/Text.h" // for inline nsINode::AsText() definition
16 #include "mozilla/PresShell.h"
17 #include "mozilla/ReflowInput.h"
18 #include "nsTextFrameUtils.h"
19
20 using namespace mozilla;
21 using namespace mozilla::layout;
22
NS_DECLARE_FRAME_PROPERTY_DELETABLE(FontInflationDataProperty,nsFontInflationData)23 NS_DECLARE_FRAME_PROPERTY_DELETABLE(FontInflationDataProperty,
24 nsFontInflationData)
25
26 /* static */ nsFontInflationData* nsFontInflationData::FindFontInflationDataFor(
27 const nsIFrame* aFrame) {
28 // We have one set of font inflation data per block formatting context.
29 const nsIFrame* bfc = FlowRootFor(aFrame);
30 NS_ASSERTION(bfc->HasAnyStateBits(NS_FRAME_FONT_INFLATION_FLOW_ROOT),
31 "should have found a flow root");
32 MOZ_ASSERT(aFrame->GetWritingMode().IsVertical() ==
33 bfc->GetWritingMode().IsVertical(),
34 "current writing mode should match that of our flow root");
35
36 return bfc->GetProperty(FontInflationDataProperty());
37 }
38
39 /* static */
UpdateFontInflationDataISizeFor(const ReflowInput & aReflowInput)40 bool nsFontInflationData::UpdateFontInflationDataISizeFor(
41 const ReflowInput& aReflowInput) {
42 nsIFrame* bfc = aReflowInput.mFrame;
43 NS_ASSERTION(bfc->HasAnyStateBits(NS_FRAME_FONT_INFLATION_FLOW_ROOT),
44 "should have been given a flow root");
45 nsFontInflationData* data = bfc->GetProperty(FontInflationDataProperty());
46 bool oldInflationEnabled;
47 nscoord oldUsableISize;
48 if (data) {
49 oldUsableISize = data->mUsableISize;
50 oldInflationEnabled = data->mInflationEnabled;
51 } else {
52 data = new nsFontInflationData(bfc);
53 bfc->SetProperty(FontInflationDataProperty(), data);
54 oldUsableISize = -1;
55 oldInflationEnabled = true; /* not relevant */
56 }
57
58 data->UpdateISize(aReflowInput);
59
60 if (oldInflationEnabled != data->mInflationEnabled) return true;
61
62 return oldInflationEnabled && oldUsableISize != data->mUsableISize;
63 }
64
65 /* static */
MarkFontInflationDataTextDirty(nsIFrame * aBFCFrame)66 void nsFontInflationData::MarkFontInflationDataTextDirty(nsIFrame* aBFCFrame) {
67 NS_ASSERTION(aBFCFrame->HasAnyStateBits(NS_FRAME_FONT_INFLATION_FLOW_ROOT),
68 "should have been given a flow root");
69
70 nsFontInflationData* data =
71 aBFCFrame->GetProperty(FontInflationDataProperty());
72 if (data) {
73 data->MarkTextDirty();
74 }
75 }
76
nsFontInflationData(nsIFrame * aBFCFrame)77 nsFontInflationData::nsFontInflationData(nsIFrame* aBFCFrame)
78 : mBFCFrame(aBFCFrame),
79 mUsableISize(0),
80 mTextAmount(0),
81 mTextThreshold(0),
82 mInflationEnabled(false),
83 mTextDirty(true) {}
84
85 /**
86 * Find the closest common ancestor between aFrame1 and aFrame2, except
87 * treating the parent of a frame as the first-in-flow of its parent (so
88 * the result doesn't change when breaking changes).
89 *
90 * aKnownCommonAncestor is a known common ancestor of both.
91 */
NearestCommonAncestorFirstInFlow(nsIFrame * aFrame1,nsIFrame * aFrame2,nsIFrame * aKnownCommonAncestor)92 static nsIFrame* NearestCommonAncestorFirstInFlow(
93 nsIFrame* aFrame1, nsIFrame* aFrame2, nsIFrame* aKnownCommonAncestor) {
94 aFrame1 = aFrame1->FirstInFlow();
95 aFrame2 = aFrame2->FirstInFlow();
96 aKnownCommonAncestor = aKnownCommonAncestor->FirstInFlow();
97
98 AutoTArray<nsIFrame*, 32> ancestors1, ancestors2;
99 for (nsIFrame* f = aFrame1; f != aKnownCommonAncestor;
100 (f = f->GetParent()) && (f = f->FirstInFlow())) {
101 ancestors1.AppendElement(f);
102 }
103 for (nsIFrame* f = aFrame2; f != aKnownCommonAncestor;
104 (f = f->GetParent()) && (f = f->FirstInFlow())) {
105 ancestors2.AppendElement(f);
106 }
107
108 nsIFrame* result = aKnownCommonAncestor;
109 uint32_t i1 = ancestors1.Length(), i2 = ancestors2.Length();
110 while (i1-- != 0 && i2-- != 0) {
111 if (ancestors1[i1] != ancestors2[i2]) {
112 break;
113 }
114 result = ancestors1[i1];
115 }
116
117 return result;
118 }
119
ComputeDescendantISize(const ReflowInput & aAncestorReflowInput,nsIFrame * aDescendantFrame)120 static nscoord ComputeDescendantISize(const ReflowInput& aAncestorReflowInput,
121 nsIFrame* aDescendantFrame) {
122 nsIFrame* ancestorFrame = aAncestorReflowInput.mFrame->FirstInFlow();
123 if (aDescendantFrame == ancestorFrame) {
124 return aAncestorReflowInput.ComputedISize();
125 }
126
127 AutoTArray<nsIFrame*, 16> frames;
128 for (nsIFrame* f = aDescendantFrame; f != ancestorFrame;
129 f = f->GetParent()->FirstInFlow()) {
130 frames.AppendElement(f);
131 }
132
133 // This ignores the inline-size contributions made by scrollbars, though in
134 // reality we don't have any scrollbars on the sorts of devices on
135 // which we use font inflation, so it's not a problem. But it may
136 // occasionally cause problems when writing tests on desktop.
137
138 uint32_t len = frames.Length();
139 ReflowInput* reflowInputs =
140 static_cast<ReflowInput*>(moz_xmalloc(sizeof(ReflowInput) * len));
141 nsPresContext* presContext = aDescendantFrame->PresContext();
142 for (uint32_t i = 0; i < len; ++i) {
143 const ReflowInput& parentReflowInput =
144 (i == 0) ? aAncestorReflowInput : reflowInputs[i - 1];
145 nsIFrame* frame = frames[len - i - 1];
146 WritingMode wm = frame->GetWritingMode();
147 LogicalSize availSize = parentReflowInput.ComputedSize(wm);
148 availSize.BSize(wm) = NS_UNCONSTRAINEDSIZE;
149 MOZ_ASSERT(frame->GetParent()->FirstInFlow() ==
150 parentReflowInput.mFrame->FirstInFlow(),
151 "bad logic in this function");
152 new (reflowInputs + i)
153 ReflowInput(presContext, parentReflowInput, frame, availSize);
154 }
155
156 MOZ_ASSERT(reflowInputs[len - 1].mFrame == aDescendantFrame,
157 "bad logic in this function");
158 nscoord result = reflowInputs[len - 1].ComputedISize();
159
160 for (uint32_t i = len; i-- != 0;) {
161 reflowInputs[i].~ReflowInput();
162 }
163 free(reflowInputs);
164
165 return result;
166 }
167
UpdateISize(const ReflowInput & aReflowInput)168 void nsFontInflationData::UpdateISize(const ReflowInput& aReflowInput) {
169 nsIFrame* bfc = aReflowInput.mFrame;
170 NS_ASSERTION(bfc->HasAnyStateBits(NS_FRAME_FONT_INFLATION_FLOW_ROOT),
171 "must be block formatting context");
172
173 nsIFrame* firstInflatableDescendant =
174 FindEdgeInflatableFrameIn(bfc, eFromStart);
175 if (!firstInflatableDescendant) {
176 mTextAmount = 0;
177 mTextThreshold = 0; // doesn't matter
178 mTextDirty = false;
179 mInflationEnabled = false;
180 return;
181 }
182 nsIFrame* lastInflatableDescendant = FindEdgeInflatableFrameIn(bfc, eFromEnd);
183 MOZ_ASSERT(!firstInflatableDescendant == !lastInflatableDescendant,
184 "null-ness should match; NearestCommonAncestorFirstInFlow"
185 " will crash when passed null");
186
187 // Particularly when we're computing for the root BFC, the inline-size of
188 // nca might differ significantly for the inline-size of bfc.
189 nsIFrame* nca = NearestCommonAncestorFirstInFlow(
190 firstInflatableDescendant, lastInflatableDescendant, bfc);
191 while (!nca->IsContainerForFontSizeInflation()) {
192 nca = nca->GetParent()->FirstInFlow();
193 }
194
195 nscoord newNCAISize = ComputeDescendantISize(aReflowInput, nca);
196
197 // See comment above "font.size.inflation.lineThreshold" in
198 // modules/libpref/src/init/StaticPrefList.yaml .
199 PresShell* presShell = bfc->PresShell();
200 uint32_t lineThreshold = presShell->FontSizeInflationLineThreshold();
201 nscoord newTextThreshold = (newNCAISize * lineThreshold) / 100;
202
203 if (mTextThreshold <= mTextAmount && mTextAmount < newTextThreshold) {
204 // Because we truncate our scan when we hit sufficient text, we now
205 // need to rescan.
206 mTextDirty = true;
207 }
208
209 // Font inflation increases the font size for a given flow root so that the
210 // text is legible when we've zoomed such that the respective nearest common
211 // ancestor's (NCA) full inline-size (ISize) fills the screen. We assume how-
212 // ever that we don't want to zoom out further than the root iframe's ISize
213 // (i.e. the viewport for a top-level document, or the containing iframe
214 // otherwise), since in some cases zooming out further might not even be
215 // possible or make sense.
216 // Hence the ISize assumed to be usable for displaying text is limited to the
217 // visible area.
218 nsPresContext* presContext = bfc->PresContext();
219 MOZ_ASSERT(
220 bfc->GetWritingMode().IsVertical() == nca->GetWritingMode().IsVertical(),
221 "writing mode of NCA should match that of its flow root");
222 nscoord iFrameISize = bfc->GetWritingMode().IsVertical()
223 ? presContext->GetVisibleArea().height
224 : presContext->GetVisibleArea().width;
225 mUsableISize = std::min(iFrameISize, newNCAISize);
226 mTextThreshold = newTextThreshold;
227 mInflationEnabled = mTextAmount >= mTextThreshold;
228 }
229
FindEdgeInflatableFrameIn(nsIFrame * aFrame,SearchDirection aDirection)230 /* static */ nsIFrame* nsFontInflationData::FindEdgeInflatableFrameIn(
231 nsIFrame* aFrame, SearchDirection aDirection) {
232 // NOTE: This function has a similar structure to ScanTextIn!
233
234 // FIXME: Should probably only scan the text that's actually going to
235 // be inflated!
236
237 nsIFormControlFrame* fcf = do_QueryFrame(aFrame);
238 if (fcf) {
239 return aFrame;
240 }
241
242 // FIXME: aDirection!
243 AutoTArray<FrameChildList, 4> lists;
244 aFrame->GetChildLists(&lists);
245 for (uint32_t i = 0, len = lists.Length(); i < len; ++i) {
246 const nsFrameList& list =
247 lists[(aDirection == eFromStart) ? i : len - i - 1].mList;
248 for (nsIFrame* kid = (aDirection == eFromStart) ? list.FirstChild()
249 : list.LastChild();
250 kid; kid = (aDirection == eFromStart) ? kid->GetNextSibling()
251 : kid->GetPrevSibling()) {
252 if (kid->HasAnyStateBits(NS_FRAME_FONT_INFLATION_FLOW_ROOT)) {
253 // Goes in a different set of inflation data.
254 continue;
255 }
256
257 if (kid->IsTextFrame()) {
258 nsIContent* content = kid->GetContent();
259 if (content && kid == content->GetPrimaryFrame()) {
260 uint32_t len = nsTextFrameUtils::
261 ComputeApproximateLengthWithWhitespaceCompression(
262 content->AsText(), kid->StyleText());
263 if (len != 0) {
264 return kid;
265 }
266 }
267 } else {
268 nsIFrame* kidResult = FindEdgeInflatableFrameIn(kid, aDirection);
269 if (kidResult) {
270 return kidResult;
271 }
272 }
273 }
274 }
275
276 return nullptr;
277 }
278
ScanText()279 void nsFontInflationData::ScanText() {
280 mTextDirty = false;
281 mTextAmount = 0;
282 ScanTextIn(mBFCFrame);
283 mInflationEnabled = mTextAmount >= mTextThreshold;
284 }
285
DoCharCountOfLargestOption(nsIFrame * aContainer)286 static uint32_t DoCharCountOfLargestOption(nsIFrame* aContainer) {
287 uint32_t result = 0;
288 for (nsIFrame* option : aContainer->PrincipalChildList()) {
289 uint32_t optionResult;
290 if (option->GetContent()->IsHTMLElement(nsGkAtoms::optgroup)) {
291 optionResult = DoCharCountOfLargestOption(option);
292 } else {
293 // REVIEW: Check the frame structure for this!
294 optionResult = 0;
295 for (nsIFrame* optionChild : option->PrincipalChildList()) {
296 if (optionChild->IsTextFrame()) {
297 optionResult += nsTextFrameUtils::
298 ComputeApproximateLengthWithWhitespaceCompression(
299 optionChild->GetContent()->AsText(),
300 optionChild->StyleText());
301 }
302 }
303 }
304 if (optionResult > result) {
305 result = optionResult;
306 }
307 }
308 return result;
309 }
310
CharCountOfLargestOption(nsIFrame * aListControlFrame)311 static uint32_t CharCountOfLargestOption(nsIFrame* aListControlFrame) {
312 return DoCharCountOfLargestOption(
313 static_cast<nsListControlFrame*>(aListControlFrame)
314 ->GetOptionsContainer());
315 }
316
ScanTextIn(nsIFrame * aFrame)317 void nsFontInflationData::ScanTextIn(nsIFrame* aFrame) {
318 // NOTE: This function has a similar structure to FindEdgeInflatableFrameIn!
319
320 // FIXME: Should probably only scan the text that's actually going to
321 // be inflated!
322
323 for (const auto& childList : aFrame->ChildLists()) {
324 for (nsIFrame* kid : childList.mList) {
325 if (kid->HasAnyStateBits(NS_FRAME_FONT_INFLATION_FLOW_ROOT)) {
326 // Goes in a different set of inflation data.
327 continue;
328 }
329
330 LayoutFrameType fType = kid->Type();
331 if (fType == LayoutFrameType::Text) {
332 nsIContent* content = kid->GetContent();
333 if (content && kid == content->GetPrimaryFrame()) {
334 uint32_t len = nsTextFrameUtils::
335 ComputeApproximateLengthWithWhitespaceCompression(
336 content->AsText(), kid->StyleText());
337 if (len != 0) {
338 nscoord fontSize = kid->StyleFont()->mFont.size.ToAppUnits();
339 if (fontSize > 0) {
340 mTextAmount += fontSize * len;
341 }
342 }
343 }
344 } else if (fType == LayoutFrameType::TextInput) {
345 // We don't want changes to the amount of text in a text input
346 // to change what we count towards inflation.
347 nscoord fontSize = kid->StyleFont()->mFont.size.ToAppUnits();
348 int32_t charCount = static_cast<nsTextControlFrame*>(kid)->GetCols();
349 mTextAmount += charCount * fontSize;
350 } else if (fType == LayoutFrameType::ComboboxControl) {
351 // See textInputFrame above (with s/amount of text/selected option/).
352 // Don't just recurse down to the list control inside, since we
353 // need to exclude the display frame.
354 nscoord fontSize = kid->StyleFont()->mFont.size.ToAppUnits();
355 int32_t charCount = CharCountOfLargestOption(
356 static_cast<nsComboboxControlFrame*>(kid)->GetDropDown());
357 mTextAmount += charCount * fontSize;
358 } else if (fType == LayoutFrameType::ListControl) {
359 // See textInputFrame above (with s/amount of text/selected option/).
360 nscoord fontSize = kid->StyleFont()->mFont.size.ToAppUnits();
361 int32_t charCount = CharCountOfLargestOption(kid);
362 mTextAmount += charCount * fontSize;
363 } else {
364 // recursive step
365 ScanTextIn(kid);
366 }
367
368 if (mTextAmount >= mTextThreshold) {
369 return;
370 }
371 }
372 }
373 }
374