| /* |
| * Copyright (C) 2010 Alex Milowski (alex@milowski.com). All rights reserved. |
| * Copyright (C) 2010 François Sausset (sausset@gmail.com). All rights reserved. |
| * Copyright (C) 2013 Igalia S.L. |
| * |
| * 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 THE COPYRIGHT HOLDERS AND 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 THE COPYRIGHT |
| * OWNER OR 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" |
| |
| #if ENABLE(MATHML) |
| |
| #include "RenderMathMLOperator.h" |
| |
| #include "FontCache.h" |
| #include "FontSelector.h" |
| #include "MathMLNames.h" |
| #include "PaintInfo.h" |
| #include "RenderBlockFlow.h" |
| #include "RenderText.h" |
| #include "ScaleTransformOperation.h" |
| #include "TransformOperations.h" |
| #include <wtf/MathExtras.h> |
| |
| namespace WebCore { |
| |
| using namespace MathMLNames; |
| |
| // FIXME: The OpenType MATH table contains information that should override this table (http://wkbug/122297). |
| static RenderMathMLOperator::StretchyCharacter stretchyCharacters[13] = { |
| { 0x28 , 0x239b, 0x239c, 0x239d, 0x0 }, // left parenthesis |
| { 0x29 , 0x239e, 0x239f, 0x23a0, 0x0 }, // right parenthesis |
| { 0x5b , 0x23a1, 0x23a2, 0x23a3, 0x0 }, // left square bracket |
| { 0x2308, 0x23a1, 0x23a2, 0x23a2, 0x0 }, // left ceiling |
| { 0x230a, 0x23a2, 0x23a2, 0x23a3, 0x0 }, // left floor |
| { 0x5d , 0x23a4, 0x23a5, 0x23a6, 0x0 }, // right square bracket |
| { 0x2309, 0x23a4, 0x23a5, 0x23a5, 0x0 }, // right ceiling |
| { 0x230b, 0x23a5, 0x23a5, 0x23a6, 0x0 }, // right floor |
| { 0x7b , 0x23a7, 0x23aa, 0x23a9, 0x23a8 }, // left curly bracket |
| { 0x7c , 0x23aa, 0x23aa, 0x23aa, 0x0 }, // vertical bar |
| { 0x2016, 0x2016, 0x2016, 0x2016, 0x0 }, // double vertical line |
| { 0x7d , 0x23ab, 0x23aa, 0x23ad, 0x23ac }, // right curly bracket |
| { 0x222b, 0x2320, 0x23ae, 0x2321, 0x0 } // integral sign |
| }; |
| |
| RenderMathMLOperator::RenderMathMLOperator(MathMLElement& element) |
| : RenderMathMLBlock(element) |
| , m_stretchHeight(0) |
| , m_operator(0) |
| , m_operatorType(Default) |
| , m_stretchyCharacter(nullptr) |
| { |
| } |
| |
| RenderMathMLOperator::RenderMathMLOperator(MathMLElement& element, UChar operatorChar) |
| : RenderMathMLBlock(element) |
| , m_stretchHeight(0) |
| , m_operator(convertHyphenMinusToMinusSign(operatorChar)) |
| , m_operatorType(Default) |
| , m_stretchyCharacter(nullptr) |
| { |
| } |
| |
| bool RenderMathMLOperator::isChildAllowed(const RenderObject&, const RenderStyle&) const |
| { |
| return false; |
| } |
| |
| static const float gOperatorExpansion = 1.2f; |
| |
| float RenderMathMLOperator::expandedStretchHeight() const |
| { |
| return m_stretchHeight * gOperatorExpansion; |
| } |
| |
| void RenderMathMLOperator::stretchToHeight(int height) |
| { |
| if (m_stretchHeight == height) |
| return; |
| |
| m_stretchHeight = height; |
| updateStyle(); |
| } |
| |
| void RenderMathMLOperator::styleDidChange(StyleDifference diff, const RenderStyle* oldStyle) |
| { |
| RenderMathMLBlock::styleDidChange(diff, oldStyle); |
| updateFromElement(); |
| } |
| |
| FloatRect RenderMathMLOperator::glyphBoundsForCharacter(UChar character) |
| { |
| GlyphData data = style()->font().glyphDataForCharacter(character, false); |
| return data.fontData->boundsForGlyph(data.glyph); |
| } |
| |
| float RenderMathMLOperator::glyphHeightForCharacter(UChar character) |
| { |
| return glyphBoundsForCharacter(character).height(); |
| } |
| |
| float RenderMathMLOperator::advanceForCharacter(UChar character) |
| { |
| // Hyphen minus is always replaced by the minus sign in rendered text. |
| GlyphData data = style()->font().glyphDataForCharacter(convertHyphenMinusToMinusSign(character), false); |
| return data.fontData->widthForGlyph(data.glyph); |
| } |
| |
| void RenderMathMLOperator::computePreferredLogicalWidths() |
| { |
| ASSERT(preferredLogicalWidthsDirty()); |
| |
| UChar stretchedCharacter; |
| bool allowStretching = shouldAllowStretching(stretchedCharacter); |
| if (!allowStretching) { |
| RenderMathMLBlock::computePreferredLogicalWidths(); |
| return; |
| } |
| |
| float maximumGlyphWidth = advanceForCharacter(stretchedCharacter); |
| for (unsigned index = 0; index < WTF_ARRAY_LENGTH(stretchyCharacters); ++index) { |
| if (stretchyCharacters[index].character != stretchedCharacter) |
| continue; |
| |
| StretchyCharacter& character = stretchyCharacters[index]; |
| if (character.topGlyph) |
| maximumGlyphWidth = std::max(maximumGlyphWidth, advanceForCharacter(character.topGlyph)); |
| if (character.extensionGlyph) |
| maximumGlyphWidth = std::max(maximumGlyphWidth, advanceForCharacter(character.extensionGlyph)); |
| if (character.bottomGlyph) |
| maximumGlyphWidth = std::max(maximumGlyphWidth, advanceForCharacter(character.bottomGlyph)); |
| if (character.middleGlyph) |
| maximumGlyphWidth = std::max(maximumGlyphWidth, advanceForCharacter(character.middleGlyph)); |
| m_maxPreferredLogicalWidth = m_minPreferredLogicalWidth = maximumGlyphWidth; |
| return; |
| } |
| |
| m_maxPreferredLogicalWidth = m_minPreferredLogicalWidth = maximumGlyphWidth; |
| } |
| |
| // FIXME: It's cleaner to only call updateFromElement when an attribute has changed. The body of |
| // this method should probably be moved to a private stretchHeightChanged or checkStretchHeight |
| // method. Probably at the same time, addChild/removeChild methods should be made to work for |
| // dynamic DOM changes. |
| void RenderMathMLOperator::updateFromElement() |
| { |
| RenderElement* savedRenderer = element().renderer(); |
| |
| // Destroy our current children |
| destroyLeftoverChildren(); |
| |
| // Since we share a node with our children, destroying our children may set our node's |
| // renderer to 0, so we need to restore it. |
| element().setRenderer(savedRenderer); |
| |
| auto newStyle = RenderStyle::create(); |
| newStyle.get().inheritFrom(style()); |
| newStyle.get().setDisplay(FLEX); |
| |
| RenderMathMLBlock* container = new RenderMathMLBlock(element()); |
| // This container doesn't offer any useful information to accessibility. |
| container->setIgnoreInAccessibilityTree(true); |
| container->setStyle(std::move(newStyle)); |
| |
| addChild(container); |
| RenderText* text; |
| if (m_operator) |
| text = new RenderText(document(), String(&m_operator, 1)); |
| else |
| text = new RenderText(document(), element().textContent().replace(hyphenMinus, minusSign).impl()); |
| |
| // If we can't figure out the text, leave it blank. |
| if (text) |
| container->addChild(text); |
| |
| updateStyle(); |
| setNeedsLayoutAndPrefWidthsRecalc(); |
| } |
| |
| bool RenderMathMLOperator::shouldAllowStretching(UChar& stretchedCharacter) |
| { |
| if (equalIgnoringCase(element().getAttribute(MathMLNames::stretchyAttr), "false")) |
| return false; |
| |
| if (m_operator) { |
| stretchedCharacter = m_operator; |
| return true; |
| } |
| |
| // FIXME: This does not handle surrogate pairs (http://wkbug.com/122296/). |
| String opText = element().textContent(); |
| stretchedCharacter = 0; |
| for (unsigned i = 0; i < opText.length(); ++i) { |
| // If there's more than one non-whitespace character in this node, then don't even try to stretch it. |
| if (stretchedCharacter && !isSpaceOrNewline(opText[i])) |
| return false; |
| |
| if (!isSpaceOrNewline(opText[i])) |
| stretchedCharacter = opText[i]; |
| } |
| |
| return stretchedCharacter; |
| } |
| |
| // FIXME: We should also look at alternate characters defined in the OpenType MATH table (http://wkbug/122297). |
| RenderMathMLOperator::StretchyCharacter* RenderMathMLOperator::findAcceptableStretchyCharacter(UChar character) |
| { |
| StretchyCharacter* stretchyCharacter = 0; |
| const int maxIndex = WTF_ARRAY_LENGTH(stretchyCharacters); |
| for (int index = 0; index < maxIndex; ++index) { |
| if (stretchyCharacters[index].character == character) { |
| stretchyCharacter = &stretchyCharacters[index]; |
| break; |
| } |
| } |
| |
| // If we didn't find a stretchy character set for this character, we don't know how to stretch it. |
| if (!stretchyCharacter) |
| return 0; |
| |
| float height = glyphHeightForCharacter(stretchyCharacter->topGlyph) + glyphHeightForCharacter(stretchyCharacter->bottomGlyph); |
| if (stretchyCharacter->middleGlyph) |
| height += glyphHeightForCharacter(stretchyCharacter->middleGlyph); |
| |
| if (height > expandedStretchHeight()) |
| return 0; |
| |
| return stretchyCharacter; |
| } |
| |
| void RenderMathMLOperator::updateStyle() |
| { |
| ASSERT(firstChild()); |
| if (!firstChild()) |
| return; |
| |
| UChar stretchedCharacter; |
| bool allowStretching = shouldAllowStretching(stretchedCharacter); |
| |
| float stretchedCharacterHeight = style()->fontMetrics().floatHeight(); |
| m_isStretched = allowStretching && expandedStretchHeight() > stretchedCharacterHeight; |
| |
| // Sometimes we cannot stretch an operator properly, so in that case, we should just use the original size. |
| m_stretchyCharacter = m_isStretched ? findAcceptableStretchyCharacter(stretchedCharacter) : 0; |
| if (!m_stretchyCharacter) |
| m_isStretched = false; |
| } |
| |
| int RenderMathMLOperator::firstLineBoxBaseline() const |
| { |
| if (m_isStretched) |
| return expandedStretchHeight() * 2 / 3 - (expandedStretchHeight() - m_stretchHeight) / 2; |
| return RenderMathMLBlock::firstLineBoxBaseline(); |
| } |
| |
| void RenderMathMLOperator::computeLogicalHeight(LayoutUnit logicalHeight, LayoutUnit logicalTop, LogicalExtentComputedValues& computedValues) const |
| { |
| if (m_isStretched) |
| logicalHeight = expandedStretchHeight(); |
| RenderBox::computeLogicalHeight(logicalHeight, logicalTop, computedValues); |
| } |
| |
| LayoutRect RenderMathMLOperator::paintCharacter(PaintInfo& info, UChar character, const LayoutPoint& origin, CharacterPaintTrimming trim) |
| { |
| GlyphData data = style()->font().glyphDataForCharacter(character, false); |
| FloatRect glyphBounds = data.fontData->boundsForGlyph(data.glyph); |
| |
| LayoutRect glyphPaintRect(origin, LayoutSize(glyphBounds.x() + glyphBounds.width(), glyphBounds.height())); |
| glyphPaintRect.setY(origin.y() + glyphBounds.y()); |
| |
| // In order to have glyphs fit snugly with one another we snap the connecting edges to pixel boundaries |
| // and trim off one pixel. The pixel trim is to account for fonts that have edge pixels that have less |
| // than full coverage. These edge pixels can introduce small seams between connected glyphs |
| FloatRect clipBounds = info.rect; |
| switch (trim) { |
| case TrimTop: |
| glyphPaintRect.shiftYEdgeTo(glyphPaintRect.y().ceil() + 1); |
| clipBounds.shiftYEdgeTo(glyphPaintRect.y()); |
| break; |
| case TrimBottom: |
| glyphPaintRect.shiftMaxYEdgeTo(glyphPaintRect.maxY().floor() - 1); |
| clipBounds.shiftMaxYEdgeTo(glyphPaintRect.maxY()); |
| break; |
| case TrimTopAndBottom: |
| LayoutUnit temp = glyphPaintRect.y() + 1; |
| glyphPaintRect.shiftYEdgeTo(temp.ceil()); |
| glyphPaintRect.shiftMaxYEdgeTo(glyphPaintRect.maxY().floor() - 1); |
| clipBounds.shiftYEdgeTo(glyphPaintRect.y()); |
| clipBounds.shiftMaxYEdgeTo(glyphPaintRect.maxY()); |
| break; |
| } |
| |
| // Clipping the enclosing IntRect avoids any potential issues at joined edges. |
| GraphicsContextStateSaver stateSaver(*info.context); |
| info.context->clip(clipBounds); |
| |
| info.context->drawText(style()->font(), TextRun(&character, 1), origin); |
| |
| return glyphPaintRect; |
| } |
| |
| void RenderMathMLOperator::fillWithExtensionGlyph(PaintInfo& info, const LayoutPoint& from, const LayoutPoint& to) |
| { |
| ASSERT(m_stretchyCharacter); |
| ASSERT(m_stretchyCharacter->extensionGlyph); |
| ASSERT(from.y() < to.y()); |
| |
| GraphicsContextStateSaver stateSaver(*info.context); |
| |
| FloatRect glyphBounds = glyphBoundsForCharacter(m_stretchyCharacter->extensionGlyph); |
| |
| // Clipping the extender region here allows us to draw the bottom extender glyph into the |
| // regions of the bottom glyph without worrying about overdraw (hairy pixels) and simplifies later clipping. |
| IntRect clipBounds = info.rect; |
| clipBounds.shiftYEdgeTo(from.y()); |
| clipBounds.shiftMaxYEdgeTo(to.y()); |
| info.context->clip(clipBounds); |
| |
| // Trimming may remove up to two pixels from the top of the extender glyph, so we move it up by two pixels. |
| float offsetToGlyphTop = glyphBounds.y() + 2; |
| LayoutPoint glyphOrigin = LayoutPoint(from.x(), from.y() - offsetToGlyphTop); |
| FloatRect lastPaintedGlyphRect(from, FloatSize()); |
| |
| while (lastPaintedGlyphRect.maxY() < to.y()) { |
| lastPaintedGlyphRect = paintCharacter(info, m_stretchyCharacter->extensionGlyph, glyphOrigin, TrimTopAndBottom); |
| glyphOrigin.setY(glyphOrigin.y() + lastPaintedGlyphRect.height()); |
| |
| // There's a chance that if the font size is small enough the glue glyph has been reduced to an empty rectangle |
| // with trimming. In that case we just draw nothing. |
| if (lastPaintedGlyphRect.isEmpty()) |
| break; |
| } |
| } |
| |
| void RenderMathMLOperator::paint(PaintInfo& info, const LayoutPoint& paintOffset) |
| { |
| RenderMathMLBlock::paint(info, paintOffset); |
| |
| if (info.context->paintingDisabled() || info.phase != PaintPhaseForeground) |
| return; |
| |
| if (!m_isStretched && !m_stretchyCharacter) { |
| RenderMathMLBlock::paint(info, paintOffset); |
| return; |
| } |
| |
| GraphicsContextStateSaver stateSaver(*info.context); |
| info.context->setFillColor(style()->visitedDependentColor(CSSPropertyColor), style()->colorSpace()); |
| |
| ASSERT(m_stretchyCharacter->topGlyph); |
| ASSERT(m_stretchyCharacter->bottomGlyph); |
| |
| // We are positioning the glyphs so that the edge of the tight glyph bounds line up exactly with the edges of our paint box. |
| LayoutPoint operatorTopLeft = ceiledIntPoint(paintOffset + location()); |
| FloatRect topGlyphBounds = glyphBoundsForCharacter(m_stretchyCharacter->topGlyph); |
| LayoutPoint topGlyphOrigin(operatorTopLeft.x(), operatorTopLeft.y() - topGlyphBounds.y()); |
| LayoutRect topGlyphPaintRect = paintCharacter(info, m_stretchyCharacter->topGlyph, topGlyphOrigin, TrimBottom); |
| |
| FloatRect bottomGlyphBounds = glyphBoundsForCharacter(m_stretchyCharacter->bottomGlyph); |
| LayoutPoint bottomGlyphOrigin(operatorTopLeft.x(), operatorTopLeft.y() + offsetHeight() - (bottomGlyphBounds.height() + bottomGlyphBounds.y())); |
| LayoutRect bottomGlyphPaintRect = paintCharacter(info, m_stretchyCharacter->bottomGlyph, bottomGlyphOrigin, TrimTop); |
| |
| if (m_stretchyCharacter->middleGlyph) { |
| // Center the glyph origin between the start and end glyph paint extents. Then shift it half the paint height toward the bottom glyph. |
| FloatRect middleGlyphBounds = glyphBoundsForCharacter(m_stretchyCharacter->middleGlyph); |
| LayoutPoint middleGlyphOrigin(operatorTopLeft.x(), topGlyphOrigin.y() + y()); |
| middleGlyphOrigin.moveBy(LayoutPoint(0, (bottomGlyphPaintRect.y() - topGlyphPaintRect.maxY()) / 2.0)); |
| middleGlyphOrigin.moveBy(LayoutPoint(0, middleGlyphBounds.height() / 2.0)); |
| |
| LayoutRect middleGlyphPaintRect = paintCharacter(info, m_stretchyCharacter->middleGlyph, middleGlyphOrigin, TrimTopAndBottom); |
| fillWithExtensionGlyph(info, topGlyphPaintRect.minXMaxYCorner(), middleGlyphPaintRect.minXMinYCorner()); |
| fillWithExtensionGlyph(info, middleGlyphPaintRect.minXMaxYCorner(), bottomGlyphPaintRect.minXMinYCorner()); |
| } else |
| fillWithExtensionGlyph(info, topGlyphPaintRect.minXMaxYCorner(), bottomGlyphPaintRect.minXMinYCorner()); |
| } |
| |
| void RenderMathMLOperator::paintChildren(PaintInfo& paintInfo, const LayoutPoint& paintOffset, PaintInfo& paintInfoForChild, bool usePrintRect) |
| { |
| if (m_isStretched) |
| return; |
| RenderMathMLBlock::paintChildren(paintInfo, paintOffset, paintInfoForChild, usePrintRect); |
| } |
| |
| } |
| |
| #endif |