1 | /* |
2 | * Copyright (C) 2009 Alex Milowski (alex@milowski.com). All rights reserved. |
3 | * Copyright (C) 2016 Igalia S.L. |
4 | * |
5 | * Redistribution and use in source and binary forms, with or without |
6 | * modification, are permitted provided that the following conditions |
7 | * are met: |
8 | * 1. Redistributions of source code must retain the above copyright |
9 | * notice, this list of conditions and the following disclaimer. |
10 | * 2. Redistributions in binary form must reproduce the above copyright |
11 | * notice, this list of conditions and the following disclaimer in the |
12 | * documentation and/or other materials provided with the distribution. |
13 | * |
14 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
15 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
16 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
17 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
18 | * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
19 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
20 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
21 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
22 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
23 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
24 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
25 | */ |
26 | |
27 | #include "config.h" |
28 | #include "RenderMathMLUnderOver.h" |
29 | |
30 | #if ENABLE(MATHML) |
31 | |
32 | #include "MathMLElement.h" |
33 | #include "MathMLOperatorDictionary.h" |
34 | #include "MathMLUnderOverElement.h" |
35 | #include "RenderIterator.h" |
36 | #include "RenderMathMLOperator.h" |
37 | #include <wtf/IsoMallocInlines.h> |
38 | |
39 | namespace WebCore { |
40 | |
41 | WTF_MAKE_ISO_ALLOCATED_IMPL(RenderMathMLUnderOver); |
42 | |
43 | RenderMathMLUnderOver::RenderMathMLUnderOver(MathMLUnderOverElement& element, RenderStyle&& style) |
44 | : RenderMathMLScripts(element, WTFMove(style)) |
45 | { |
46 | } |
47 | |
48 | MathMLUnderOverElement& RenderMathMLUnderOver::element() const |
49 | { |
50 | return static_cast<MathMLUnderOverElement&>(nodeForNonAnonymous()); |
51 | } |
52 | |
53 | static RenderMathMLOperator* horizontalStretchyOperator(const RenderBox& box) |
54 | { |
55 | if (!is<RenderMathMLBlock>(box)) |
56 | return nullptr; |
57 | |
58 | auto* renderOperator = downcast<RenderMathMLBlock>(box).unembellishedOperator(); |
59 | if (!renderOperator) |
60 | return nullptr; |
61 | |
62 | if (!renderOperator->isStretchy() || renderOperator->isVertical() || renderOperator->isStretchWidthLocked()) |
63 | return nullptr; |
64 | |
65 | return renderOperator; |
66 | } |
67 | |
68 | static void fixLayoutAfterStretch(RenderBox& ancestor, RenderMathMLOperator& stretchyOperator) |
69 | { |
70 | stretchyOperator.setStretchWidthLocked(true); |
71 | stretchyOperator.setNeedsLayout(); |
72 | ancestor.layoutIfNeeded(); |
73 | stretchyOperator.setStretchWidthLocked(false); |
74 | } |
75 | |
76 | void RenderMathMLUnderOver::stretchHorizontalOperatorsAndLayoutChildren() |
77 | { |
78 | ASSERT(isValid()); |
79 | ASSERT(needsLayout()); |
80 | |
81 | // We apply horizontal stretchy rules from the MathML spec (sections 3.2.5.8.3 and 3.2.5.8.4), which |
82 | // can be roughly summarized as "stretching opersators to the maximum widths of all children" and |
83 | // minor variations of that algorithm do not affect the result. However, the spec is a bit ambiguous |
84 | // for embellished operators (section 3.2.5.7.3) and different approaches can lead to significant |
85 | // stretch size differences. We made the following decisions: |
86 | // - The unstretched size is the embellished operator width with the <mo> at the core unstretched. |
87 | // - In general, the target size is just the maximum widths of non-stretchy children because the |
88 | // embellishments could make widths significantly larger. |
89 | // - In the edge case when all operators of stretchy, we follow the specification and take the |
90 | // maximum of all unstretched sizes. |
91 | // - The <mo> at the core is stretched to cover the target size, even if the embellished operator |
92 | // might become much wider. |
93 | |
94 | Vector<RenderBox*, 3> embellishedOperators; |
95 | Vector<RenderMathMLOperator*, 3> stretchyOperators; |
96 | bool isAllStretchyOperators = true; |
97 | LayoutUnit stretchWidth; |
98 | |
99 | for (auto* child = firstChildBox(); child; child = child->nextSiblingBox()) { |
100 | if (auto* stretchyOperator = horizontalStretchyOperator(*child)) { |
101 | embellishedOperators.append(child); |
102 | stretchyOperators.append(stretchyOperator); |
103 | } else { |
104 | isAllStretchyOperators = false; |
105 | child->layoutIfNeeded(); |
106 | stretchWidth = std::max(stretchWidth, child->logicalWidth()); |
107 | } |
108 | } |
109 | |
110 | if (isAllStretchyOperators) { |
111 | for (size_t i = 0; i < embellishedOperators.size(); i++) { |
112 | stretchyOperators[i]->resetStretchSize(); |
113 | fixLayoutAfterStretch(*embellishedOperators[i], *stretchyOperators[i]); |
114 | stretchWidth = std::max(stretchWidth, embellishedOperators[i]->logicalWidth()); |
115 | } |
116 | } |
117 | |
118 | for (size_t i = 0; i < embellishedOperators.size(); i++) { |
119 | stretchyOperators[i]->stretchTo(stretchWidth); |
120 | fixLayoutAfterStretch(*embellishedOperators[i], *stretchyOperators[i]); |
121 | } |
122 | } |
123 | |
124 | bool RenderMathMLUnderOver::isValid() const |
125 | { |
126 | // Verify whether the list of children is valid: |
127 | // <munder> base under </munder> |
128 | // <mover> base over </mover> |
129 | // <munderover> base under over </munderover> |
130 | auto* child = firstChildBox(); |
131 | if (!child) |
132 | return false; |
133 | child = child->nextSiblingBox(); |
134 | if (!child) |
135 | return false; |
136 | child = child->nextSiblingBox(); |
137 | switch (scriptType()) { |
138 | case MathMLScriptsElement::ScriptType::Over: |
139 | case MathMLScriptsElement::ScriptType::Under: |
140 | return !child; |
141 | case MathMLScriptsElement::ScriptType::UnderOver: |
142 | return child && !child->nextSiblingBox(); |
143 | default: |
144 | ASSERT_NOT_REACHED(); |
145 | return false; |
146 | } |
147 | } |
148 | |
149 | bool RenderMathMLUnderOver::shouldMoveLimits() |
150 | { |
151 | if (auto* renderOperator = unembellishedOperator()) |
152 | return renderOperator->shouldMoveLimits(); |
153 | return false; |
154 | } |
155 | |
156 | RenderBox& RenderMathMLUnderOver::base() const |
157 | { |
158 | ASSERT(isValid()); |
159 | return *firstChildBox(); |
160 | } |
161 | |
162 | RenderBox& RenderMathMLUnderOver::under() const |
163 | { |
164 | ASSERT(isValid()); |
165 | ASSERT(scriptType() == MathMLScriptsElement::ScriptType::Under || scriptType() == MathMLScriptsElement::ScriptType::UnderOver); |
166 | return *firstChildBox()->nextSiblingBox(); |
167 | } |
168 | |
169 | RenderBox& RenderMathMLUnderOver::over() const |
170 | { |
171 | ASSERT(isValid()); |
172 | ASSERT(scriptType() == MathMLScriptsElement::ScriptType::Over || scriptType() == MathMLScriptsElement::ScriptType::UnderOver); |
173 | auto* secondChild = firstChildBox()->nextSiblingBox(); |
174 | return scriptType() == MathMLScriptsElement::ScriptType::Over ? *secondChild : *secondChild->nextSiblingBox(); |
175 | } |
176 | |
177 | |
178 | void RenderMathMLUnderOver::computePreferredLogicalWidths() |
179 | { |
180 | ASSERT(preferredLogicalWidthsDirty()); |
181 | |
182 | if (!isValid()) { |
183 | m_minPreferredLogicalWidth = m_maxPreferredLogicalWidth = 0; |
184 | setPreferredLogicalWidthsDirty(false); |
185 | return; |
186 | } |
187 | |
188 | if (shouldMoveLimits()) { |
189 | RenderMathMLScripts::computePreferredLogicalWidths(); |
190 | return; |
191 | } |
192 | |
193 | LayoutUnit preferredWidth = base().maxPreferredLogicalWidth(); |
194 | |
195 | if (scriptType() == MathMLScriptsElement::ScriptType::Under || scriptType() == MathMLScriptsElement::ScriptType::UnderOver) |
196 | preferredWidth = std::max(preferredWidth, under().maxPreferredLogicalWidth()); |
197 | |
198 | if (scriptType() == MathMLScriptsElement::ScriptType::Over || scriptType() == MathMLScriptsElement::ScriptType::UnderOver) |
199 | preferredWidth = std::max(preferredWidth, over().maxPreferredLogicalWidth()); |
200 | |
201 | m_minPreferredLogicalWidth = m_maxPreferredLogicalWidth = preferredWidth; |
202 | |
203 | setPreferredLogicalWidthsDirty(false); |
204 | } |
205 | |
206 | LayoutUnit RenderMathMLUnderOver::horizontalOffset(const RenderBox& child) const |
207 | { |
208 | return (logicalWidth() - child.logicalWidth()) / 2; |
209 | } |
210 | |
211 | bool RenderMathMLUnderOver::hasAccent(bool accentUnder) const |
212 | { |
213 | ASSERT(scriptType() == MathMLScriptsElement::ScriptType::UnderOver || (accentUnder && scriptType() == MathMLScriptsElement::ScriptType::Under) || (!accentUnder && scriptType() == MathMLScriptsElement::ScriptType::Over)); |
214 | |
215 | const MathMLElement::BooleanValue& attributeValue = accentUnder ? element().accentUnder() : element().accent(); |
216 | if (attributeValue == MathMLElement::BooleanValue::True) |
217 | return true; |
218 | if (attributeValue == MathMLElement::BooleanValue::False) |
219 | return false; |
220 | RenderBox& script = accentUnder ? under() : over(); |
221 | if (!is<RenderMathMLBlock>(script)) |
222 | return false; |
223 | auto* scriptOperator = downcast<RenderMathMLBlock>(script).unembellishedOperator(); |
224 | return scriptOperator && scriptOperator->hasOperatorFlag(MathMLOperatorDictionary::Accent); |
225 | } |
226 | |
227 | RenderMathMLUnderOver::VerticalParameters RenderMathMLUnderOver::verticalParameters() const |
228 | { |
229 | VerticalParameters parameters; |
230 | |
231 | // By default, we set all values to zero. |
232 | parameters.underGapMin = 0; |
233 | parameters.overGapMin = 0; |
234 | parameters.underShiftMin = 0; |
235 | parameters.overShiftMin = 0; |
236 | parameters.underExtraDescender = 0; |
237 | parameters.overExtraAscender = 0; |
238 | parameters.accentBaseHeight = 0; |
239 | |
240 | const auto& primaryFont = style().fontCascade().primaryFont(); |
241 | auto* mathData = primaryFont.mathData(); |
242 | if (!mathData) { |
243 | // The MATH table specification does not really provide any suggestions, except for some underbar/overbar values and AccentBaseHeight. |
244 | LayoutUnit defaultLineThickness = ruleThicknessFallback(); |
245 | parameters.underGapMin = 3 * defaultLineThickness; |
246 | parameters.overGapMin = 3 * defaultLineThickness; |
247 | parameters.underExtraDescender = defaultLineThickness; |
248 | parameters.overExtraAscender = defaultLineThickness; |
249 | parameters.accentBaseHeight = style().fontMetrics().xHeight(); |
250 | parameters.useUnderOverBarFallBack = true; |
251 | return parameters; |
252 | } |
253 | |
254 | if (is<RenderMathMLBlock>(base())) { |
255 | if (auto* baseOperator = downcast<RenderMathMLBlock>(base()).unembellishedOperator()) { |
256 | if (baseOperator->hasOperatorFlag(MathMLOperatorDictionary::LargeOp)) { |
257 | // The base is a large operator so we read UpperLimit/LowerLimit constants from the MATH table. |
258 | parameters.underGapMin = mathData->getMathConstant(primaryFont, OpenTypeMathData::LowerLimitGapMin); |
259 | parameters.overGapMin = mathData->getMathConstant(primaryFont, OpenTypeMathData::UpperLimitGapMin); |
260 | parameters.underShiftMin = mathData->getMathConstant(primaryFont, OpenTypeMathData::LowerLimitBaselineDropMin); |
261 | parameters.overShiftMin = mathData->getMathConstant(primaryFont, OpenTypeMathData::UpperLimitBaselineRiseMin); |
262 | parameters.useUnderOverBarFallBack = false; |
263 | return parameters; |
264 | } |
265 | if (baseOperator->isStretchy() && !baseOperator->isVertical()) { |
266 | // The base is a horizontal stretchy operator, so we read StretchStack constants from the MATH table. |
267 | parameters.underGapMin = mathData->getMathConstant(primaryFont, OpenTypeMathData::StretchStackGapBelowMin); |
268 | parameters.overGapMin = mathData->getMathConstant(primaryFont, OpenTypeMathData::StretchStackGapAboveMin); |
269 | parameters.underShiftMin = mathData->getMathConstant(primaryFont, OpenTypeMathData::StretchStackBottomShiftDown); |
270 | parameters.overShiftMin = mathData->getMathConstant(primaryFont, OpenTypeMathData::StretchStackTopShiftUp); |
271 | parameters.useUnderOverBarFallBack = false; |
272 | return parameters; |
273 | } |
274 | } |
275 | } |
276 | |
277 | // By default, we just use the underbar/overbar constants. |
278 | parameters.underGapMin = mathData->getMathConstant(primaryFont, OpenTypeMathData::UnderbarVerticalGap); |
279 | parameters.overGapMin = mathData->getMathConstant(primaryFont, OpenTypeMathData::OverbarVerticalGap); |
280 | parameters.underExtraDescender = mathData->getMathConstant(primaryFont, OpenTypeMathData::UnderbarExtraDescender); |
281 | parameters.overExtraAscender = mathData->getMathConstant(primaryFont, OpenTypeMathData::OverbarExtraAscender); |
282 | parameters.accentBaseHeight = mathData->getMathConstant(primaryFont, OpenTypeMathData::AccentBaseHeight); |
283 | parameters.useUnderOverBarFallBack = true; |
284 | return parameters; |
285 | } |
286 | |
287 | void RenderMathMLUnderOver::layoutBlock(bool relayoutChildren, LayoutUnit pageLogicalHeight) |
288 | { |
289 | ASSERT(needsLayout()); |
290 | |
291 | if (!relayoutChildren && simplifiedLayout()) |
292 | return; |
293 | |
294 | if (!isValid()) { |
295 | layoutInvalidMarkup(relayoutChildren); |
296 | return; |
297 | } |
298 | |
299 | if (shouldMoveLimits()) { |
300 | RenderMathMLScripts::layoutBlock(relayoutChildren, pageLogicalHeight); |
301 | return; |
302 | } |
303 | |
304 | recomputeLogicalWidth(); |
305 | |
306 | stretchHorizontalOperatorsAndLayoutChildren(); |
307 | |
308 | ASSERT(!base().needsLayout()); |
309 | ASSERT(scriptType() == MathMLScriptsElement::ScriptType::Over || !under().needsLayout()); |
310 | ASSERT(scriptType() == MathMLScriptsElement::ScriptType::Under || !over().needsLayout()); |
311 | |
312 | LayoutUnit logicalWidth = base().logicalWidth(); |
313 | if (scriptType() == MathMLScriptsElement::ScriptType::Under || scriptType() == MathMLScriptsElement::ScriptType::UnderOver) |
314 | logicalWidth = std::max(logicalWidth, under().logicalWidth()); |
315 | if (scriptType() == MathMLScriptsElement::ScriptType::Over || scriptType() == MathMLScriptsElement::ScriptType::UnderOver) |
316 | logicalWidth = std::max(logicalWidth, over().logicalWidth()); |
317 | setLogicalWidth(logicalWidth); |
318 | |
319 | VerticalParameters parameters = verticalParameters(); |
320 | LayoutUnit verticalOffset; |
321 | if (scriptType() == MathMLScriptsElement::ScriptType::Over || scriptType() == MathMLScriptsElement::ScriptType::UnderOver) { |
322 | verticalOffset += parameters.overExtraAscender; |
323 | over().setLocation(LayoutPoint(horizontalOffset(over()), verticalOffset)); |
324 | if (parameters.useUnderOverBarFallBack) { |
325 | verticalOffset += over().logicalHeight(); |
326 | if (hasAccent()) { |
327 | LayoutUnit baseAscent = ascentForChild(base()); |
328 | if (baseAscent < parameters.accentBaseHeight) |
329 | verticalOffset += parameters.accentBaseHeight - baseAscent; |
330 | } else |
331 | verticalOffset += parameters.overGapMin; |
332 | } else { |
333 | LayoutUnit overAscent = ascentForChild(over()); |
334 | verticalOffset += std::max(over().logicalHeight() + parameters.overGapMin, overAscent + parameters.overShiftMin); |
335 | } |
336 | } |
337 | base().setLocation(LayoutPoint(horizontalOffset(base()), verticalOffset)); |
338 | verticalOffset += base().logicalHeight(); |
339 | if (scriptType() == MathMLScriptsElement::ScriptType::Under || scriptType() == MathMLScriptsElement::ScriptType::UnderOver) { |
340 | if (parameters.useUnderOverBarFallBack) { |
341 | if (!hasAccentUnder()) |
342 | verticalOffset += parameters.underGapMin; |
343 | } else { |
344 | LayoutUnit underAscent = ascentForChild(under()); |
345 | verticalOffset += std::max(parameters.underGapMin, parameters.underShiftMin - underAscent); |
346 | } |
347 | under().setLocation(LayoutPoint(horizontalOffset(under()), verticalOffset)); |
348 | verticalOffset += under().logicalHeight(); |
349 | verticalOffset += parameters.underExtraDescender; |
350 | } |
351 | |
352 | setLogicalHeight(verticalOffset); |
353 | |
354 | layoutPositionedObjects(relayoutChildren); |
355 | |
356 | updateScrollInfoAfterLayout(); |
357 | |
358 | clearNeedsLayout(); |
359 | } |
360 | |
361 | } |
362 | |
363 | #endif // ENABLE(MATHML) |
364 | |