blob: b6a8dab804881b94eb4209cd89ba1de2c76759da [file]
/*
* Copyright (C) 2007, 2008, 2009, 2010, 2011, 2012, 2013 Apple Inc. All rights reserved.
* Copyright (C) 2026 Samuel Weinig <sam@webkit.org>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. 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.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS 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 APPLE INC. OR ITS 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 "config.h"
#include "TextAutoSizing.h"
#include "CSSFontSelector.h"
#include "Document.h"
#include "FontCascade.h"
#include "Logging.h"
#include "RenderBlock.h"
#include "RenderElementInlines.h"
#include "RenderListOutsideMarker.h"
#include "RenderObjectInlines.h"
#include "RenderText.h"
#include "RenderTextFragment.h"
#include "RenderTreeBuilder.h"
#include "Settings.h"
#include "StyleComputedStyle+SettersInlines.h"
#include "StylePrimitiveNumericTypes+Evaluation.h"
#include "StyleResolver.h"
#include "StyleTextSizeAdjust.h"
#include <utility>
#include <wtf/HashSet.h>
#include <wtf/Ref.h>
#include <wtf/TZoneMallocInlines.h>
#include <wtf/text/StringHash.h>
namespace WebCore {
struct TextAutoSizingHashTranslator {
static unsigned hash(const Style::ComputedStyle&);
static bool equal(const Style::ComputedStyle&, const Style::ComputedStyle&);
static bool equal(const TextAutoSizingKey&, const Style::ComputedStyle&);
static void translate(TextAutoSizingKey&, const Style::ComputedStyle&, unsigned hash);
};
class TextAutoSizingValue {
WTF_MAKE_TZONE_ALLOCATED(TextAutoSizingValue);
public:
TextAutoSizingValue() = default;
~TextAutoSizingValue();
void addTextNode(Text&, float size);
enum class StillHasNodes : bool { No, Yes };
StillHasNodes adjustTextNodeSizes();
private:
void reset();
HashSet<Ref<Text>> m_autoSizedNodes;
};
WTF_MAKE_TZONE_ALLOCATED_IMPL(TextAutoSizingValue);
WTF_MAKE_TZONE_ALLOCATED_IMPL(TextAutoSizing);
// MARK: - TextAutoSizingKey
TextAutoSizingKey::TextAutoSizingKey(DeletedTag)
{
HashTraits<std::unique_ptr<Style::ComputedStyle>>::constructDeletedValue(m_style);
}
TextAutoSizingKey::TextAutoSizingKey(const Style::ComputedStyle& style, unsigned hash)
: m_style(Style::ComputedStyle::clonePtr(style)) // FIXME: This seems very inefficient.
, m_hash(hash)
{
}
bool TextAutoSizingKey::operator==(const TextAutoSizingKey& other) const
{
if (isDeleted() || other.isDeleted())
return false;
if (!style() || !other.style())
return style() == other.style();
return TextAutoSizingHashTranslator::equal(*style(), *other.style());
}
// MARK: - TextAutoSizingHashTranslator
static unsigned computeFontHash(const FontCascade& font)
{
// FIXME: Would be better to hash the family name rather than hashing a hash of the family name. Also, should this use FontCascadeDescription::familyNameHash?
return computeHash(
ASCIICaseInsensitiveHash::hash(font.fontDescription().firstFamily().name),
font.fontDescription().computedSize()
);
}
unsigned TextAutoSizingHashTranslator::hash(const Style::ComputedStyle& style)
{
// FIXME: Not a very smart hash. Could be improved upon. See <https://bugs.webkit.org/show_bug.cgi?id=121131>.
unsigned hash = std::to_underlying(style.usedAppearance());
hash ^= style.lineClamp().valueForHash();
hash ^= std::to_underlying(style.overflowWrap());
hash ^= std::to_underlying(style.nbspMode());
hash ^= std::to_underlying(style.lineBreak());
hash ^= std::to_underlying(style.textSecurity());
hash ^= style.specifiedLineHeight().valueForHash();
hash ^= computeFontHash(style.fontCascade());
hash ^= WTF::FloatHash<float>::hash(style.borderHorizontalSpacing().unresolvedValue());
hash ^= WTF::FloatHash<float>::hash(style.borderVerticalSpacing().unresolvedValue());
hash ^= std::to_underlying(style.boxDirection());
hash ^= std::to_underlying(style.rtlOrdering());
hash ^= std::to_underlying(style.position());
hash ^= std::to_underlying(style.floating());
hash ^= style.textOverflow().switchOn(
[](const CSS::Keyword::Clip&) -> unsigned { return computeHash(0); },
[](const CSS::Keyword::Ellipsis&) -> unsigned { return computeHash(1); },
[](const Style::String& string) -> unsigned { return computeHash(2, string.value); }
);
return hash;
}
bool TextAutoSizingHashTranslator::equal(const TextAutoSizingKey& key, const Style::ComputedStyle& styleB)
{
return !key.isDeleted() && key.style() && equal(*key.style(), styleB);
}
bool TextAutoSizingHashTranslator::equal(const Style::ComputedStyle& styleA, const Style::ComputedStyle& styleB)
{
return styleA.usedAppearance() == styleB.usedAppearance()
&& styleA.lineClamp() == styleB.lineClamp()
&& styleA.textSizeAdjust() == styleB.textSizeAdjust()
&& styleA.overflowWrap() == styleB.overflowWrap()
&& styleA.nbspMode() == styleB.nbspMode()
&& styleA.lineBreak() == styleB.lineBreak()
&& styleA.textSecurity() == styleB.textSecurity()
&& styleA.specifiedLineHeight() == styleB.specifiedLineHeight()
&& styleA.fontCascade().equalForTextAutoSizing(styleB.fontCascade())
&& styleA.borderHorizontalSpacing() == styleB.borderHorizontalSpacing()
&& styleA.borderVerticalSpacing() == styleB.borderVerticalSpacing()
&& styleA.boxDirection() == styleB.boxDirection()
&& styleA.rtlOrdering() == styleB.rtlOrdering()
&& styleA.position() == styleB.position()
&& styleA.floating() == styleB.floating()
&& styleA.textOverflow() == styleB.textOverflow();
}
void TextAutoSizingHashTranslator::translate(TextAutoSizingKey& key, const Style::ComputedStyle& style, unsigned hash)
{
key = { style, hash };
}
// MARK: - TextAutoSizingValue
static Style::ComputedStyle cloneRenderStyleWithState(const Style::ComputedStyle& currentStyle)
{
auto newStyle = Style::ComputedStyle::clone(currentStyle);
// FIXME: This should probably handle at least ::first-line too.
if (auto* firstLetterStyle = currentStyle.pseudoElementStyle({ PseudoElementType::FirstLetter }))
newStyle.addPseudoElementStyle(makeUnique<Style::ComputedStyle>(Style::ComputedStyle::clone(*firstLetterStyle)));
if (currentStyle.lastChildState())
newStyle.setLastChildState();
if (currentStyle.firstChildState())
newStyle.setFirstChildState();
return newStyle;
}
void TextAutoSizingValue::addTextNode(Text& node, float size)
{
node.renderer()->setCandidateComputedTextSize(size);
m_autoSizedNodes.add(node);
}
auto TextAutoSizingValue::adjustTextNodeSizes() -> StillHasNodes
{
// Remove stale nodes. Nodes may have had their renderers detached. We'll also need to remove the style from the documents m_textAutoSizedNodes
// collection. Return true indicates we need to do that removal.
m_autoSizedNodes.removeIf([&](auto& textNode) {
auto* renderer = textNode->renderer();
return !renderer || !renderer->style().textSizeAdjust().isAuto() || !renderer->candidateComputedTextSize();
});
StillHasNodes stillHasNodes = m_autoSizedNodes.isEmpty() ? StillHasNodes::No : StillHasNodes::Yes;
// If we only have one piece of text with the style on the page don't adjust it's size.
if (m_autoSizedNodes.size() <= 1)
return stillHasNodes;
// Compute average size.
float cumulativeSize = 0;
for (auto& node : m_autoSizedNodes)
cumulativeSize += node->renderer()->candidateComputedTextSize();
float averageSize = std::round(cumulativeSize / m_autoSizedNodes.size());
// FIXME: Figure out how to make this code use RenderTreeUpdater/Builder properly.
RenderTreeBuilder builder((*m_autoSizedNodes.begin())->renderer()->view());
// Adjust sizes.
bool firstPass = true;
for (auto& node : m_autoSizedNodes) {
auto& renderer = *node->renderer();
if (renderer.style().fontDescription().usedSize() == averageSize)
continue;
float computedSize = renderer.style().fontDescription().computedSize();
float maxScaleIncrease = renderer.settings().maxTextAutosizingScaleIncrease();
float scaleChange = averageSize / computedSize;
if (scaleChange > maxScaleIncrease && firstPass) {
firstPass = false;
averageSize = std::round(computedSize * maxScaleIncrease);
scaleChange = averageSize / computedSize;
}
LOG(TextAutosizing, " adjust node size %p firstPass=%d averageSize=%f scaleChange=%f", node.ptr(), firstPass, averageSize, scaleChange);
auto* parentRenderer = renderer.parent();
auto style = cloneRenderStyleWithState(renderer.style());
auto fontDescription = style.fontDescription();
fontDescription.setUsedSize(averageSize);
style.setFontDescription(FontCascadeDescription { fontDescription });
parentRenderer->setStyle(WTF::move(style));
if (parentRenderer->isAnonymousBlock())
parentRenderer = parentRenderer->parent();
// If we have a list we should resize ListMarkers separately.
if (auto* listMarkerRenderer = dynamicDowncast<RenderListOutsideMarker>(*parentRenderer->firstChild())) {
auto style = cloneRenderStyleWithState(listMarkerRenderer->style());
style.setFontDescription(FontCascadeDescription { fontDescription });
listMarkerRenderer->setStyle(WTF::move(style));
}
// Resize the line height of the parent.
auto& parentStyle = parentRenderer->style();
auto& lineHeightLength = parentStyle.specifiedLineHeight();
int specifiedLineHeight = WTF::switchOn(lineHeightLength,
[&](const CSS::Keyword::Normal&) {
return 0;
},
[&](const Style::LineHeight::Length& length) {
return Style::evaluate<LayoutUnit>(length, Style::ZoomFactor::none()).toInt();
},
[&](const Style::LineHeight::Number& number) {
return LayoutUnit { number.value * LayoutUnit { fontDescription.computedSize() } }.toInt();
}
);
// This calculation matches the line-height computed size calculation in StyleBuilderCustom::applyValueLineHeight().
int lineHeight = specifiedLineHeight * scaleChange;
if (auto fixedLineHeight = lineHeightLength.tryLength(); fixedLineHeight && fixedLineHeight->resolveZoom(Style::ZoomFactor::none()) == lineHeight)
continue;
auto newParentStyle = cloneRenderStyleWithState(parentStyle);
newParentStyle.setLineHeight(lineHeightLength.isNormal() ? Style::LineHeight { lineHeightLength } : Style::LineHeight { Style::LineHeight::Length { static_cast<float>(lineHeight) } });
newParentStyle.setSpecifiedLineHeight(Style::LineHeight { lineHeightLength });
newParentStyle.setFontDescription(WTF::move(fontDescription));
parentRenderer->setStyle(WTF::move(newParentStyle));
builder.updateAfterDescendants(*parentRenderer);
}
for (auto& node : m_autoSizedNodes) {
auto* textRenderer = dynamicDowncast<RenderTextFragment>(*node->renderer());
if (!textRenderer)
continue;
CheckedPtr block = textRenderer->blockForAccompanyingFirstLetter();
if (!block)
continue;
auto [firstLetter, firstLetterContainer] = block->firstLetterAndContainer();
if (firstLetter && firstLetter->parent() && firstLetter->parent()->parent()) {
auto& parentStyle = firstLetter->parent()->parent()->style();
auto* firstLetterStyle = parentStyle.pseudoElementStyle({ PseudoElementType::FirstLetter });
if (!firstLetterStyle)
continue;
auto fontDescription = firstLetterStyle->fontDescription();
fontDescription.setUsedSize(averageSize * fontDescription.computedSize() / parentStyle.fontDescription().computedSize());
firstLetterStyle->setFontDescription(FontCascadeDescription { fontDescription });
}
builder.updateAfterDescendants(*block);
}
return stillHasNodes;
}
TextAutoSizingValue::~TextAutoSizingValue()
{
reset();
}
void TextAutoSizingValue::reset()
{
for (auto& node : m_autoSizedNodes) {
auto* renderer = node->renderer();
if (!renderer)
continue;
auto* parentRenderer = renderer->parent();
if (!parentRenderer)
continue;
// Reset the font size back to the original specified size
auto fontDescription = renderer->style().fontDescription();
float originalSize = fontDescription.computedSize();
if (fontDescription.usedSize() != originalSize) {
fontDescription.setUsedSize(originalSize);
auto style = cloneRenderStyleWithState(renderer->style());
style.setFontDescription(FontCascadeDescription { fontDescription });
parentRenderer->setStyle(WTF::move(style));
}
// Reset the line height of the parent.
if (parentRenderer->isAnonymousBlock())
parentRenderer = parentRenderer->parent();
auto& parentStyle = parentRenderer->style();
auto& originalLineHeight = parentStyle.specifiedLineHeight();
if (originalLineHeight == parentStyle.lineHeight())
continue;
auto newParentStyle = cloneRenderStyleWithState(parentStyle);
newParentStyle.setLineHeight(Style::LineHeight { originalLineHeight });
newParentStyle.setFontDescription(WTF::move(fontDescription));
parentRenderer->setStyle(WTF::move(newParentStyle));
}
}
// MARK: - TextAutoSizing
TextAutoSizing::TextAutoSizing() = default;
TextAutoSizing::~TextAutoSizing() = default;
void TextAutoSizing::addTextNode(Text& node, float candidateSize)
{
LOG(TextAutosizing, " addAutoSizedNode %p candidateSize=%f", &node, candidateSize);
auto addResult = m_textNodes.add<TextAutoSizingHashTranslator>(node.renderer()->style(), nullptr);
if (addResult.isNewEntry)
addResult.iterator->value = makeUnique<TextAutoSizingValue>();
addResult.iterator->value->addTextNode(node, candidateSize);
}
void TextAutoSizing::updateRenderTree()
{
m_textNodes.removeIf([](auto& keyAndValue) {
return keyAndValue.value->adjustTextNodeSizes() == TextAutoSizingValue::StillHasNodes::No;
});
}
void TextAutoSizing::reset()
{
m_textNodes.clear();
}
} // namespace WebCore