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