/* -*- 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 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->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 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(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(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 = CharCountOfLargestOption( static_cast(kid)->GetDropDown()); 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; } } } }