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