Combine event and touch action regions into a single class
[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/IsoMallocInlines.h>
27 #include <wtf/NeverDestroyed.h>
28
29 namespace WebCore {
30
31 WTF_MAKE_ISO_ALLOCATED_IMPL(RenderCombineText);
32
33 const float textCombineMargin = 1.15f; // Allow em + 15% margin
34
35 RenderCombineText::RenderCombineText(Text& textNode, const String& string)
36     : RenderText(textNode, string)
37     , m_isCombined(false)
38     , m_needsFontUpdate(false)
39 {
40 }
41
42 void 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
63 void RenderCombineText::setRenderedText(const String& text)
64 {
65     RenderText::setRenderedText(text);
66
67     m_needsFontUpdate = true;
68     combineTextIfNeeded();
69 }
70
71 float 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
79 Optional<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
92 String 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
103 void 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