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