Backed out changeset 7f86a5d0334d (bug 1853821) for causing xpcshell failures on...
[gecko.git] / layout / generic / nsFontInflationData.cpp
blobdaa61d91f226bf072fcb6b69819056a4a89bff96
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/. */
7 /* Per-block-formatting-context manager of font size inflation for pan and zoom
8 * UI. */
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"
20 using namespace mozilla;
21 using namespace mozilla::layout;
23 NS_DECLARE_FRAME_PROPERTY_DELETABLE(FontInflationDataProperty,
24 nsFontInflationData)
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");
36 return bfc->GetProperty(FontInflationDataProperty());
39 /* static */
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 */
58 data->UpdateISize(aReflowInput);
60 if (oldInflationEnabled != data->mInflationEnabled) return true;
62 return oldInflationEnabled && oldUsableISize != data->mUsableISize;
65 /* static */
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");
70 nsFontInflationData* data =
71 aBFCFrame->GetProperty(FontInflationDataProperty());
72 if (data) {
73 data->MarkTextDirty();
77 nsFontInflationData::nsFontInflationData(nsIFrame* aBFCFrame)
78 : mBFCFrame(aBFCFrame),
79 mUsableISize(0),
80 mTextAmount(0),
81 mTextThreshold(0),
82 mInflationEnabled(false),
83 mTextDirty(true) {}
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).
90 * aKnownCommonAncestor is a known common ancestor of both.
92 static nsIFrame* NearestCommonAncestorFirstInFlow(
93 nsIFrame* aFrame1, nsIFrame* aFrame2, nsIFrame* aKnownCommonAncestor) {
94 aFrame1 = aFrame1->FirstInFlow();
95 aFrame2 = aFrame2->FirstInFlow();
96 aKnownCommonAncestor = aKnownCommonAncestor->FirstInFlow();
98 AutoTArray<nsIFrame*, 32> ancestors1, ancestors2;
99 for (nsIFrame* f = aFrame1; f != aKnownCommonAncestor;
100 (f = f->GetParent()) && (f = f->FirstInFlow())) {
101 ancestors1.AppendElement(f);
103 for (nsIFrame* f = aFrame2; f != aKnownCommonAncestor;
104 (f = f->GetParent()) && (f = f->FirstInFlow())) {
105 ancestors2.AppendElement(f);
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;
114 result = ancestors1[i1];
117 return result;
120 static nscoord ComputeDescendantISize(const ReflowInput& aAncestorReflowInput,
121 nsIFrame* aDescendantFrame) {
122 nsIFrame* ancestorFrame = aAncestorReflowInput.mFrame->FirstInFlow();
123 if (aDescendantFrame == ancestorFrame) {
124 return aAncestorReflowInput.ComputedISize();
127 AutoTArray<nsIFrame*, 16> frames;
128 for (nsIFrame* f = aDescendantFrame; f != ancestorFrame;
129 f = f->GetParent()->FirstInFlow()) {
130 frames.AppendElement(f);
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.
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);
156 MOZ_ASSERT(reflowInputs[len - 1].mFrame == aDescendantFrame,
157 "bad logic in this function");
158 nscoord result = reflowInputs[len - 1].ComputedISize();
160 for (uint32_t i = len; i-- != 0;) {
161 reflowInputs[i].~ReflowInput();
163 free(reflowInputs);
165 return result;
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");
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;
182 nsIFrame* lastInflatableDescendant = FindEdgeInflatableFrameIn(bfc, eFromEnd);
183 MOZ_ASSERT(!firstInflatableDescendant == !lastInflatableDescendant,
184 "null-ness should match; NearestCommonAncestorFirstInFlow"
185 " will crash when passed null");
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();
195 nscoord newNCAISize = ComputeDescendantISize(aReflowInput, nca);
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;
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;
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;
230 /* static */ nsIFrame* nsFontInflationData::FindEdgeInflatableFrameIn(
231 nsIFrame* aFrame, SearchDirection aDirection) {
232 // NOTE: This function has a similar structure to ScanTextIn!
234 // FIXME: Should probably only scan the text that's actually going to
235 // be inflated!
237 nsIFormControlFrame* fcf = do_QueryFrame(aFrame);
238 if (fcf) {
239 return aFrame;
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;
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;
267 } else {
268 nsIFrame* kidResult = FindEdgeInflatableFrameIn(kid, aDirection);
269 if (kidResult) {
270 return kidResult;
276 return nullptr;
279 void nsFontInflationData::ScanText() {
280 mTextDirty = false;
281 mTextAmount = 0;
282 ScanTextIn(mBFCFrame);
283 mInflationEnabled = mTextAmount >= mTextThreshold;
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());
304 if (optionResult > result) {
305 result = optionResult;
308 return result;
311 static uint32_t CharCountOfLargestOption(nsIFrame* aListControlFrame) {
312 return DoCharCountOfLargestOption(
313 static_cast<nsListControlFrame*>(aListControlFrame)
314 ->GetOptionsContainer());
317 void nsFontInflationData::ScanTextIn(nsIFrame* aFrame) {
318 // NOTE: This function has a similar structure to FindEdgeInflatableFrameIn!
320 // FIXME: Should probably only scan the text that's actually going to
321 // be inflated!
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;
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;
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 =
349 static_cast<nsTextControlFrame*>(kid)->GetColsOrDefault();
350 mTextAmount += charCount * fontSize;
351 } else if (fType == LayoutFrameType::ComboboxControl) {
352 // See textInputFrame above (with s/amount of text/selected option/).
353 // Don't just recurse down to the list control inside, since we
354 // need to exclude the display frame.
355 nscoord fontSize = kid->StyleFont()->mFont.size.ToAppUnits();
356 int32_t charCount = static_cast<nsComboboxControlFrame*>(kid)
357 ->CharCountOfLargestOptionForInflation();
358 mTextAmount += charCount * fontSize;
359 } else if (fType == LayoutFrameType::ListControl) {
360 // See textInputFrame above (with s/amount of text/selected option/).
361 nscoord fontSize = kid->StyleFont()->mFont.size.ToAppUnits();
362 int32_t charCount = CharCountOfLargestOption(kid);
363 mTextAmount += charCount * fontSize;
364 } else {
365 // recursive step
366 ScanTextIn(kid);
369 if (mTextAmount >= mTextThreshold) {
370 return;