[WTF] Import std::optional reference implementation as WTF::Optional
[WebKit-https.git] / Source / WebCore / rendering / RenderCombineText.cpp
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/NeverDestroyed.h>
27
28 namespace WebCore {
29
30 const float textCombineMargin = 1.15f; // Allow em + 15% margin
31
32 RenderCombineText::RenderCombineText(Text& textNode, const String& string)
33     : RenderText(textNode, string)
34     , m_isCombined(false)
35     , m_needsFontUpdate(false)
36 {
37 }
38
39 void RenderCombineText::styleDidChange(StyleDifference diff, const RenderStyle* oldStyle)
40 {
41     // FIXME: This is pretty hackish.
42     // Only cache a new font style if our old one actually changed. We do this to avoid
43     // clobbering width variants and shrink-to-fit changes, since we won't recombine when
44     // the font doesn't change.
45     if (!oldStyle || oldStyle->fontCascade() != style().fontCascade())
46         m_combineFontStyle = RenderStyle::clonePtr(style());
47
48     RenderText::styleDidChange(diff, oldStyle);
49
50     if (m_isCombined && selfNeedsLayout()) {
51         // Layouts cause the text to be recombined; therefore, only only un-combine when the style diff causes a layout.
52         RenderText::setRenderedText(originalText()); // This RenderCombineText has been combined once. Restore the original text for the next combineText().
53         m_isCombined = false;
54     }
55
56     m_needsFontUpdate = true;
57 }
58
59 void RenderCombineText::setRenderedText(const String& text)
60 {
61     RenderText::setRenderedText(text);
62
63     m_needsFontUpdate = true;
64 }
65
66 float RenderCombineText::width(unsigned from, unsigned length, const FontCascade& font, float xPosition, HashSet<const Font*>* fallbackFonts, GlyphOverflow* glyphOverflow) const
67 {
68     if (m_isCombined)
69         return !length ? 0 : font.size();
70
71     return RenderText::width(from, length, font, xPosition, fallbackFonts, glyphOverflow);
72 }
73
74 std::optional<FloatPoint> RenderCombineText::computeTextOrigin(const FloatRect& boxRect) const
75 {
76     if (!m_isCombined)
77         return std::nullopt;
78
79     // Visually center m_combinedTextWidth/Ascent/Descent within boxRect
80     FloatPoint result = boxRect.minXMaxYCorner();
81     FloatSize combinedTextSize(m_combinedTextWidth, m_combinedTextAscent + m_combinedTextDescent);
82     result.move((boxRect.size().transposedSize() - combinedTextSize) / 2);
83     result.move(0, m_combinedTextAscent);
84     return result;
85 }
86
87 String RenderCombineText::combinedStringForRendering() const
88 {
89     if (m_isCombined) {
90         auto originalText = this->originalText();
91         ASSERT(!originalText.isNull());
92         return originalText;
93     }
94  
95     return { };
96 }
97
98 void RenderCombineText::combineText()
99 {
100     if (!m_needsFontUpdate)
101         return;
102
103     // An ancestor element may trigger us to lay out again, even when we're already combined.
104     if (m_isCombined)
105         RenderText::setRenderedText(originalText());
106
107     m_isCombined = false;
108     m_needsFontUpdate = false;
109
110     // CSS3 spec says text-combine works only in vertical writing mode.
111     if (style().isHorizontalWritingMode())
112         return;
113
114     auto description = originalFont().fontDescription();
115     float emWidth = description.computedSize() * textCombineMargin;
116     bool shouldUpdateFont = false;
117
118     FontSelector* fontSelector = style().fontCascade().fontSelector();
119
120     description.setOrientation(Horizontal); // We are going to draw combined text horizontally.
121
122     FontCascade horizontalFont(description, style().fontCascade().letterSpacing(), style().fontCascade().wordSpacing());
123     horizontalFont.update(fontSelector);
124     
125     GlyphOverflow glyphOverflow;
126     glyphOverflow.computeBounds = true;
127     float combinedTextWidth = width(0, textLength(), horizontalFont, 0, nullptr, &glyphOverflow);
128
129     float bestFitDelta = combinedTextWidth - emWidth;
130     auto bestFitDescription = description;
131
132     m_isCombined = combinedTextWidth <= emWidth;
133     
134     if (m_isCombined)
135         shouldUpdateFont = m_combineFontStyle->setFontDescription(description); // Need to change font orientation to horizontal.
136     else {
137         // Need to try compressed glyphs.
138         static const FontWidthVariant widthVariants[] = { HalfWidth, ThirdWidth, QuarterWidth };
139         for (size_t i = 0 ; i < WTF_ARRAY_LENGTH(widthVariants) ; ++i) {
140             description.setWidthVariant(widthVariants[i]); // When modifying this, make sure to keep it in sync with FontPlatformData::isForTextCombine()!
141
142             FontCascade compressedFont(description, style().fontCascade().letterSpacing(), style().fontCascade().wordSpacing());
143             compressedFont.update(fontSelector);
144             
145             glyphOverflow.left = glyphOverflow.top = glyphOverflow.right = glyphOverflow.bottom = 0;
146             float runWidth = RenderText::width(0, textLength(), compressedFont, 0, nullptr, &glyphOverflow);
147             if (runWidth <= emWidth) {
148                 combinedTextWidth = runWidth;
149                 m_isCombined = true;
150
151                 // Replace my font with the new one.
152                 shouldUpdateFont = m_combineFontStyle->setFontDescription(description);
153                 break;
154             }
155             
156             float widthDelta = runWidth - emWidth;
157             if (widthDelta < bestFitDelta) {
158                 bestFitDelta = widthDelta;
159                 bestFitDescription = description;
160             }
161         }
162     }
163
164     if (!m_isCombined) {
165         float scaleFactor = std::max(0.4f, emWidth / (emWidth + bestFitDelta));
166         float originalSize = bestFitDescription.computedSize();
167         do {
168             float computedSize = originalSize * scaleFactor;
169             bestFitDescription.setComputedSize(computedSize);
170             shouldUpdateFont = m_combineFontStyle->setFontDescription(bestFitDescription);
171         
172             FontCascade compressedFont(bestFitDescription, style().fontCascade().letterSpacing(), style().fontCascade().wordSpacing());
173             compressedFont.update(fontSelector);
174             
175             glyphOverflow.left = glyphOverflow.top = glyphOverflow.right = glyphOverflow.bottom = 0;
176             float runWidth = RenderText::width(0, textLength(), compressedFont, 0, nullptr, &glyphOverflow);
177             if (runWidth <= emWidth) {
178                 combinedTextWidth = runWidth;
179                 m_isCombined = true;
180                 break;
181             }
182             scaleFactor -= 0.05f;
183         } while (scaleFactor >= 0.4f);
184     }
185
186     if (shouldUpdateFont)
187         m_combineFontStyle->fontCascade().update(fontSelector);
188
189     if (m_isCombined) {
190         static NeverDestroyed<String> objectReplacementCharacterString(&objectReplacementCharacter, 1);
191         RenderText::setRenderedText(objectReplacementCharacterString.get());
192         m_combinedTextWidth = combinedTextWidth;
193         m_combinedTextAscent = glyphOverflow.top;
194         m_combinedTextDescent = glyphOverflow.bottom;
195     }
196 }
197
198 } // namespace WebCore