mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-24 21:31:04 +00:00
ac5b595f91
Differential Revision: https://phabricator.services.mozilla.com/D81201
374 lines
14 KiB
C++
374 lines
14 KiB
C++
/* -*- 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 = 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.ToAppUnits();
|
|
int32_t charCount = CharCountOfLargestOption(kid);
|
|
mTextAmount += charCount * fontSize;
|
|
} else {
|
|
// recursive step
|
|
ScanTextIn(kid);
|
|
}
|
|
|
|
if (mTextAmount >= mTextThreshold) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|