1/*
2 * Copyright (C) 2011 Apple Inc. All rights reserved.
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Library General Public
6 * License as published by the Free Software Foundation; either
7 * version 2 of the License, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Library General Public License for more details.
13 *
14 * You should have received a copy of the GNU Library General Public License
15 * along with this library; see the file COPYING.LIB. If not, write to
16 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
17 * Boston, MA 02110-1301, USA.
18 *
19 */
20
21#include "config.h"
22#include "RenderCombineText.h"
23
24#include "RenderBlock.h"
25#include "StyleInheritedData.h"
26#include <wtf/IsoMallocInlines.h>
27#include <wtf/NeverDestroyed.h>
28
29namespace WebCore {
30
31WTF_MAKE_ISO_ALLOCATED_IMPL(RenderCombineText);
32
33const float textCombineMargin = 1.15f; // Allow em + 15% margin
34
35RenderCombineText::RenderCombineText(Text& textNode, const String& string)
36 : RenderText(textNode, string)
37 , m_isCombined(false)
38 , m_needsFontUpdate(false)
39{
40}
41
42void RenderCombineText::styleDidChange(StyleDifference diff, const RenderStyle* oldStyle)
43{
44 // FIXME: This is pretty hackish.
45 // Only cache a new font style if our old one actually changed. We do this to avoid
46 // clobbering width variants and shrink-to-fit changes, since we won't recombine when
47 // the font doesn't change.
48 if (!oldStyle || oldStyle->fontCascade() != style().fontCascade())
49 m_combineFontStyle = RenderStyle::clonePtr(style());
50
51 RenderText::styleDidChange(diff, oldStyle);
52
53 if (m_isCombined && selfNeedsLayout()) {
54 // Layouts cause the text to be recombined; therefore, only only un-combine when the style diff causes a layout.
55 RenderText::setRenderedText(originalText()); // This RenderCombineText has been combined once. Restore the original text for the next combineText().
56 m_isCombined = false;
57 }
58
59 m_needsFontUpdate = true;
60 combineTextIfNeeded();
61}
62
63void RenderCombineText::setRenderedText(const String& text)
64{
65 RenderText::setRenderedText(text);
66
67 m_needsFontUpdate = true;
68 combineTextIfNeeded();
69}
70
71float RenderCombineText::width(unsigned from, unsigned length, const FontCascade& font, float xPosition, HashSet<const Font*>* fallbackFonts, GlyphOverflow* glyphOverflow) const
72{
73 if (m_isCombined)
74 return !length ? 0 : font.size();
75
76 return RenderText::width(from, length, font, xPosition, fallbackFonts, glyphOverflow);
77}
78
79Optional<FloatPoint> RenderCombineText::computeTextOrigin(const FloatRect& boxRect) const
80{
81 if (!m_isCombined)
82 return WTF::nullopt;
83
84 // Visually center m_combinedTextWidth/Ascent/Descent within boxRect
85 FloatPoint result = boxRect.minXMaxYCorner();
86 FloatSize combinedTextSize(m_combinedTextWidth, m_combinedTextAscent + m_combinedTextDescent);
87 result.move((boxRect.size().transposedSize() - combinedTextSize) / 2);
88 result.move(0, m_combinedTextAscent);
89 return result;
90}
91
92String RenderCombineText::combinedStringForRendering() const
93{
94 if (m_isCombined) {
95 auto originalText = this->originalText();
96 ASSERT(!originalText.isNull());
97 return originalText;
98 }
99
100 return { };
101}
102
103void RenderCombineText::combineTextIfNeeded()
104{
105 if (!m_needsFontUpdate)
106 return;
107
108 // An ancestor element may trigger us to lay out again, even when we're already combined.
109 if (m_isCombined)
110 RenderText::setRenderedText(originalText());
111
112 m_isCombined = false;
113 m_needsFontUpdate = false;
114
115 // CSS3 spec says text-combine works only in vertical writing mode.
116 if (style().isHorizontalWritingMode())
117 return;
118
119 auto description = originalFont().fontDescription();
120 float emWidth = description.computedSize() * textCombineMargin;
121 bool shouldUpdateFont = false;
122
123 FontSelector* fontSelector = style().fontCascade().fontSelector();
124
125 description.setOrientation(FontOrientation::Horizontal); // We are going to draw combined text horizontally.
126
127 FontCascade horizontalFont(FontCascadeDescription { description }, style().fontCascade().letterSpacing(), style().fontCascade().wordSpacing());
128 horizontalFont.update(fontSelector);
129
130 GlyphOverflow glyphOverflow;
131 glyphOverflow.computeBounds = true;
132 float combinedTextWidth = width(0, text().length(), horizontalFont, 0, nullptr, &glyphOverflow);
133
134 float bestFitDelta = combinedTextWidth - emWidth;
135 auto bestFitDescription = description;
136
137 m_isCombined = combinedTextWidth <= emWidth;
138
139 if (m_isCombined)
140 shouldUpdateFont = m_combineFontStyle->setFontDescription(WTFMove(description)); // Need to change font orientation to horizontal.
141 else {
142 // Need to try compressed glyphs.
143 static const FontWidthVariant widthVariants[] = { FontWidthVariant::HalfWidth, FontWidthVariant::ThirdWidth, FontWidthVariant::QuarterWidth };
144 for (auto widthVariant : widthVariants) {
145 description.setWidthVariant(widthVariant); // When modifying this, make sure to keep it in sync with FontPlatformData::isForTextCombine()!
146
147 FontCascade compressedFont(FontCascadeDescription { description }, style().fontCascade().letterSpacing(), style().fontCascade().wordSpacing());
148 compressedFont.update(fontSelector);
149
150 glyphOverflow.left = glyphOverflow.top = glyphOverflow.right = glyphOverflow.bottom = 0;
151 float runWidth = RenderText::width(0, text().length(), compressedFont, 0, nullptr, &glyphOverflow);
152 if (runWidth <= emWidth) {
153 combinedTextWidth = runWidth;
154 m_isCombined = true;
155
156 // Replace my font with the new one.
157 shouldUpdateFont = m_combineFontStyle->setFontDescription(WTFMove(description));
158 break;
159 }
160
161 float widthDelta = runWidth - emWidth;
162 if (widthDelta < bestFitDelta) {
163 bestFitDelta = widthDelta;
164 bestFitDescription = description;
165 }
166 }
167 }
168
169 if (!m_isCombined) {
170 float scaleFactor = std::max(0.4f, emWidth / (emWidth + bestFitDelta));
171 float originalSize = bestFitDescription.computedSize();
172 do {
173 float computedSize = originalSize * scaleFactor;
174 bestFitDescription.setComputedSize(computedSize);
175 shouldUpdateFont = m_combineFontStyle->setFontDescription(FontCascadeDescription { bestFitDescription });
176
177 FontCascade compressedFont(FontCascadeDescription(bestFitDescription), style().fontCascade().letterSpacing(), style().fontCascade().wordSpacing());
178 compressedFont.update(fontSelector);
179
180 glyphOverflow.left = glyphOverflow.top = glyphOverflow.right = glyphOverflow.bottom = 0;
181 float runWidth = RenderText::width(0, text().length(), compressedFont, 0, nullptr, &glyphOverflow);
182 if (runWidth <= emWidth) {
183 combinedTextWidth = runWidth;
184 m_isCombined = true;
185 break;
186 }
187 scaleFactor -= 0.05f;
188 } while (scaleFactor >= 0.4f);
189 }
190
191 if (shouldUpdateFont)
192 m_combineFontStyle->fontCascade().update(fontSelector);
193
194 if (m_isCombined) {
195 static NeverDestroyed<String> objectReplacementCharacterString(&objectReplacementCharacter, 1);
196 RenderText::setRenderedText(objectReplacementCharacterString.get());
197 m_combinedTextWidth = combinedTextWidth;
198 m_combinedTextAscent = glyphOverflow.top;
199 m_combinedTextDescent = glyphOverflow.bottom;
200 m_lineBoxes.dirtyRange(*this, 0, originalText().length(), originalText().length());
201 setNeedsLayout();
202 }
203}
204
205} // namespace WebCore
206