layout/generic/nsFontInflationData.cpp
author Brian Hackett <bhackett1024@gmail.com>
Wed, 14 Nov 2018 16:09:58 -1000
changeset 446931 1c7fc8389e012c987347efefca6b35f3948b742a
parent 439187 57e3c8d09acea5e0bff83ddbb78d385f71dd352d
child 448947 6f3709b3878117466168c40affa7bca0b60cf75b
permissions -rw-r--r--
Bug 1507359 Part 2 - Bindings and internal changes to allow ReplayDebugger to control child pausing/resuming, r=mccr8.

/* -*- 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/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");
  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->GetStateBits() & 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->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)
  , 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->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->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->GetStateBits() & 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, 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(), 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!

  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;
      }

      LayoutFrameType fType = kid->Type();
      if (fType == LayoutFrameType::Text) {
        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 == 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;
        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;
        int32_t charCount = CharCountOfLargestOption(
          static_cast<nsComboboxControlFrame*>(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;
        int32_t charCount = CharCountOfLargestOption(kid);
        mTextAmount += charCount * fontSize;
      } else {
        // recursive step
        ScanTextIn(kid);
      }

      if (mTextAmount >= mTextThreshold) {
        return;
      }
    }
  }
}