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