diff options
Diffstat (limited to 'layout/generic/nsFontInflationData.cpp')
-rw-r--r-- | layout/generic/nsFontInflationData.cpp | 373 |
1 files changed, 373 insertions, 0 deletions
diff --git a/layout/generic/nsFontInflationData.cpp b/layout/generic/nsFontInflationData.cpp new file mode 100644 index 0000000000..b510a62b0c --- /dev/null +++ b/layout/generic/nsFontInflationData.cpp @@ -0,0 +1,373 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/dom/Text.h" // for inline nsINode::AsText() definition +#include "mozilla/PresShell.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->HasAnyStateBits(NS_FRAME_FONT_INFLATION_FLOW_ROOT), + "should have found a flow root"); + MOZ_ASSERT(aFrame->GetWritingMode().IsVertical() == + bfc->GetWritingMode().IsVertical(), + "current writing mode should match that of our flow root"); + + return bfc->GetProperty(FontInflationDataProperty()); +} + +/* static */ +bool nsFontInflationData::UpdateFontInflationDataISizeFor( + const ReflowInput& aReflowInput) { + nsIFrame* bfc = aReflowInput.mFrame; + NS_ASSERTION(bfc->HasAnyStateBits(NS_FRAME_FONT_INFLATION_FLOW_ROOT), + "should have been given a flow root"); + nsFontInflationData* data = bfc->GetProperty(FontInflationDataProperty()); + bool oldInflationEnabled; + nscoord oldUsableISize; + if (data) { + oldUsableISize = data->mUsableISize; + oldInflationEnabled = data->mInflationEnabled; + } else { + data = new nsFontInflationData(bfc); + bfc->SetProperty(FontInflationDataProperty(), data); + oldUsableISize = -1; + oldInflationEnabled = true; /* not relevant */ + } + + data->UpdateISize(aReflowInput); + + if (oldInflationEnabled != data->mInflationEnabled) return true; + + return oldInflationEnabled && oldUsableISize != data->mUsableISize; +} + +/* static */ +void nsFontInflationData::MarkFontInflationDataTextDirty(nsIFrame* aBFCFrame) { + NS_ASSERTION(aBFCFrame->HasAnyStateBits(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), + mUsableISize(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<nsIFrame*, 32> 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<nsIFrame*, 16> 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<ReflowInput*>(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->HasAnyStateBits(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/StaticPrefList.yaml . + PresShell* presShell = bfc->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; + } + + // Font inflation increases the font size for a given flow root so that the + // text is legible when we've zoomed such that the respective nearest common + // ancestor's (NCA) full inline-size (ISize) fills the screen. We assume how- + // ever that we don't want to zoom out further than the root iframe's ISize + // (i.e. the viewport for a top-level document, or the containing iframe + // otherwise), since in some cases zooming out further might not even be + // possible or make sense. + // Hence the ISize assumed to be usable for displaying text is limited to the + // visible area. + nsPresContext* presContext = bfc->PresContext(); + MOZ_ASSERT( + bfc->GetWritingMode().IsVertical() == nca->GetWritingMode().IsVertical(), + "writing mode of NCA should match that of its flow root"); + nscoord iFrameISize = bfc->GetWritingMode().IsVertical() + ? presContext->GetVisibleArea().height + : presContext->GetVisibleArea().width; + mUsableISize = std::min(iFrameISize, 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<FrameChildList, 4> 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->HasAnyStateBits(NS_FRAME_FONT_INFLATION_FLOW_ROOT)) { + // Goes in a different set of inflation data. + continue; + } + + if (kid->IsTextFrame()) { + nsIContent* content = kid->GetContent(); + if (content && kid == content->GetPrimaryFrame()) { + uint32_t len = nsTextFrameUtils:: + ComputeApproximateLengthWithWhitespaceCompression( + content->AsText(), 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->IsTextFrame()) { + optionResult += nsTextFrameUtils:: + ComputeApproximateLengthWithWhitespaceCompression( + optionChild->GetContent()->AsText(), + optionChild->StyleText()); + } + } + } + if (optionResult > result) { + result = optionResult; + } + } + return result; +} + +static uint32_t CharCountOfLargestOption(nsIFrame* aListControlFrame) { + return DoCharCountOfLargestOption( + static_cast<nsListControlFrame*>(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! + + for (const auto& childList : aFrame->ChildLists()) { + for (nsIFrame* kid : childList.mList) { + if (kid->HasAnyStateBits(NS_FRAME_FONT_INFLATION_FLOW_ROOT)) { + // Goes in a different set of inflation data. + continue; + } + + LayoutFrameType fType = kid->Type(); + if (fType == LayoutFrameType::Text) { + nsIContent* content = kid->GetContent(); + if (content && kid == content->GetPrimaryFrame()) { + uint32_t len = nsTextFrameUtils:: + ComputeApproximateLengthWithWhitespaceCompression( + content->AsText(), kid->StyleText()); + if (len != 0) { + nscoord fontSize = kid->StyleFont()->mFont.size.ToAppUnits(); + if (fontSize > 0) { + mTextAmount += fontSize * len; + } + } + } + } else if (fType == LayoutFrameType::TextInput) { + // 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.ToAppUnits(); + int32_t charCount = static_cast<nsTextControlFrame*>(kid)->GetCols(); + mTextAmount += charCount * fontSize; + } else if (fType == LayoutFrameType::ComboboxControl) { + // 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.ToAppUnits(); + int32_t charCount = static_cast<nsComboboxControlFrame*>(kid) + ->CharCountOfLargestOptionForInflation(); + mTextAmount += charCount * fontSize; + } else if (fType == LayoutFrameType::ListControl) { + // See textInputFrame above (with s/amount of text/selected option/). + nscoord fontSize = kid->StyleFont()->mFont.size.ToAppUnits(); + int32_t charCount = CharCountOfLargestOption(kid); + mTextAmount += charCount * fontSize; + } else { + // recursive step + ScanTextIn(kid); + } + + if (mTextAmount >= mTextThreshold) { + return; + } + } + } +} |