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