/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* Per-block-formatting-context manager of font size inflation for pan and zoom UI. */ #include "nsFontInflationData.h" #include "FrameProperties.h" #include "nsTextControlFrame.h" #include "nsListControlFrame.h" #include "nsComboboxControlFrame.h" #include "mozilla/ReflowInput.h" #include "nsTextFrameUtils.h" using namespace mozilla; using namespace mozilla::layout; NS_DECLARE_FRAME_PROPERTY_DELETABLE(FontInflationDataProperty, nsFontInflationData) /* static */ nsFontInflationData* nsFontInflationData::FindFontInflationDataFor(const nsIFrame *aFrame) { // We have one set of font inflation data per block formatting context. const nsIFrame *bfc = FlowRootFor(aFrame); NS_ASSERTION(bfc->GetStateBits() & NS_FRAME_FONT_INFLATION_FLOW_ROOT, "should have found a flow root"); return bfc->GetProperty(FontInflationDataProperty()); } /* static */ bool nsFontInflationData::UpdateFontInflationDataISizeFor(const ReflowInput& aReflowInput) { nsIFrame *bfc = aReflowInput.mFrame; NS_ASSERTION(bfc->GetStateBits() & NS_FRAME_FONT_INFLATION_FLOW_ROOT, "should have been given a flow root"); nsFontInflationData *data = bfc->GetProperty(FontInflationDataProperty()); bool oldInflationEnabled; nscoord oldNCAISize; if (data) { oldNCAISize = data->mNCAISize; oldInflationEnabled = data->mInflationEnabled; } else { data = new nsFontInflationData(bfc); bfc->SetProperty(FontInflationDataProperty(), data); oldNCAISize = -1; oldInflationEnabled = true; /* not relevant */ } data->UpdateISize(aReflowInput); if (oldInflationEnabled != data->mInflationEnabled) return true; return oldInflationEnabled && oldNCAISize != data->mNCAISize; } /* static */ void nsFontInflationData::MarkFontInflationDataTextDirty(nsIFrame *aBFCFrame) { NS_ASSERTION(aBFCFrame->GetStateBits() & NS_FRAME_FONT_INFLATION_FLOW_ROOT, "should have been given a flow root"); nsFontInflationData *data = aBFCFrame->GetProperty(FontInflationDataProperty()); if (data) { data->MarkTextDirty(); } } nsFontInflationData::nsFontInflationData(nsIFrame *aBFCFrame) : mBFCFrame(aBFCFrame) , mNCAISize(0) , mTextAmount(0) , mTextThreshold(0) , mInflationEnabled(false) , mTextDirty(true) { } /** * Find the closest common ancestor between aFrame1 and aFrame2, except * treating the parent of a frame as the first-in-flow of its parent (so * the result doesn't change when breaking changes). * * aKnownCommonAncestor is a known common ancestor of both. */ static nsIFrame* NearestCommonAncestorFirstInFlow(nsIFrame *aFrame1, nsIFrame *aFrame2, nsIFrame *aKnownCommonAncestor) { aFrame1 = aFrame1->FirstInFlow(); aFrame2 = aFrame2->FirstInFlow(); aKnownCommonAncestor = aKnownCommonAncestor->FirstInFlow(); AutoTArray ancestors1, ancestors2; for (nsIFrame *f = aFrame1; f != aKnownCommonAncestor; (f = f->GetParent()) && (f = f->FirstInFlow())) { ancestors1.AppendElement(f); } for (nsIFrame *f = aFrame2; f != aKnownCommonAncestor; (f = f->GetParent()) && (f = f->FirstInFlow())) { ancestors2.AppendElement(f); } nsIFrame *result = aKnownCommonAncestor; uint32_t i1 = ancestors1.Length(), i2 = ancestors2.Length(); while (i1-- != 0 && i2-- != 0) { if (ancestors1[i1] != ancestors2[i2]) { break; } result = ancestors1[i1]; } return result; } static nscoord ComputeDescendantISize(const ReflowInput& aAncestorReflowInput, nsIFrame *aDescendantFrame) { nsIFrame *ancestorFrame = aAncestorReflowInput.mFrame->FirstInFlow(); if (aDescendantFrame == ancestorFrame) { return aAncestorReflowInput.ComputedISize(); } AutoTArray frames; for (nsIFrame *f = aDescendantFrame; f != ancestorFrame; f = f->GetParent()->FirstInFlow()) { frames.AppendElement(f); } // This ignores the inline-size contributions made by scrollbars, though in // reality we don't have any scrollbars on the sorts of devices on // which we use font inflation, so it's not a problem. But it may // occasionally cause problems when writing tests on desktop. uint32_t len = frames.Length(); ReflowInput *reflowInputs = static_cast (moz_xmalloc(sizeof(ReflowInput) * len)); nsPresContext *presContext = aDescendantFrame->PresContext(); for (uint32_t i = 0; i < len; ++i) { const ReflowInput &parentReflowInput = (i == 0) ? aAncestorReflowInput : reflowInputs[i - 1]; nsIFrame *frame = frames[len - i - 1]; WritingMode wm = frame->GetWritingMode(); LogicalSize availSize = parentReflowInput.ComputedSize(wm); availSize.BSize(wm) = NS_UNCONSTRAINEDSIZE; MOZ_ASSERT(frame->GetParent()->FirstInFlow() == parentReflowInput.mFrame->FirstInFlow(), "bad logic in this function"); new (reflowInputs + i) ReflowInput(presContext, parentReflowInput, frame, availSize); } MOZ_ASSERT(reflowInputs[len - 1].mFrame == aDescendantFrame, "bad logic in this function"); nscoord result = reflowInputs[len - 1].ComputedISize(); for (uint32_t i = len; i-- != 0; ) { reflowInputs[i].~ReflowInput(); } free(reflowInputs); return result; } void nsFontInflationData::UpdateISize(const ReflowInput &aReflowInput) { nsIFrame *bfc = aReflowInput.mFrame; NS_ASSERTION(bfc->GetStateBits() & NS_FRAME_FONT_INFLATION_FLOW_ROOT, "must be block formatting context"); nsIFrame *firstInflatableDescendant = FindEdgeInflatableFrameIn(bfc, eFromStart); if (!firstInflatableDescendant) { mTextAmount = 0; mTextThreshold = 0; // doesn't matter mTextDirty = false; mInflationEnabled = false; return; } nsIFrame *lastInflatableDescendant = FindEdgeInflatableFrameIn(bfc, eFromEnd); MOZ_ASSERT(!firstInflatableDescendant == !lastInflatableDescendant, "null-ness should match; NearestCommonAncestorFirstInFlow" " will crash when passed null"); // Particularly when we're computing for the root BFC, the inline-size of // nca might differ significantly for the inline-size of bfc. nsIFrame *nca = NearestCommonAncestorFirstInFlow(firstInflatableDescendant, lastInflatableDescendant, bfc); while (!nca->IsContainerForFontSizeInflation()) { nca = nca->GetParent()->FirstInFlow(); } nscoord newNCAISize = ComputeDescendantISize(aReflowInput, nca); // See comment above "font.size.inflation.lineThreshold" in // modules/libpref/src/init/all.js . nsIPresShell* presShell = bfc->PresContext()->PresShell(); uint32_t lineThreshold = presShell->FontSizeInflationLineThreshold(); nscoord newTextThreshold = (newNCAISize * lineThreshold) / 100; if (mTextThreshold <= mTextAmount && mTextAmount < newTextThreshold) { // Because we truncate our scan when we hit sufficient text, we now // need to rescan. mTextDirty = true; } mNCAISize = newNCAISize; mTextThreshold = newTextThreshold; mInflationEnabled = mTextAmount >= mTextThreshold; } /* static */ nsIFrame* nsFontInflationData::FindEdgeInflatableFrameIn(nsIFrame* aFrame, SearchDirection aDirection) { // NOTE: This function has a similar structure to ScanTextIn! // FIXME: Should probably only scan the text that's actually going to // be inflated! nsIFormControlFrame* fcf = do_QueryFrame(aFrame); if (fcf) { return aFrame; } // FIXME: aDirection! AutoTArray lists; aFrame->GetChildLists(&lists); for (uint32_t i = 0, len = lists.Length(); i < len; ++i) { const nsFrameList& list = lists[(aDirection == eFromStart) ? i : len - i - 1].mList; for (nsIFrame *kid = (aDirection == eFromStart) ? list.FirstChild() : list.LastChild(); kid; kid = (aDirection == eFromStart) ? kid->GetNextSibling() : kid->GetPrevSibling()) { if (kid->GetStateBits() & NS_FRAME_FONT_INFLATION_FLOW_ROOT) { // Goes in a different set of inflation data. continue; } if (kid->GetType() == nsGkAtoms::textFrame) { nsIContent *content = kid->GetContent(); if (content && kid == content->GetPrimaryFrame()) { uint32_t len = nsTextFrameUtils:: ComputeApproximateLengthWithWhitespaceCompression( content, kid->StyleText()); if (len != 0) { return kid; } } } else { nsIFrame *kidResult = FindEdgeInflatableFrameIn(kid, aDirection); if (kidResult) { return kidResult; } } } } return nullptr; } void nsFontInflationData::ScanText() { mTextDirty = false; mTextAmount = 0; ScanTextIn(mBFCFrame); mInflationEnabled = mTextAmount >= mTextThreshold; } static uint32_t DoCharCountOfLargestOption(nsIFrame *aContainer) { uint32_t result = 0; for (nsIFrame* option : aContainer->PrincipalChildList()) { uint32_t optionResult; if (option->GetContent()->IsHTMLElement(nsGkAtoms::optgroup)) { optionResult = DoCharCountOfLargestOption(option); } else { // REVIEW: Check the frame structure for this! optionResult = 0; for (nsIFrame* optionChild : option->PrincipalChildList()) { if (optionChild->GetType() == nsGkAtoms::textFrame) { optionResult += nsTextFrameUtils:: ComputeApproximateLengthWithWhitespaceCompression( optionChild->GetContent(), optionChild->StyleText()); } } } if (optionResult > result) { result = optionResult; } } return result; } static uint32_t CharCountOfLargestOption(nsIFrame *aListControlFrame) { return DoCharCountOfLargestOption( static_cast(aListControlFrame)->GetOptionsContainer()); } void nsFontInflationData::ScanTextIn(nsIFrame *aFrame) { // NOTE: This function has a similar structure to FindEdgeInflatableFrameIn! // FIXME: Should probably only scan the text that's actually going to // be inflated! nsIFrame::ChildListIterator lists(aFrame); for (; !lists.IsDone(); lists.Next()) { nsFrameList::Enumerator kids(lists.CurrentList()); for (; !kids.AtEnd(); kids.Next()) { nsIFrame *kid = kids.get(); if (kid->GetStateBits() & NS_FRAME_FONT_INFLATION_FLOW_ROOT) { // Goes in a different set of inflation data. continue; } nsIAtom *fType = kid->GetType(); if (fType == nsGkAtoms::textFrame) { nsIContent *content = kid->GetContent(); if (content && kid == content->GetPrimaryFrame()) { uint32_t len = nsTextFrameUtils:: ComputeApproximateLengthWithWhitespaceCompression( content, kid->StyleText()); if (len != 0) { nscoord fontSize = kid->StyleFont()->mFont.size; if (fontSize > 0) { mTextAmount += fontSize * len; } } } } else if (fType == nsGkAtoms::textInputFrame) { // We don't want changes to the amount of text in a text input // to change what we count towards inflation. nscoord fontSize = kid->StyleFont()->mFont.size; int32_t charCount = static_cast(kid)->GetCols(); mTextAmount += charCount * fontSize; } else if (fType == nsGkAtoms::comboboxControlFrame) { // See textInputFrame above (with s/amount of text/selected option/). // Don't just recurse down to the list control inside, since we // need to exclude the display frame. nscoord fontSize = kid->StyleFont()->mFont.size; int32_t charCount = CharCountOfLargestOption( static_cast(kid)->GetDropDown()); mTextAmount += charCount * fontSize; } else if (fType == nsGkAtoms::listControlFrame) { // See textInputFrame above (with s/amount of text/selected option/). nscoord fontSize = kid->StyleFont()->mFont.size; int32_t charCount = CharCountOfLargestOption(kid); mTextAmount += charCount * fontSize; } else { // recursive step ScanTextIn(kid); } if (mTextAmount >= mTextThreshold) { return; } } } }