blob: 29584c4447567251b175e8525a5f7f0bc7e38ef4 [file] [log] [blame]
/*
* Copyright (C) 2013 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "core/layout/TextAutosizer.h"
#include "core/dom/Document.h"
#include "core/frame/FrameHost.h"
#include "core/frame/FrameView.h"
#include "core/frame/LocalFrame.h"
#include "core/frame/Settings.h"
#include "core/frame/VisualViewport.h"
#include "core/html/HTMLTextAreaElement.h"
#include "core/layout/LayoutBlock.h"
#include "core/layout/LayoutInline.h"
#include "core/layout/LayoutListItem.h"
#include "core/layout/LayoutListMarker.h"
#include "core/layout/LayoutTable.h"
#include "core/layout/LayoutTableCell.h"
#include "core/layout/LayoutView.h"
#include "core/layout/api/LayoutAPIShim.h"
#include "core/layout/api/LayoutViewItem.h"
#include "core/page/Page.h"
#include "wtf/PtrUtil.h"
#include <memory>
#ifdef AUTOSIZING_DOM_DEBUG_INFO
#include "core/dom/ExecutionContextTask.h"
#endif
namespace blink {
#ifdef AUTOSIZING_DOM_DEBUG_INFO
class WriteDebugInfoTask : public ExecutionContextTask {
public:
WriteDebugInfoTask(Element* element, AtomicString value)
: m_element(element)
, m_value(value)
{
}
virtual void performTask(ExecutionContext*)
{
m_element->setAttribute("data-autosizing", m_value, ASSERT_NO_EXCEPTION);
}
private:
Persistent<Element> m_element;
AtomicString m_value;
};
static void writeDebugInfo(LayoutObject* layoutObject, const AtomicString& output)
{
Node* node = layoutObject->node();
if (!node)
return;
if (node->isDocumentNode())
node = toDocument(node)->documentElement();
if (!node->isElementNode())
return;
node->document().postTask(BLINK_FROM_HERE, wrapUnique(new WriteDebugInfoTask(toElement(node), output)));
}
void TextAutosizer::writeClusterDebugInfo(Cluster* cluster)
{
String explanation = "";
if (cluster->m_flags & SUPPRESSING) {
explanation = "[suppressed]";
} else if (!(cluster->m_flags & (INDEPENDENT | WIDER_OR_NARROWER))) {
explanation = "[inherited]";
} else if (cluster->m_supercluster) {
explanation = "[supercluster]";
} else if (!clusterHasEnoughTextToAutosize(cluster)) {
explanation = "[insufficient-text]";
} else {
const LayoutBlock* widthProvider = clusterWidthProvider(cluster->m_root);
if (cluster->m_hasTableAncestor && cluster->m_multiplier < multiplierFromBlock(widthProvider)) {
explanation = "[table-ancestor-limited]";
} else {
explanation = String::format("[from width %d of %s]",
static_cast<int>(widthFromBlock(widthProvider)), widthProvider->debugName().utf8().data());
}
}
String pageInfo = "";
if (cluster->m_root->isLayoutView()) {
pageInfo = String::format("; pageinfo: afsf %f * dsa %f * (lw %d / fw %d)",
m_pageInfo.m_accessibilityFontScaleFactor, m_pageInfo.m_deviceScaleAdjustment, m_pageInfo.m_layoutWidth, m_pageInfo.m_frameWidth);
}
float multiplier = cluster->m_flags & SUPPRESSING ? 1.0 : cluster->m_multiplier;
writeDebugInfo(const_cast<LayoutBlock*>(cluster->m_root),
AtomicString(String::format("cluster: %f %s%s", multiplier,
explanation.utf8().data(), pageInfo.utf8().data())));
}
#endif
static const LayoutObject* parentElementLayoutObject(const LayoutObject* layoutObject)
{
// At style recalc, the layoutObject's parent may not be attached,
// so we need to obtain this from the DOM tree.
const Node* node = layoutObject->node();
if (!node)
return nullptr;
// FIXME: This should be using LayoutTreeBuilderTraversal::parent().
if (Element* parent = node->parentElement())
return parent->layoutObject();
return nullptr;
}
static bool isNonTextAreaFormControl(const LayoutObject* layoutObject)
{
const Node* node = layoutObject ? layoutObject->node() : nullptr;
if (!node || !node->isElementNode())
return false;
const Element* element = toElement(node);
return (element->isFormControlElement() && !isHTMLTextAreaElement(element));
}
static bool isPotentialClusterRoot(const LayoutObject* layoutObject)
{
// "Potential cluster roots" are the smallest unit for which we can
// enable/disable text autosizing.
// - Must have children.
// An exception is made for LayoutView which should create a root to
// maintain consistency with documents that have no child nodes but may
// still have LayoutObject children.
// - Must not be inline, as different multipliers on one line looks terrible.
// Exceptions are inline-block and alike elements (inline-table, -webkit-inline-*),
// as they often contain entire multi-line columns of text.
// - Must not be normal list items, as items in the same list should look
// consistent, unless they are floating or position:absolute/fixed.
Node* node = layoutObject->generatingNode();
if (node && !node->hasChildren() && !layoutObject->isLayoutView())
return false;
if (!layoutObject->isLayoutBlock())
return false;
if (layoutObject->isInline() && !layoutObject->style()->isDisplayReplacedType())
return false;
if (layoutObject->isListItem())
return (layoutObject->isFloating() || layoutObject->isOutOfFlowPositioned());
return true;
}
static bool isIndependentDescendant(const LayoutBlock* layoutObject)
{
ASSERT(isPotentialClusterRoot(layoutObject));
LayoutBlock* containingBlock = layoutObject->containingBlock();
return layoutObject->isLayoutView()
|| layoutObject->isFloating()
|| layoutObject->isOutOfFlowPositioned()
|| layoutObject->isTableCell()
|| layoutObject->isTableCaption()
|| layoutObject->isFlexibleBoxIncludingDeprecated()
|| (containingBlock && containingBlock->isHorizontalWritingMode() != layoutObject->isHorizontalWritingMode())
|| layoutObject->style()->isDisplayReplacedType()
|| layoutObject->isTextArea()
|| layoutObject->style()->userModify() != READ_ONLY;
}
static bool blockIsRowOfLinks(const LayoutBlock* block)
{
// A "row of links" is a block for which:
// 1. It does not contain non-link text elements longer than 3 characters
// 2. It contains a minimum of 3 inline links and all links should
// have the same specified font size.
// 3. It should not contain <br> elements.
// 4. It should contain only inline elements unless they are containers,
// children of link elements or children of sub-containers.
int linkCount = 0;
LayoutObject* layoutObject = block->firstChild();
float matchingFontSize = -1;
while (layoutObject) {
if (!isPotentialClusterRoot(layoutObject)) {
if (layoutObject->isText() && toLayoutText(layoutObject)->text().stripWhiteSpace().length() > 3)
return false;
if (!layoutObject->isInline() || layoutObject->isBR())
return false;
}
if (layoutObject->style()->isLink()) {
linkCount++;
if (matchingFontSize < 0)
matchingFontSize = layoutObject->style()->specifiedFontSize();
else if (matchingFontSize != layoutObject->style()->specifiedFontSize())
return false;
// Skip traversing descendants of the link.
layoutObject = layoutObject->nextInPreOrderAfterChildren(block);
continue;
}
layoutObject = layoutObject->nextInPreOrder(block);
}
return (linkCount >= 3);
}
static bool blockHeightConstrained(const LayoutBlock* block)
{
// FIXME: Propagate constrainedness down the tree, to avoid inefficiently walking back up from each box.
// FIXME: This code needs to take into account vertical writing modes.
// FIXME: Consider additional heuristics, such as ignoring fixed heights if the content is already overflowing before autosizing kicks in.
for (; block; block = block->containingBlock()) {
const ComputedStyle& style = block->styleRef();
if (style.overflowY() >= OverflowScroll)
return false;
if (style.height().isSpecified() || style.maxHeight().isSpecified() || block->isOutOfFlowPositioned()) {
// Some sites (e.g. wikipedia) set their html and/or body elements to height:100%,
// without intending to constrain the height of the content within them.
return !block->isDocumentElement() && !block->isBody() && !block->isLayoutView();
}
if (block->isFloating())
return false;
}
return false;
}
static bool blockOrImmediateChildrenAreFormControls(const LayoutBlock* block)
{
if (isNonTextAreaFormControl(block))
return true;
const LayoutObject* layoutObject = block->firstChild();
while (layoutObject) {
if (isNonTextAreaFormControl(layoutObject))
return true;
layoutObject = layoutObject->nextSibling();
}
return false;
}
// Some blocks are not autosized even if their parent cluster wants them to.
static bool blockSuppressesAutosizing(const LayoutBlock* block)
{
if (blockOrImmediateChildrenAreFormControls(block))
return true;
if (blockIsRowOfLinks(block))
return true;
// Don't autosize block-level text that can't wrap (as it's likely to
// expand sideways and break the page's layout).
if (!block->style()->autoWrap())
return true;
if (blockHeightConstrained(block))
return true;
return false;
}
static bool hasExplicitWidth(const LayoutBlock* block)
{
// FIXME: This heuristic may need to be expanded to other ways a block can be wider or narrower
// than its parent containing block.
return block->style() && block->style()->width().isSpecified();
}
TextAutosizer::TextAutosizer(const Document* document)
: m_document(document)
, m_firstBlockToBeginLayout(nullptr)
#if ENABLE(ASSERT)
, m_blocksThatHaveBegunLayout()
#endif
, m_superclusters()
, m_clusterStack()
, m_fingerprintMapper()
, m_pageInfo()
, m_updatePageInfoDeferred(false)
{
}
TextAutosizer::~TextAutosizer()
{
}
void TextAutosizer::record(const LayoutBlock* block)
{
if (!m_pageInfo.m_settingEnabled)
return;
ASSERT(!m_blocksThatHaveBegunLayout.contains(block));
if (!classifyBlock(block, INDEPENDENT | EXPLICIT_WIDTH))
return;
if (Fingerprint fingerprint = computeFingerprint(block))
m_fingerprintMapper.addTentativeClusterRoot(block, fingerprint);
}
void TextAutosizer::destroy(const LayoutBlock* block)
{
if (!m_pageInfo.m_settingEnabled && !m_fingerprintMapper.hasFingerprints())
return;
ASSERT(!m_blocksThatHaveBegunLayout.contains(block));
if (m_fingerprintMapper.remove(block) && m_firstBlockToBeginLayout) {
// LayoutBlock with a fingerprint was destroyed during layout.
// Clear the cluster stack and the supercluster map to avoid stale pointers.
// Speculative fix for http://crbug.com/369485.
m_firstBlockToBeginLayout = nullptr;
m_clusterStack.clear();
m_superclusters.clear();
}
}
TextAutosizer::BeginLayoutBehavior TextAutosizer::prepareForLayout(const LayoutBlock* block)
{
#if ENABLE(ASSERT)
m_blocksThatHaveBegunLayout.add(block);
#endif
if (!m_firstBlockToBeginLayout) {
m_firstBlockToBeginLayout = block;
prepareClusterStack(block->parent());
} else if (block == currentCluster()->m_root) {
// Ignore beginLayout on the same block twice.
// This can happen with paginated overflow.
return StopLayout;
}
return ContinueLayout;
}
void TextAutosizer::prepareClusterStack(const LayoutObject* layoutObject)
{
if (!layoutObject)
return;
prepareClusterStack(layoutObject->parent());
if (layoutObject->isLayoutBlock()) {
const LayoutBlock* block = toLayoutBlock(layoutObject);
#if ENABLE(ASSERT)
m_blocksThatHaveBegunLayout.add(block);
#endif
if (Cluster* cluster = maybeCreateCluster(block))
m_clusterStack.append(wrapUnique(cluster));
}
}
void TextAutosizer::beginLayout(LayoutBlock* block, SubtreeLayoutScope* layouter)
{
ASSERT(shouldHandleLayout());
if (prepareForLayout(block) == StopLayout)
return;
ASSERT(!m_clusterStack.isEmpty() || block->isLayoutView());
if (Cluster* cluster = maybeCreateCluster(block))
m_clusterStack.append(wrapUnique(cluster));
ASSERT(!m_clusterStack.isEmpty());
// Cells in auto-layout tables are handled separately by inflateAutoTable.
bool isAutoTableCell = block->isTableCell() && !toLayoutTableCell(block)->table()->style()->isFixedTableLayout();
if (!isAutoTableCell && !m_clusterStack.isEmpty())
inflate(block, layouter);
}
void TextAutosizer::inflateAutoTable(LayoutTable* table)
{
ASSERT(table);
ASSERT(!table->style()->isFixedTableLayout());
ASSERT(table->containingBlock());
Cluster* cluster = currentCluster();
if (cluster->m_root != table)
return;
// Pre-inflate cells that have enough text so that their inflated preferred widths will be used
// for column sizing.
for (LayoutObject* section = table->firstChild(); section; section = section->nextSibling()) {
if (!section->isTableSection())
continue;
for (LayoutTableRow* row = toLayoutTableSection(section)->firstRow(); row; row = row->nextRow()) {
for (LayoutTableCell* cell = row->firstCell(); cell; cell = cell->nextCell()) {
if (!cell->needsLayout())
continue;
beginLayout(cell, nullptr);
inflate(cell, nullptr, DescendToInnerBlocks);
endLayout(cell);
}
}
}
}
void TextAutosizer::endLayout(LayoutBlock* block)
{
ASSERT(shouldHandleLayout());
if (block == m_firstBlockToBeginLayout) {
m_firstBlockToBeginLayout = nullptr;
m_clusterStack.clear();
m_superclusters.clear();
m_stylesRetainedDuringLayout.clear();
#if ENABLE(ASSERT)
m_blocksThatHaveBegunLayout.clear();
#endif
// Tables can create two layout scopes for the same block so the isEmpty
// check below is needed to guard against endLayout being called twice.
} else if (!m_clusterStack.isEmpty() && currentCluster()->m_root == block) {
m_clusterStack.removeLast();
}
}
float TextAutosizer::inflate(LayoutObject* parent, SubtreeLayoutScope* layouter, InflateBehavior behavior, float multiplier)
{
Cluster* cluster = currentCluster();
bool hasTextChild = false;
LayoutObject* child = nullptr;
if (parent->isLayoutBlock() && (parent->childrenInline() || behavior == DescendToInnerBlocks))
child = toLayoutBlock(parent)->firstChild();
else if (parent->isLayoutInline())
child = toLayoutInline(parent)->firstChild();
while (child) {
if (child->isText()) {
hasTextChild = true;
// We only calculate this multiplier on-demand to ensure the parent block of this text
// has entered layout.
if (!multiplier)
multiplier = cluster->m_flags & SUPPRESSING ? 1.0f : clusterMultiplier(cluster);
applyMultiplier(child, multiplier, layouter);
// FIXME: Investigate why MarkOnlyThis is sufficient.
if (parent->isLayoutInline())
child->setPreferredLogicalWidthsDirty(MarkOnlyThis);
} else if (child->isLayoutInline()) {
multiplier = inflate(child, layouter, behavior, multiplier);
} else if (child->isLayoutBlock() && behavior == DescendToInnerBlocks
&& !classifyBlock(child, INDEPENDENT | EXPLICIT_WIDTH | SUPPRESSING)) {
multiplier = inflate(child, layouter, behavior, multiplier);
}
child = child->nextSibling();
}
if (hasTextChild) {
applyMultiplier(parent, multiplier, layouter); // Parent handles line spacing.
} else if (!parent->isListItem()) {
// For consistency, a block with no immediate text child should always have a
// multiplier of 1.
applyMultiplier(parent, 1, layouter);
}
if (parent->isListItem()) {
float multiplier = clusterMultiplier(cluster);
applyMultiplier(parent, multiplier, layouter);
// The list item has to be treated special because we can have a tree such that you have
// a list item for a form inside it. The list marker then ends up inside the form and when
// we try to get the clusterMultiplier we have the wrong cluster root to work from and get
// the wrong value.
LayoutListItem* item = toLayoutListItem(parent);
if (LayoutListMarker* marker = item->marker()) {
applyMultiplier(marker, multiplier, layouter);
marker->setPreferredLogicalWidthsDirty(MarkOnlyThis);
}
}
if (m_pageInfo.m_hasAutosized)
UseCounter::count(*m_document, UseCounter::TextAutosizing);
return multiplier;
}
bool TextAutosizer::shouldHandleLayout() const
{
return m_pageInfo.m_settingEnabled && m_pageInfo.m_pageNeedsAutosizing && !m_updatePageInfoDeferred;
}
bool TextAutosizer::pageNeedsAutosizing() const
{
return m_pageInfo.m_pageNeedsAutosizing;
}
void TextAutosizer::updatePageInfoInAllFrames()
{
ASSERT(!m_document->frame() || m_document->frame()->isMainFrame());
for (Frame* frame = m_document->frame(); frame; frame = frame->tree().traverseNext()) {
if (!frame->isLocalFrame())
continue;
Document* document = toLocalFrame(frame)->document();
// If document is being detached, skip updatePageInfo.
if (!document || !document->isActive())
continue;
if (TextAutosizer* textAutosizer = document->textAutosizer())
textAutosizer->updatePageInfo();
}
}
void TextAutosizer::updatePageInfo()
{
if (m_updatePageInfoDeferred || !m_document->page() || !m_document->settings())
return;
PageInfo previousPageInfo(m_pageInfo);
m_pageInfo.m_settingEnabled = m_document->settings()->textAutosizingEnabled();
if (!m_pageInfo.m_settingEnabled || m_document->printing()) {
m_pageInfo.m_pageNeedsAutosizing = false;
} else {
LayoutViewItem layoutViewItem = m_document->layoutViewItem();
bool horizontalWritingMode = isHorizontalWritingMode(layoutViewItem.style()->getWritingMode());
// FIXME: With out-of-process iframes, the top frame can be remote and
// doesn't have sizing information. Just return if this is the case.
Frame* frame = m_document->frame()->tree().top();
if (frame->isRemoteFrame())
return;
LocalFrame* mainFrame = toLocalFrame(frame);
IntSize frameSize = m_document->settings()->textAutosizingWindowSizeOverride();
if (frameSize.isEmpty())
frameSize = windowSize();
m_pageInfo.m_frameWidth = horizontalWritingMode ? frameSize.width() : frameSize.height();
IntSize layoutSize = mainFrame->view()->layoutSize();
m_pageInfo.m_layoutWidth = horizontalWritingMode ? layoutSize.width() : layoutSize.height();
// TODO(pdr): Accessibility should be moved out of the text autosizer. See: crbug.com/645717.
m_pageInfo.m_accessibilityFontScaleFactor = m_document->settings()->accessibilityFontScaleFactor();
// If the page has a meta viewport or @viewport, don't apply the device scale adjustment.
if (!mainFrame->document()->viewportDescription().isSpecifiedByAuthor())
m_pageInfo.m_deviceScaleAdjustment = m_document->settings()->deviceScaleAdjustment();
else
m_pageInfo.m_deviceScaleAdjustment = 1.0f;
// TODO(pdr): pageNeedsAutosizing should take into account whether text-size-adjust is used
// anywhere on the page because that also needs to trigger autosizing. See: crbug.com/646237.
m_pageInfo.m_pageNeedsAutosizing = !!m_pageInfo.m_frameWidth
&& (m_pageInfo.m_accessibilityFontScaleFactor * m_pageInfo.m_deviceScaleAdjustment * (static_cast<float>(m_pageInfo.m_layoutWidth) / m_pageInfo.m_frameWidth) > 1.0f);
}
if (m_pageInfo.m_pageNeedsAutosizing) {
// If page info has changed, multipliers may have changed. Force a layout to recompute them.
if (m_pageInfo.m_frameWidth != previousPageInfo.m_frameWidth
|| m_pageInfo.m_layoutWidth != previousPageInfo.m_layoutWidth
|| m_pageInfo.m_accessibilityFontScaleFactor != previousPageInfo.m_accessibilityFontScaleFactor
|| m_pageInfo.m_deviceScaleAdjustment != previousPageInfo.m_deviceScaleAdjustment
|| m_pageInfo.m_settingEnabled != previousPageInfo.m_settingEnabled)
setAllTextNeedsLayout();
} else if (previousPageInfo.m_hasAutosized) {
// If we are no longer autosizing the page, we won't do anything during the next layout.
// Set all the multipliers back to 1 now.
resetMultipliers();
m_pageInfo.m_hasAutosized = false;
}
}
IntSize TextAutosizer::windowSize() const
{
Page * page = m_document->page();
ASSERT(page);
return page->frameHost().visualViewport().size();
}
void TextAutosizer::resetMultipliers()
{
LayoutObject* layoutObject = LayoutAPIShim::layoutObjectFrom(m_document->layoutViewItem());
while (layoutObject) {
if (const ComputedStyle* style = layoutObject->style()) {
if (style->textAutosizingMultiplier() != 1)
applyMultiplier(layoutObject, 1, nullptr, LayoutNeeded);
}
layoutObject = layoutObject->nextInPreOrder();
}
}
void TextAutosizer::setAllTextNeedsLayout()
{
LayoutItem layoutItem = m_document->layoutViewItem();
while (!layoutItem.isNull()) {
if (layoutItem.isText())
layoutItem.setNeedsLayoutAndFullPaintInvalidation(LayoutInvalidationReason::TextAutosizing);
layoutItem = layoutItem.nextInPreOrder();
}
}
TextAutosizer::BlockFlags TextAutosizer::classifyBlock(const LayoutObject* layoutObject, BlockFlags mask) const
{
if (!layoutObject->isLayoutBlock())
return 0;
const LayoutBlock* block = toLayoutBlock(layoutObject);
BlockFlags flags = 0;
if (isPotentialClusterRoot(block)) {
if (mask & POTENTIAL_ROOT)
flags |= POTENTIAL_ROOT;
if ((mask & INDEPENDENT) && (isIndependentDescendant(block) || block->isTable()))
flags |= INDEPENDENT;
if ((mask & EXPLICIT_WIDTH) && hasExplicitWidth(block))
flags |= EXPLICIT_WIDTH;
if ((mask & SUPPRESSING) && blockSuppressesAutosizing(block))
flags |= SUPPRESSING;
}
return flags;
}
bool TextAutosizer::clusterWouldHaveEnoughTextToAutosize(const LayoutBlock* root, const LayoutBlock* widthProvider)
{
Cluster hypotheticalCluster(root, classifyBlock(root), nullptr);
return clusterHasEnoughTextToAutosize(&hypotheticalCluster, widthProvider);
}
bool TextAutosizer::clusterHasEnoughTextToAutosize(Cluster* cluster, const LayoutBlock* widthProvider)
{
if (cluster->m_hasEnoughTextToAutosize != UnknownAmountOfText)
return cluster->m_hasEnoughTextToAutosize == HasEnoughText;
const LayoutBlock* root = cluster->m_root;
if (!widthProvider)
widthProvider = clusterWidthProvider(root);
// TextAreas and user-modifiable areas get a free pass to autosize regardless of text content.
if (root->isTextArea() || (root->style() && root->style()->userModify() != READ_ONLY)) {
cluster->m_hasEnoughTextToAutosize = HasEnoughText;
return true;
}
if (cluster->m_flags & SUPPRESSING) {
cluster->m_hasEnoughTextToAutosize = NotEnoughText;
return false;
}
// 4 lines of text is considered enough to autosize.
float minimumTextLengthToAutosize = widthFromBlock(widthProvider) * 4;
float length = 0;
LayoutObject* descendant = root->firstChild();
while (descendant) {
if (descendant->isLayoutBlock()) {
if (classifyBlock(descendant, INDEPENDENT | SUPPRESSING)) {
descendant = descendant->nextInPreOrderAfterChildren(root);
continue;
}
} else if (descendant->isText()) {
// Note: Using text().stripWhiteSpace().length() instead of resolvedTextLength() because
// the lineboxes will not be built until layout. These values can be different.
// Note: This is an approximation assuming each character is 1em wide.
length += toLayoutText(descendant)->text().stripWhiteSpace().length() * descendant->style()->specifiedFontSize();
if (length >= minimumTextLengthToAutosize) {
cluster->m_hasEnoughTextToAutosize = HasEnoughText;
return true;
}
}
descendant = descendant->nextInPreOrder(root);
}
cluster->m_hasEnoughTextToAutosize = NotEnoughText;
return false;
}
TextAutosizer::Fingerprint TextAutosizer::getFingerprint(const LayoutObject* layoutObject)
{
Fingerprint result = m_fingerprintMapper.get(layoutObject);
if (!result) {
result = computeFingerprint(layoutObject);
m_fingerprintMapper.add(layoutObject, result);
}
return result;
}
TextAutosizer::Fingerprint TextAutosizer::computeFingerprint(const LayoutObject* layoutObject)
{
Node* node = layoutObject->generatingNode();
if (!node || !node->isElementNode())
return 0;
FingerprintSourceData data;
if (const LayoutObject* parent = parentElementLayoutObject(layoutObject))
data.m_parentHash = getFingerprint(parent);
data.m_qualifiedNameHash = QualifiedNameHash::hash(toElement(node)->tagQName());
if (const ComputedStyle* style = layoutObject->style()) {
data.m_packedStyleProperties = style->direction();
data.m_packedStyleProperties |= (style->position() << 1);
data.m_packedStyleProperties |= (static_cast<unsigned>(style->floating()) << 4);
data.m_packedStyleProperties |= (style->display() << 6);
data.m_packedStyleProperties |= (style->width().type() << 11);
// packedStyleProperties effectively using 15 bits now.
// consider for adding: writing mode, padding.
data.m_width = style->width().getFloatValue();
}
// Use nodeIndex as a rough approximation of column number
// (it's too early to call LayoutTableCell::col).
// FIXME: account for colspan
if (layoutObject->isTableCell())
data.m_column = layoutObject->node()->nodeIndex();
return StringHasher::computeHash<UChar>(
static_cast<const UChar*>(static_cast<const void*>(&data)),
sizeof data / sizeof(UChar));
}
TextAutosizer::Cluster* TextAutosizer::maybeCreateCluster(const LayoutBlock* block)
{
BlockFlags flags = classifyBlock(block);
if (!(flags & POTENTIAL_ROOT))
return nullptr;
Cluster* parentCluster = m_clusterStack.isEmpty() ? nullptr : currentCluster();
ASSERT(parentCluster || block->isLayoutView());
// If a non-independent block would not alter the SUPPRESSING flag, it doesn't need to be a cluster.
bool parentSuppresses = parentCluster && (parentCluster->m_flags & SUPPRESSING);
if (!(flags & INDEPENDENT) && !(flags & EXPLICIT_WIDTH) && !!(flags & SUPPRESSING) == parentSuppresses)
return nullptr;
Cluster* cluster = new Cluster(block, flags, parentCluster, getSupercluster(block));
#ifdef AUTOSIZING_DOM_DEBUG_INFO
// Non-SUPPRESSING clusters are annotated in clusterMultiplier.
if (flags & SUPPRESSING)
writeClusterDebugInfo(cluster);
#endif
return cluster;
}
TextAutosizer::Supercluster* TextAutosizer::getSupercluster(const LayoutBlock* block)
{
Fingerprint fingerprint = m_fingerprintMapper.get(block);
if (!fingerprint)
return nullptr;
BlockSet* roots = m_fingerprintMapper.getTentativeClusterRoots(fingerprint);
if (!roots || roots->size() < 2 || !roots->contains(block))
return nullptr;
SuperclusterMap::AddResult addResult = m_superclusters.add(fingerprint, std::unique_ptr<Supercluster>());
if (!addResult.isNewEntry)
return addResult.storedValue->value.get();
Supercluster* supercluster = new Supercluster(roots);
addResult.storedValue->value = wrapUnique(supercluster);
return supercluster;
}
float TextAutosizer::clusterMultiplier(Cluster* cluster)
{
if (cluster->m_multiplier)
return cluster->m_multiplier;
// FIXME: why does isWiderOrNarrowerDescendant crash on independent clusters?
if (!(cluster->m_flags & INDEPENDENT) && isWiderOrNarrowerDescendant(cluster))
cluster->m_flags |= WIDER_OR_NARROWER;
if (cluster->m_flags & (INDEPENDENT | WIDER_OR_NARROWER)) {
if (cluster->m_supercluster)
cluster->m_multiplier = superclusterMultiplier(cluster);
else if (clusterHasEnoughTextToAutosize(cluster))
cluster->m_multiplier = multiplierFromBlock(clusterWidthProvider(cluster->m_root));
else
cluster->m_multiplier = 1.0f;
} else {
cluster->m_multiplier = cluster->m_parent ? clusterMultiplier(cluster->m_parent) : 1.0f;
}
#ifdef AUTOSIZING_DOM_DEBUG_INFO
writeClusterDebugInfo(cluster);
#endif
ASSERT(cluster->m_multiplier);
return cluster->m_multiplier;
}
bool TextAutosizer::superclusterHasEnoughTextToAutosize(Supercluster* supercluster, const LayoutBlock* widthProvider)
{
if (supercluster->m_hasEnoughTextToAutosize != UnknownAmountOfText)
return supercluster->m_hasEnoughTextToAutosize == HasEnoughText;
for (auto* root : *supercluster->m_roots) {
if (clusterWouldHaveEnoughTextToAutosize(root, widthProvider)) {
supercluster->m_hasEnoughTextToAutosize = HasEnoughText;
return true;
}
}
supercluster->m_hasEnoughTextToAutosize = NotEnoughText;
return false;
}
float TextAutosizer::superclusterMultiplier(Cluster* cluster)
{
Supercluster* supercluster = cluster->m_supercluster;
if (!supercluster->m_multiplier) {
const LayoutBlock* widthProvider = maxClusterWidthProvider(cluster->m_supercluster, cluster->m_root);
supercluster->m_multiplier = superclusterHasEnoughTextToAutosize(supercluster, widthProvider)
? multiplierFromBlock(widthProvider) : 1.0f;
}
ASSERT(supercluster->m_multiplier);
return supercluster->m_multiplier;
}
const LayoutBlock* TextAutosizer::clusterWidthProvider(const LayoutBlock* root) const
{
if (root->isTable() || root->isTableCell())
return root;
return deepestBlockContainingAllText(root);
}
const LayoutBlock* TextAutosizer::maxClusterWidthProvider(const Supercluster* supercluster, const LayoutBlock* currentRoot) const
{
const LayoutBlock* result = clusterWidthProvider(currentRoot);
float maxWidth = widthFromBlock(result);
const BlockSet* roots = supercluster->m_roots;
for (const auto* root : *roots) {
const LayoutBlock* widthProvider = clusterWidthProvider(root);
if (widthProvider->needsLayout())
continue;
float width = widthFromBlock(widthProvider);
if (width > maxWidth) {
maxWidth = width;
result = widthProvider;
}
}
RELEASE_ASSERT(result);
return result;
}
float TextAutosizer::widthFromBlock(const LayoutBlock* block) const
{
RELEASE_ASSERT(block);
RELEASE_ASSERT(block->style());
if (!(block->isTable() || block->isTableCell() || block->isListItem()))
return block->contentLogicalWidth().toFloat();
if (!block->containingBlock())
return 0;
// Tables may be inflated before computing their preferred widths. Try several methods to
// obtain a width, and fall back on a containing block's width.
for (; block; block = block->containingBlock()) {
float width;
Length specifiedWidth = block->isTableCell()
? toLayoutTableCell(block)->styleOrColLogicalWidth() : block->style()->logicalWidth();
if (specifiedWidth.isFixed()) {
if ((width = specifiedWidth.value()) > 0)
return width;
}
if (specifiedWidth.isPercentOrCalc()) {
if (float containerWidth = block->containingBlock()->contentLogicalWidth().toFloat()) {
if ((width = floatValueForLength(specifiedWidth, containerWidth)) > 0)
return width;
}
}
if ((width = block->contentLogicalWidth().toFloat()) > 0)
return width;
}
return 0;
}
float TextAutosizer::multiplierFromBlock(const LayoutBlock* block)
{
// If block->needsLayout() is false, it does not need to be in m_blocksThatHaveBegunLayout.
// This can happen during layout of a positioned object if the cluster's DBCAT is deeper
// than the positioned object's containing block, and wasn't marked as needing layout.
ASSERT(m_blocksThatHaveBegunLayout.contains(block) || !block->needsLayout());
// Block width, in CSS pixels.
float blockWidth = widthFromBlock(block);
float layoutWidth = std::min(blockWidth, static_cast<float>(m_pageInfo.m_layoutWidth));
float multiplier = m_pageInfo.m_frameWidth ? layoutWidth / m_pageInfo.m_frameWidth : 1.0f;
multiplier *= m_pageInfo.m_accessibilityFontScaleFactor * m_pageInfo.m_deviceScaleAdjustment;
return std::max(multiplier, 1.0f);
}
const LayoutBlock* TextAutosizer::deepestBlockContainingAllText(Cluster* cluster)
{
if (!cluster->m_deepestBlockContainingAllText)
cluster->m_deepestBlockContainingAllText = deepestBlockContainingAllText(cluster->m_root);
return cluster->m_deepestBlockContainingAllText;
}
// FIXME: Refactor this to look more like TextAutosizer::deepestCommonAncestor.
const LayoutBlock* TextAutosizer::deepestBlockContainingAllText(const LayoutBlock* root) const
{
size_t firstDepth = 0;
const LayoutObject* firstTextLeaf = findTextLeaf(root, firstDepth, First);
if (!firstTextLeaf)
return root;
size_t lastDepth = 0;
const LayoutObject* lastTextLeaf = findTextLeaf(root, lastDepth, Last);
ASSERT(lastTextLeaf);
// Equalize the depths if necessary. Only one of the while loops below will get executed.
const LayoutObject* firstNode = firstTextLeaf;
const LayoutObject* lastNode = lastTextLeaf;
while (firstDepth > lastDepth) {
firstNode = firstNode->parent();
--firstDepth;
}
while (lastDepth > firstDepth) {
lastNode = lastNode->parent();
--lastDepth;
}
// Go up from both nodes until the parent is the same. Both pointers will point to the LCA then.
while (firstNode != lastNode) {
firstNode = firstNode->parent();
lastNode = lastNode->parent();
}
if (firstNode->isLayoutBlock())
return toLayoutBlock(firstNode);
// containingBlock() should never leave the cluster, since it only skips ancestors when finding
// the container of position:absolute/fixed blocks, and those cannot exist between a cluster and
// its text node's lowest common ancestor as isAutosizingCluster would have made them into their
// own independent cluster.
const LayoutBlock* containingBlock = firstNode->containingBlock();
if (!containingBlock)
return root;
ASSERT(containingBlock->isDescendantOf(root));
return containingBlock;
}
const LayoutObject* TextAutosizer::findTextLeaf(const LayoutObject* parent, size_t& depth, TextLeafSearch firstOrLast) const
{
// List items are treated as text due to the marker.
if (parent->isListItem())
return parent;
if (parent->isText())
return parent;
++depth;
const LayoutObject* child = (firstOrLast == First) ? parent->slowFirstChild() : parent->slowLastChild();
while (child) {
// Note: At this point clusters may not have been created for these blocks so we cannot rely
// on m_clusters. Instead, we use a best-guess about whether the block will become a cluster.
if (!classifyBlock(child, INDEPENDENT)) {
if (const LayoutObject* leaf = findTextLeaf(child, depth, firstOrLast))
return leaf;
}
child = (firstOrLast == First) ? child->nextSibling() : child->previousSibling();
}
--depth;
return nullptr;
}
void TextAutosizer::applyMultiplier(LayoutObject* layoutObject, float multiplier, SubtreeLayoutScope* layouter, RelayoutBehavior relayoutBehavior)
{
ASSERT(layoutObject);
ComputedStyle& currentStyle = layoutObject->mutableStyleRef();
if (!currentStyle.getTextSizeAdjust().isAuto()) {
// The accessibility font scale factor is applied by the autosizer so we need to apply that
// scale factor on top of the text-size-adjust multiplier.
multiplier = currentStyle.getTextSizeAdjust().multiplier() * m_pageInfo.m_accessibilityFontScaleFactor;
} else if (multiplier < 1) {
// Unlike text-size-adjust, the text autosizer should only inflate fonts.
multiplier = 1;
}
if (currentStyle.textAutosizingMultiplier() == multiplier)
return;
// We need to clone the layoutObject style to avoid breaking style sharing.
RefPtr<ComputedStyle> style = ComputedStyle::clone(currentStyle);
style->setTextAutosizingMultiplier(multiplier);
style->setUnique();
switch (relayoutBehavior) {
case AlreadyInLayout:
// Don't free currentStyle until the end of the layout pass. This allows other parts of the system
// to safely hold raw ComputedStyle* pointers during layout, e.g. BreakingContext::m_currentStyle.
m_stylesRetainedDuringLayout.append(&currentStyle);
layoutObject->setStyleInternal(style.release());
DCHECK(!layouter || layoutObject->isDescendantOf(&layouter->root()));
layoutObject->setNeedsLayoutAndFullPaintInvalidation(LayoutInvalidationReason::TextAutosizing, MarkContainerChain, layouter);
break;
case LayoutNeeded:
DCHECK(!layouter);
layoutObject->setStyle(style.release());
break;
}
if (multiplier != 1)
m_pageInfo.m_hasAutosized = true;
layoutObject->clearBaseComputedStyle();
}
bool TextAutosizer::isWiderOrNarrowerDescendant(Cluster* cluster)
{
// FIXME: Why do we return true when hasExplicitWidth returns false??
if (!cluster->m_parent || !hasExplicitWidth(cluster->m_root))
return true;
const LayoutBlock* parentDeepestBlockContainingAllText = deepestBlockContainingAllText(cluster->m_parent);
ASSERT(m_blocksThatHaveBegunLayout.contains(cluster->m_root));
ASSERT(m_blocksThatHaveBegunLayout.contains(parentDeepestBlockContainingAllText));
float contentWidth = cluster->m_root->contentLogicalWidth().toFloat();
float clusterTextWidth = parentDeepestBlockContainingAllText->contentLogicalWidth().toFloat();
// Clusters with a root that is wider than the deepestBlockContainingAllText of their parent
// autosize independently of their parent.
if (contentWidth > clusterTextWidth)
return true;
// Clusters with a root that is significantly narrower than the deepestBlockContainingAllText of
// their parent autosize independently of their parent.
static float narrowWidthDifference = 200;
if (clusterTextWidth - contentWidth > narrowWidthDifference)
return true;
return false;
}
TextAutosizer::Cluster* TextAutosizer::currentCluster() const
{
ASSERT_WITH_SECURITY_IMPLICATION(!m_clusterStack.isEmpty());
return m_clusterStack.last().get();
}
TextAutosizer::Cluster::Cluster(const LayoutBlock* root, BlockFlags flags, Cluster* parent, Supercluster* supercluster)
: m_root(root)
, m_flags(flags)
, m_deepestBlockContainingAllText(nullptr)
, m_parent(parent)
, m_multiplier(0)
, m_hasEnoughTextToAutosize(UnknownAmountOfText)
, m_supercluster(supercluster)
, m_hasTableAncestor(root->isTableCell() || (m_parent && m_parent->m_hasTableAncestor))
{
}
#if ENABLE(ASSERT)
void TextAutosizer::FingerprintMapper::assertMapsAreConsistent()
{
// For each fingerprint -> block mapping in m_blocksForFingerprint we should have an associated
// map from block -> fingerprint in m_fingerprints.
ReverseFingerprintMap::iterator end = m_blocksForFingerprint.end();
for (ReverseFingerprintMap::iterator fingerprintIt = m_blocksForFingerprint.begin(); fingerprintIt != end; ++fingerprintIt) {
Fingerprint fingerprint = fingerprintIt->key;
BlockSet* blocks = fingerprintIt->value.get();
for (BlockSet::iterator blockIt = blocks->begin(); blockIt != blocks->end(); ++blockIt) {
const LayoutBlock* block = (*blockIt);
ASSERT(m_fingerprints.get(block) == fingerprint);
}
}
}
#endif
void TextAutosizer::FingerprintMapper::add(const LayoutObject* layoutObject, Fingerprint fingerprint)
{
remove(layoutObject);
m_fingerprints.set(layoutObject, fingerprint);
#if ENABLE(ASSERT)
assertMapsAreConsistent();
#endif
}
void TextAutosizer::FingerprintMapper::addTentativeClusterRoot(const LayoutBlock* block, Fingerprint fingerprint)
{
add(block, fingerprint);
ReverseFingerprintMap::AddResult addResult = m_blocksForFingerprint.add(fingerprint, std::unique_ptr<BlockSet>());
if (addResult.isNewEntry)
addResult.storedValue->value = wrapUnique(new BlockSet);
addResult.storedValue->value->add(block);
#if ENABLE(ASSERT)
assertMapsAreConsistent();
#endif
}
bool TextAutosizer::FingerprintMapper::remove(const LayoutObject* layoutObject)
{
Fingerprint fingerprint = m_fingerprints.take(layoutObject);
if (!fingerprint || !layoutObject->isLayoutBlock())
return false;
ReverseFingerprintMap::iterator blocksIter = m_blocksForFingerprint.find(fingerprint);
if (blocksIter == m_blocksForFingerprint.end())
return false;
BlockSet& blocks = *blocksIter->value;
blocks.remove(toLayoutBlock(layoutObject));
if (blocks.isEmpty())
m_blocksForFingerprint.remove(blocksIter);
#if ENABLE(ASSERT)
assertMapsAreConsistent();
#endif
return true;
}
TextAutosizer::Fingerprint TextAutosizer::FingerprintMapper::get(const LayoutObject* layoutObject)
{
return m_fingerprints.get(layoutObject);
}
TextAutosizer::BlockSet* TextAutosizer::FingerprintMapper::getTentativeClusterRoots(Fingerprint fingerprint)
{
return m_blocksForFingerprint.get(fingerprint);
}
TextAutosizer::LayoutScope::LayoutScope(LayoutBlock* block, SubtreeLayoutScope* layouter)
: m_textAutosizer(block->document().textAutosizer())
, m_block(block)
{
if (!m_textAutosizer)
return;
if (m_textAutosizer->shouldHandleLayout())
m_textAutosizer->beginLayout(m_block, layouter);
else
m_textAutosizer = nullptr;
}
TextAutosizer::LayoutScope::~LayoutScope()
{
if (m_textAutosizer)
m_textAutosizer->endLayout(m_block);
}
TextAutosizer::TableLayoutScope::TableLayoutScope(LayoutTable* table)
: LayoutScope(table)
{
if (m_textAutosizer) {
ASSERT(m_textAutosizer->shouldHandleLayout());
m_textAutosizer->inflateAutoTable(table);
}
}
TextAutosizer::DeferUpdatePageInfo::DeferUpdatePageInfo(Page* page)
: m_mainFrame(page->deprecatedLocalMainFrame())
{
if (TextAutosizer* textAutosizer = m_mainFrame->document()->textAutosizer()) {
ASSERT(!textAutosizer->m_updatePageInfoDeferred);
textAutosizer->m_updatePageInfoDeferred = true;
}
}
TextAutosizer::DeferUpdatePageInfo::~DeferUpdatePageInfo()
{
if (TextAutosizer* textAutosizer = m_mainFrame->document()->textAutosizer()) {
ASSERT(textAutosizer->m_updatePageInfoDeferred);
textAutosizer->m_updatePageInfoDeferred = false;
textAutosizer->updatePageInfoInAllFrames();
}
}
float TextAutosizer::computeAutosizedFontSize(float specifiedSize, float multiplier)
{
DCHECK_GE(multiplier, 0);
// Somewhat arbitrary "pleasant" font size.
const float pleasantSize = 16;
// Multiply fonts that the page author has specified to be larger than
// pleasantSize by less and less, until huge fonts are not increased at all.
// For specifiedSize between 0 and pleasantSize we directly apply the
// multiplier; hence for specifiedSize == pleasantSize, computedSize will be
// multiplier * pleasantSize. For greater specifiedSizes we want to
// gradually fade out the multiplier, so for every 1px increase in
// specifiedSize beyond pleasantSize we will only increase computedSize
// by gradientAfterPleasantSize px until we meet the
// computedSize = specifiedSize line, after which we stay on that line (so
// then every 1px increase in specifiedSize increases computedSize by 1px).
const float gradientAfterPleasantSize = 0.5;
float computedSize;
// Skip linear backoff for multipliers that shrink the size or when the font sizes are small.
if (multiplier <= 1 || specifiedSize <= pleasantSize) {
computedSize = multiplier * specifiedSize;
} else {
computedSize = multiplier * pleasantSize + gradientAfterPleasantSize * (specifiedSize - pleasantSize);
if (computedSize < specifiedSize)
computedSize = specifiedSize;
}
return computedSize;
}
DEFINE_TRACE(TextAutosizer)
{
visitor->trace(m_document);
}
} // namespace blink